如何在执行覆盖时验证字段和条件?

How can I validate fields and criteria at execute override?

我们有一台c(中央)服务器和几台d(区服务器),如d1、d2、d3、d4、d5。

有一些table要复制。为了简单起见,假设我们有一个 tblFoo table 也存在于 d1、d2、d3、d4、d5 和 c 上,并且它们具有相同的结构。规则很简单:

目标是确保如果对 d 服务器的 tblFooinsertupdatedelete)进行了更改,那么它应该是也在 c 服务器上迅速完成。这对 insert 非常有效(因为 id pkFooID 根据定义具有 auto_increment 属性)。这也适用于 updatedelete,但我们对此有些担心。这是(简化版)代码:

namespace App\ORM;

use Cake\ORM\Query as ORMQuery;
// Some other use statements

class Query extends ORMQuery
{
    //Lots of stuff...

    /**
     * Overrides a method with the same name to handle synchonizations with c
     */
    public function execute()
    {
        //Some tables need replication. If this is such a table, then we need to perform some extra steps. Otherwise we would just call the parent
        //Method
        if (($this->_repository->getIgnoreType() || (!in_array($this->type(), ['select']))) && $this->isReplicate() && ($this->getConnection()->configName() !== 'c')) {
            //Getting the table
            $table = $this->_repository->getTable();
            //Replicating the query
            $replica = clone $this;
            //Setting the connection of the replica to c, because we need to apply the district changes on central
            $replica->setParentConnectionType('d')->setConnection(ConnectionManager::get('c'));
            $replica->setIgnoreType($this->_repository->getIgnoreType());
            //We execute the replica first, because we will need to refer to c IDs and not the other way around
            $replica->execute();
            //If this is an insert, then we need to handle the ids as well
            if (!empty($this->clause('insert'))) {
                //We load the primary key's name to use it later to find the maximum value
                $primaryKey = $this->_repository->getPrimaryKey();
                //We get the highest ID value, which will always be a positive number, because we have already executed the query at the replica
                $firstID = $replica->getConnection()
                                   ->execute("SELECT LAST_INSERT_ID() AS {$primaryKey}")
                                   ->fetchAll('assoc')[0][$primaryKey];

                //We get the columns
                $columns = $this->clause('values')->getColumns();
                //In order to add the primary key
                $columns[] = $primaryKey;
                //And then override the insert clause with this adjusted array
                $this->insert($columns);
                //We get the values
                $values = $this->clause('values')->getValues();
                //And their count
                $count = count($values);
                //There could be multiple rows inserted already into the replica as part of this query, we need to replicate all their IDs, without
                //assuming that there is a single inserted record
                for ($index = 0; $index < $count; $index++) {
                    //We add the proper ID value into all of the records to be inserted
                    $values[$index][$primaryKey] = $firstID + $index;
                }
                //We override the values clause with this adjusted array, which contains PK values as well
                $this->clause('values')->values($values);
            }
        }
        if ($this->isQueryDelete) {
            $this->setIgnoreType(false);
        }
        //We nevertheless execute the query in any case, independently of whether it was a replicate table
        //If it was a replicate table, then we have already made adjustments to the query in the if block
        return parent::execute();
    }

}

担心如下:如果我们在 d1 上执行 updatedelete 语句,其条件将被另一个地区服务器上的记录满足( d2, d3, d4, d5), 那么我们最终会在 d1 上正确执行 updatedelete 语句,但是一旦在 d1 上执行相同的语句,我们可能会不小心 update/delete其他区的记录来自c服务器。

要解决此问题,建议的解决方案是验证语句并在不满足以下条件之一时抛出异常:

不具有复制行为的表将正常执行 execute,上述限制仅对具有复制行为的 table 有效,例如我们示例中的 tblFoo

问题

如何在我的执行覆盖中验证 update/delete 查询,以便只能搜索主键或外键,并且只能使用 = 或 IN 运算符?

我就是这样解决问题的。

具有复制行为的模型执行如下验证

<?php

namespace App\ORM;

use Cake\ORM\Table as ORMTable;

class Table extends ORMTable
{
    protected static $replicateTables = [
        'inteacherkeyjoin',
    ];

    public function isValidReplicateCondition(array $conditions)
    {
        return count(array_filter($conditions, function ($v, $k) {
            return (bool) preg_match('/^[\s]*[pf]k(' . implode('|', self::$replicateTables) . ')id[\s]*((in|=).*)?$/i', strtolower(($k === intval($k)) ? $v : $k));
        }, ARRAY_FILTER_USE_BOTH)) > 0;
    }

    public function validateUpdateDeleteCondition($action, $conditions)
    {
        if ($this->behaviors()->has('Replicate')) {
            if (!is_array($conditions)) {
                throw new \Exception("When calling {$action} for replicate tables, you need to pass an array");
            } elseif (!$this->isValidReplicateCondition($conditions)) {
                throw new \Exception("Unsafe condition was passed to the {$action} action, you need to specify primary keys or foreign keys with = or IN operators");
            }
        }
    }

    public function query()
    {
        return new Query($this->getConnection(), $this);
    }
}

对于 Query class 我们有一个 isReplicate 方法来触发我们需要的验证并且 where 方法已被覆盖以确保条件是正确验证:

    /**
     * True if and only if:
     * - _repository is properly initialized
     * - _repository has the Replicate behavior
     * - The current connection is not c
     */
    protected function isReplicate()
    {
        if (($this->type() !== 'select') && ($this->getConnection()->configName() === 'c') && ($this->getParentConnectionType() !== 'd')) {
            throw new \Exception('Replica tables must always be changed from a district connection');
        }
        if (in_array($this->type(), ['update', 'delete'])) {
            $this->_repository->validateUpdateDeleteCondition($this->type(), $this->conditions);
        }

        return ($this->_repository && $this->_repository->behaviors()->has('Replicate'));
    }

    public function where($conditions = null, $types = [], $overwrite = false)
    {
        $preparedConditions = is_array($conditions) ? $conditions : [$conditions];
        $this->conditions = array_merge($this->conditions, $preparedConditions);

        return parent::where($conditions, $types, $overwrite);
    }