我们有一个中央服务器和几个地区服务器,例如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粉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); }