在Yii2中设置非标准化列的最佳做法

时间:2018-07-04 11:50:25

标签: php mysql yii2 database-normalization

对所有Yii2标准化怪胎的问题。

在Yii2中设置非标准化列的最佳位置在哪里?

例如,我有模型客户分支收银机交易。 在一个完美的世界中,在一个完美标准化的数据库中,我们的 Transaction 模型将仅包含cashregister_id CashRegister 将存储branch_id,并且分支将存储customer_id。但是,由于性能问题,我们有时有时不得不使用未规范化 交易模型,其中包含以下内容:

  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()中执行此操作,并将这些列设置为只读。但这似乎也更好,但并不完美。

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

2 个答案:

答案 0 :(得分:1)

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

class TransactionsManager extends Component {

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

然后,您不直接创建或更新Transaction模型,而是始终使用此组件并将所有逻辑封装在其中。这样,ActiveRecord的工作方式就类似于数据表示形式,并且不包含任何高级业务逻辑。在某些情况下,它看起来比$model->load($data) && $model->save()更为复杂,但毕竟,当您将所有逻辑都放在一个地方并且不需要调试save()调用链(一个模型运行{ save()中具有不同模型的{1}},其在afterSave()中具有不同模型的save(),依此类推)。

答案 1 :(得分:1)

以下是纯DB解决方案。

我认为您的关系是:

  • 客户有很多分支机构
  • 分支机构有很多收银机
  • 收银机有很多交易

对应的模式可以是:

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)
);

(注意:这应该是您的问题的一部分-因此我们无需猜测。)

如果要在branch_id表中包括冗余列(customer_idtransactions),则应使它们成为外键的一部分。但是首先,您需要在customer_id表中包含一个cashregisters列,并将其作为外键的一部分。

扩展模式为:

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)
);

注意:

  • 任何外键约束都需要在子表(引用)和父表(引用)中建立索引,以支持约束检查。键中给定的列顺序使我们可以定义每个表仅包含一个索引的模式。
  • 外键应始终引用父表中的唯一键。但是,在此示例中,引用列的组成(至少)是隐式唯一的,因为它包含主键。在几乎所有其他RDBMS中,您都需要将“中间”表(branchescashregisters)中的索引定义为UNIQUE。但是,在MySQL中这不是必需的。
  • 复合外键将负责数据的完整性/一致性。示例:如果您的分支条目中包含branch_id = 2customer_id = 1-您将无法插入带有branch_id = 2customer_id = 3的现金出纳机,因为这会违反关键约束。
  • 您可能需要更多索引来查询。很可能您将需要cashregisters(branch_id)transactions(cashregister_id)。使用这些索引,您甚至可能不需要更改您的ORM关系代码。 (尽管AFAIK Yii 支持复合外键。)
  • 您可以定义诸如“ 客户有很多交易”之类的关系。以前,您需要使用“ 有很多通过”,涉及两个中间/桥表。在许多情况下,这将为您节省两个联接。

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

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表。