搜索

首页  >  问答  >  正文

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

我们有一个中央服务器和几个地区服务器,例如d1、d2、d3、d4、d5。

有一些要进行复制的表。为了简单起见,让我们假设我们有一个名为tblFoo的表,它存在于d1、d2、d3、d4、d5和c上,并且在所有这些服务器上都具有相同的结构。规则很简单:

目标是确保如果对d服务器上的tblFoo进行更改(插入、更新、删除),则也应立即在c服务器上进行更改。这对于插入操作非常好用(因为id,即pkFooID,根据定义具有auto_increment属性)。这对于更新和删除操作也有效,但我们对这些操作有一些担忧。以下是(简化版本的)代码:

namespace App\ORM;

use Cake\ORM\Query as ORMQuery;
// 其他一些use语句

class Query extends ORMQuery
{
    // 大量的代码...

    /**
     * 重写同名方法以处理与c的同步
     */
    public function execute()
    {
        // 一些表需要复制。如果是这样的表,则我们需要执行一些额外的步骤。否则,我们只需调用父方法
        if (($this->_repository->getIgnoreType() || (!in_array($this->type(), ['select']))) && $this->isReplicate() && ($this->getConnection()->configName() !== 'c')) {
            // 获取表
            $table = $this->_repository->getTable();
            // 复制查询
            $replica = clone $this;
            // 将复制品的连接设置为c,因为我们需要在中央应用地区的更改
            $replica->setParentConnectionType('d')->setConnection(ConnectionManager::get('c'));
            $replica->setIgnoreType($this->_repository->getIgnoreType());
            // 首先执行复制品,因为我们需要引用c的ID而不是反过来
            $replica->execute();
            // 如果是插入操作,我们还需要处理ID
            if (!empty($this->clause('insert'))) {
                // 加载主键的名称,以便稍后使用它来查找最大值
                $primaryKey = $this->_repository->getPrimaryKey();
                // 获取最高的ID值,这将始终是一个正数,因为我们已经在复制品上执行了查询
                $firstID = $replica->getConnection()
                                   ->execute("SELECT LAST_INSERT_ID() AS {$primaryKey}")
                                   ->fetchAll('assoc')[0][$primaryKey];

                // 获取列
                $columns = $this->clause('values')->getColumns();
                // 为了添加主键
                $columns[] = $primaryKey;
                // 然后用这个调整后的数组覆盖插入子句
                $this->insert($columns);
                // 获取值
                $values = $this->clause('values')->getValues();
                // 以及它们的数量
                $count = count($values);
                // 可能已经将多个行插入到复制品中作为此查询的一部分,我们需要复制所有它们的ID,而不是假设有一个单独的插入记录
                for ($index = 0; $index < $count; $index++) {
                    // 将适当的ID值添加到所有要插入的记录中
                    $values[$index][$primaryKey] = $firstID + $index;
                }
                // 用这个调整后的数组覆盖值子句,其中包含PK值
                $this->clause('values')->values($values);
            }
        }
        if ($this->isQueryDelete) {
            $this->setIgnoreType(false);
        }
        // 无论如何都执行查询,无论它是复制表还是非复制表
        // 如果是复制表,则我们已经在if块中对查询进行了调整
        return parent::execute();
    }

}

担心的问题是:如果我们在d1上执行update或delete语句,其条件可以在其他地区服务器(d2、d3、d4、d5)上满足,那么在d1上正确执行update和delete语句,但一旦相同的语句在d1上执行,我们可能会意外地从c服务器更新/删除其他地区的记录。

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

没有复制行为的表将正常执行execute,上述限制仅适用于具有复制行为的表,例如我们示例中的tblFoo。

问题

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

P粉448346289P粉448346289325 天前421

全部回复(1)我来回复

  • P粉333186285

    P粉3331862852024-01-11 13:56:49

    这是我解决问题的方法。

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

    <?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("在调用{$action}时,需要传递一个数组");
                } elseif (!$this->isValidReplicateCondition($conditions)) {
                    throw new \Exception("传递给{$action}操作的条件不安全,您需要指定主键或带有=或IN运算符的外键");
                }
            }
        }
    
        public function query()
        {
            return new Query($this->getConnection(), $this);
        }
    }

    对于Query类,我们有一个isReplicate方法,触发我们需要的验证,where方法已被重写,以确保条件被正确验证:

    /**
         * 当且仅当:
         * - _repository已正确初始化
         * - _repository具有复制行为
         * - 当前连接不是c
         */
        protected function isReplicate()
        {
            if (($this->type() !== 'select') && ($this->getConnection()->configName() === 'c') && ($this->getParentConnectionType() !== 'd')) {
                throw new \Exception('副本表必须始终从区域连接更改');
            }
            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);
        }

    回复
    0
  • 取消回复