在 Yii2 中设置非规范化列的最佳实践
Best practice for setting non-normalized columns in Yii2
向所有 Yii2 标准化极客提问。
在 Yii2 中设置非规范化列的最佳位置在哪里?
例如,我有模型 Customer、Branch、CashRegister 和 交易。
在一个完美的世界中,在一个完全标准化的数据库中,我们的 Transaction 模型将只有 cashregister_id
,CashRegister 将存储 branch_id
,而 分支 将存储 customer_id
。然而,由于性能问题,我们发现自己有时不得不使用包含以下内容的 非规范化 Transaction 模型:
- cashregister_id
- branch_id
- 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_id
和 customer_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 中的索引(
branches
和 cashregisters
)定义为 UNIQUE
。然而,这在 MySQL. 中不是必需的
- 复合外键将处理数据 integrity/consistency。示例:如果您有一个带有
branch_id = 2
和 customer_id = 1
的分支条目 - 您将无法插入带有 branch_id = 2
和 customer_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.
向所有 Yii2 标准化极客提问。
在 Yii2 中设置非规范化列的最佳位置在哪里?
例如,我有模型 Customer、Branch、CashRegister 和 交易。
在一个完美的世界中,在一个完全标准化的数据库中,我们的 Transaction 模型将只有 cashregister_id
,CashRegister 将存储 branch_id
,而 分支 将存储 customer_id
。然而,由于性能问题,我们发现自己有时不得不使用包含以下内容的 非规范化 Transaction 模型:
- cashregister_id
- branch_id
- 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_id
和 customer_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 中的索引(
branches
和cashregisters
)定义为UNIQUE
。然而,这在 MySQL. 中不是必需的
- 复合外键将处理数据 integrity/consistency。示例:如果您有一个带有
branch_id = 2
和customer_id = 1
的分支条目 - 您将无法插入带有branch_id = 2
和customer_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.