Вопрос ко всем фанатам нормализации Yii2 там.
Где лучше всего установить ненормализованные столбцы в Yii2?
Например, у меня есть модели Покупатель, Ветка, Кассовый аппарат, а также Сделка.
В идеальном мире и в совершенно нормализованной базе данных наши Сделка модель будет иметь только cashregister_id
, Кассовый аппарат будет хранить branch_id
и Ветка будет хранить customer_id
, Однако из-за проблем с производительностью мы иногда вынуждены ненормализованная Сделка модель, содержащая следующее:
При создании транзакции я хочу сохранить все 3 значения. настройка
$transaction->branch_id = $transaction->cashRegister->branch_id;
$transaction->customer_id = $transaction->cashRegister->branch->customer_id;
Однако в контроллере не чувствую себя правильно.
Одним из решений будет сделать это в aftersave () в Сделка модель и сделать эти столбцы только для чтения. Но это также кажется лучше, но не идеально.
Я хотел бы знать, что является наилучшей практикой или где лучше всего устанавливать эти дубликаты столбцов, чтобы обеспечить сохранение целостности данных?
Ниже приведено решение только для БД.
Я предполагаю, что ваши отношения:
Соответствующая схема может быть:
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 поддерживает составные внешние ключи.)Если вы хотите, чтобы избыточные данные поддерживались базой данных, вы можете использовать следующие триггеры:
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
Таблица.
У меня была похожая проблема однажды и я 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()
… и так далее).