我们有一个中央服务器和几个地区服务器,例如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运算符?
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号
这是我解决问题的方法。
具有复制行为的模型执行以下验证:
<?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); }