Лучшие практики для установки ненормализованных столбцов в Yii2

Вопрос ко всем фанатам нормализации Yii2 там.

Где лучше всего установить ненормализованные столбцы в Yii2?

Например, у меня есть модели Покупатель, Ветка, Кассовый аппарат, а также Сделка.
В идеальном мире и в совершенно нормализованной базе данных наши Сделка модель будет иметь только cashregister_id, Кассовый аппарат будет хранить branch_idи Ветка будет хранить customer_id, Однако из-за проблем с производительностью мы иногда вынуждены ненормализованная Сделка модель, содержащая следующее:

  1. cashregister_id
  2. branch_id
  3. Пользовательский ИД

При создании транзакции я хочу сохранить все 3 значения. настройка

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

Однако в контроллере не чувствую себя правильно.

Одним из решений будет сделать это в aftersave () в Сделка модель и сделать эти столбцы только для чтения. Но это также кажется лучше, но не идеально.

Я хотел бы знать, что является наилучшей практикой или где лучше всего устанавливать эти дубликаты столбцов, чтобы обеспечить сохранение целостности данных?

2

Решение

Ниже приведено решение только для БД.

Я предполагаю, что ваши отношения:

  • У клиента много филиалов
  • В филиале много кассовых аппаратов
  • Кассовый аппарат имеет много транзакций

Соответствующая схема может быть:

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_id) в transactions Таблица, вы должны сделать их частью внешнего ключа. Но сначала вам нужно будет включить 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)
);

Заметки:

  • Любое ограничение внешнего ключа нуждается в индексе в дочерней (ссылающейся) и родительской (ссылающейся) таблице, которая может поддерживать проверку ограничения. Данный порядок столбцов в ключах позволяет нам определять схему только с одним индексом на таблицу.
  • Внешний ключ всегда должен ссылаться на уникальный ключ в родительской таблице. Однако в этом примере состав указанных столбцов (по крайней мере) неявно уникален, поскольку он содержит первичный ключ. Практически в любой другой СУБД вам необходимо определить индексы в «средних» таблицах (branches а также cashregisters) как UNIQUE, Это, однако, не обязательно в MySQL.
  • Составные внешние ключи позаботятся о целостности / согласованности данных. Пример: если у вас есть ветка с branch_id = 2 а также customer_id = 1 — вы не сможете вставить кассовый branch_id = 2 а также customer_id = 3потому что это нарушит ограничение внешнего ключа.
  • Вам, вероятно, понадобится больше индексов для ваших запросов. Скорее всего вам понадобится cashregisters(branch_id) а также transactions(cashregister_id), С этими индексами вам может даже не потребоваться изменить код отношения ORM. (хотя AFAIK Yii поддерживает составные внешние ключи.)
  • Вы можете определить отношения каку клиента много транзакцийMsgstr «Раньше вам нужно было использовать»имеет много через«, включающий две промежуточные / промежуточные таблицы. Во многих случаях это спасет вас от двух соединений.

Если вы хотите, чтобы избыточные данные поддерживались базой данных, вы можете использовать следующие триггеры:

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 Таблица.

1

Другие решения

У меня была похожая проблема однажды и я 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()… и так далее).

1