在 Yii2 中设置非规范化列的最佳实践

Best practice for setting non-normalized columns in Yii2

向所有 Yii2 标准化极客提问。

在 Yii2 中设置非规范化列的最佳位置在哪里?

例如,我有模型 CustomerBranchCashRegister交易。 在一个完美的世界中,在一个完全标准化的数据库中,我们的 Transaction 模型将只有 cashregister_idCashRegister 将存储 branch_id,而 分支 将存储 customer_id。然而,由于性能问题,我们发现自己有时不得不使用包含以下内容的 非规范化 Transaction 模型:

  1. cashregister_id
  2. branch_id
  3. customer_id

创建交易时,我想存储所有 3 个值。设置

$transaction->branch_id = $transaction->cashRegister->branch_id;
$transaction->customer_id = $transaction->cashRegister->branch->customer_id;

但是在控制器中感觉不对。

一个解决方案是在 Transaction 模型的 aftersave() 中执行此操作,并将这些列设置为只读。但这也似乎更好但并不完美。

我想知道最佳做法是什么,或者设置这些重复列的最佳位置在哪里,以确保保持数据完整性?

我曾经遇到过类似的问题,使用 afterSave()beforeSave() 一开始看起来是一个很好的解决方案,但最终导致难以维护意大利面条代码。我最终创建了单独的组件来管理此类关系。类似于:

class TransactionsManager extends Component {

    public function createTransaction(TransactionInfo $info, CashRegister $register) {
        // magic
    }
}

那么您不是直接创建或更新 Transaction 模型,您一直在使用该组件并将所有逻辑封装在其中。那么 ActiveRecord 的工作方式更像是一种数据表示,不包含任何高级业务逻辑。它在某些情况下看起来比 $model->load($data) && $model->save() 更复杂,但毕竟当您将所有逻辑都放在一个地方并且您不需要调试 save() 调用链(一个模型运行 save()afterSave() 中运行不同模型的 save()afterSave() 中运行不同模型的 save()... 等等)。

以下是仅数据库的解决方案。

我假设你的关系是:

  • 一个客户有很多分行
  • 一个网点有多台收银机
  • 一个收银机有很多笔交易

对应的模式可以是:

create table customers (
    customer_id int auto_increment,
    customer_data text,
    primary key (customer_id)
);

create table branches (
    branch_id int auto_increment,
    customer_id int not null,
    branch_data text,
    primary key (branch_id),
    index (customer_id),
    foreign key (customer_id) references customers(customer_id)
);

create table cashregisters (
    cashregister_id int auto_increment,
    branch_id int not null,
    cashregister_data text,
    primary key (cashregister_id),
    index (branch_id),
    foreign key (branch_id) references branches(branch_id)
);

create table transactions (
    transaction_id int auto_increment,
    cashregister_id int not null,
    transaction_data text,
    primary key (transaction_id),
    index (cashregister_id),
    foreign key (cashregister_id) references cashregisters(cashregister_id)
);

(注意:这应该是您问题的一部分 - 所以我们不需要猜测。)

如果要在 transactions table 中包含冗余列(branch_idcustomer_id),您应该将它们作为外键的一部分。但首先您需要在 cashregisters table 中包含一个 customer_id 列,并将其作为外键的一部分。

扩展架构为:

create table customers (
    customer_id int auto_increment,
    customer_data text,
    primary key (customer_id)
);

create table branches (
    branch_id int auto_increment,
    customer_id int not null,
    branch_data text,
    primary key (branch_id),
    index (customer_id, branch_id),
    foreign key (customer_id) references customers(customer_id)
);

create table cashregisters (
    cashregister_id int auto_increment,
    branch_id int not null,
    customer_id int not null,
    cashregister_data text,
    primary key (cashregister_id),
    index (customer_id, branch_id, cashregister_id),
    foreign key (customer_id, branch_id)
        references branches(customer_id, branch_id)
);

create table transactions (
    transaction_id int auto_increment,
    cashregister_id int not null,
    branch_id int not null,
    customer_id int not null,
    transaction_data text,
    primary key (transaction_id),
    index (customer_id, branch_id, cashregister_id),
    foreign key (customer_id, branch_id, cashregister_id)
        references cashregisters(customer_id, branch_id, cashregister_id)
);

备注:

  • 任何外键约束都需要在子(引用)和父(引用)中有一个索引table,它可以支持约束检查。键中给定的列顺序允许我们定义每个 table.
  • 只有一个索引的模式
  • 外键应始终引用父项中的唯一键 table。然而,在这个例子中,引用列的组合(至少)是隐式唯一的,因为它包含主键。在几乎任何其他 RDBMS 中,您都需要将 "middle" table 中的索引(branchescashregisters)定义为 UNIQUE。然而,这在 MySQL.
  • 中不是必需的
  • 复合外键将处理数据 integrity/consistency。示例:如果您有一个带有 branch_id = 2customer_id = 1 的分支条目 - 您将无法插入带有 branch_id = 2customer_id = 3 的收银机,因为这会违反外国键约束。
  • 您的查询可能需要更多索引。您很可能需要 cashregisters(branch_id)transactions(cashregister_id)。使用这些索引,您甚至可能不需要更改 ORM 关系代码。 (尽管 AFAIK Yii 支持复合外键。)
  • 您可以定义类似“客户有很多交易”的关系。以前您需要使用“has many through”,涉及两个 middle/bridge table。在许多情况下,这将为您节省两次连接。

如果希望冗余数据由数据库维护,可以使用如下触发器:

create trigger cashregisters_before_insert
before insert on cashregisters for each row
    set new.customer_id = (
        select b.customer_id
        from branches b
        where b.branch_id = new.branch_id
    )
;

delimiter $$
create trigger transactions_before_insert
before insert on transactions for each row
begin
    declare new_customer_id, new_branch_id int;
    select c.customer_id, c.branch_id into new_customer_id, new_branch_id
        from cashregisters c
        where c.cashregister_id = new.cashregister_id;
    set new.customer_id = new_customer_id;
    set new.branch_id   = new_branch_id;
end $$
delimiter ;

现在您可以在不定义冗余值的情况下插入新条目:

insert into cashregisters (branch_id, cashregister_data) values
    (2, 'cashregister 1'),
    (1, 'cashregister 2');

insert into transactions (cashregister_id, transaction_data) values
    (2, 'transaction 1'),
    (1, 'transaction 2');

查看演示:https://www.db-fiddle.com/f/fE7kVxiTcZBX3gfA81nJzE/0

如果您的业务逻辑允许更新关系,您应该使用 ON UPDATE CASCADE 扩展您的外键。这将通过关系链进行更改,直至 transactions table.