Ответ
Да, использовал генерируемые (generated) колонки в MySQL для упрощения логики приложения и обеспечения целостности данных на уровне БД.
Пример из реального проекта (MySQL 8.0):
У нас была таблица invoice_items для хранения позиций в счете. Вместо того чтобы вычислять итоговую сумму (total) в коде PHP при каждом чтении, мы вынесли это в генерируемую колонку.
CREATE TABLE invoice_items (
id INT PRIMARY KEY AUTO_INCREMENT,
invoice_id INT NOT NULL,
unit_price DECIMAL(10, 2) NOT NULL,
quantity INT NOT NULL,
discount_percent DECIMAL(5, 2) DEFAULT 0.00,
-- VIRTUAL колонка (вычисляется на лету)
discount_amount DECIMAL(10, 2) GENERATED ALWAYS AS (unit_price * quantity * discount_percent / 100) VIRTUAL,
-- STORED колонка (сохраняется на диске, можно индексировать)
line_total DECIMAL(10, 2) GENERATED ALWAYS AS ((unit_price * quantity) - discount_amount) STORED,
INDEX idx_invoice_id (invoice_id),
FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE
);
Как это работает в PHP-приложении:
// При вставке данных указываем только базовые поля
$stmt = $pdo->prepare("INSERT INTO invoice_items (invoice_id, unit_price, quantity, discount_percent) VALUES (?, ?, ?, ?)");
$stmt->execute([$invoiceId, 99.99, 5, 10.00]);
// При выборке `line_total` и `discount_amount` доступны как обычные поля
$stmt = $pdo->query("SELECT id, unit_price, quantity, line_total FROM invoice_items WHERE invoice_id = $invoiceId");
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($items as $item) {
echo "Item ID: {$item['id']}, Total: {$item['line_total']}n";
// line_total уже вычислен и готов к использованию
}
Практические соображения:
- VIRTUAL vs STORED: Использовал
VIRTUAL(по умолчанию) для значений, которые редко используются или часто обновляются, чтобы не тратить место на диске.STOREDприменял для колонок, которые часто используются вWHEREилиORDER BY, так как их можно проиндексировать, что значительно ускоряет поиск. - Переносимость: Хотя синтаксис похож в PostgreSQL (
GENERATED ALWAYS AS) и SQLite, в старых версиях MySQL (< 5.7) эта функция недоступна. Поэтому всегда проверял версию СУБД перед проектированием схемы. - Альтернатива в коде: В проектах, где нельзя было полагаться на генерируемые колонки (например, при использовании облачных БД с ограничениями), реализовывал аналогичную логику через геттеры в моделях Eloquent/Doctrine или триггеры в БД.
Ответ 18+ 🔞
А, ну это же про эти самые generated columns в MySQL! Ёпта, реально полезная штука, когда надоедает в коде одно и то же вычислять по десять раз на дню. Сам недавно на проекте с инвойсами такую же дичь реализовывал, прямо как в твоём примере.
Смотри, какая история была. У нас там таблица с позициями заказа, и каждый раз, когда нужно было итоговую сумму по строке получить, приходилось в коде писать эту формулу: цена * количество минус скидка. И везде, блядь, одно и то же — и в API-ответе, и в отчёте, и при валидации. Чисто технически — терпения ноль ебать, однообразная хуйня.
Решили вынести эту логику прямо в базу, на уровень схемы. Создали колонку line_total как GENERATED ALWAYS AS. И знаешь что? Жизнь наладилась! Вставляешь только базовые данные — цену, количество, процент скидки — а сумма сама, бля, появляется, как по волшебству. В коде теперь просто берёшь её как обычное поле, даже думать не надо.
// Раньше было вот это дерьмо в каждой модели или сервисе
$lineTotal = ($item['unit_price'] * $item['quantity']) * (1 - $item['discount_percent'] / 100);
// А теперь просто
$lineTotal = $item['line_total']; // Всё, пиздец, красота!
Но тут, чувак, есть важный момент, про который многие забывают — разница между VIRTUAL и STORED. Это, бля, принципиально!
- VIRTUAL — это как обещание. Колонка вычисляется на лету, каждый раз при запросе. Места на диске не жрёт, но если формула сложная и вызывается в тяжёлых отчётах — может подтормаживать. Использовал для всяких промежуточных расчётов, которые редко нужны.
- STORED — вот это уже серьёзно. Значение вычисляется один раз при вставке или обновлении и хранится на диске. А раз хранится — его можно, внимание, проиндексировать! Если ты эту колонку часто используешь в условиях
WHEREили сортировке (ORDER BY), то без индекса будет пиздец как медленно. Сделал еёSTORED— создал индекс — и запросы полетели в разы быстрее. Но за всё надо платить: место на диске.
С миграциями, правда, иногда забавно выходит. Помню, пытался на одном продакшене, где MySQL версии 5.6 ещё стоял, такую колонку добавить. А там, ёпта, эта фича не поддерживается! Ну я и накрылся медным тазом. Пришлось откатывать миграцию и лезть в код, делать через геттер в модели. С тех пор всегда в начале проекта версию БД проверяю, чтобы потом не было мучительно больно.
Альтернативы, конечно, есть. Можно ту же логику в геттер модели Eloquent запихнуть — тоже удобно. Или, если совсем параноик, на триггерах в БД сделать. Но generated columns — это самый декларативный и чистый способ, на мой взгляд. Как будто сама база данных тебе говорит: «Расслабься, чувак, я всё посчитаю». Главное — понять, где VIRTUAL, а где STORED нужен, и не пытаться использовать это на древних версиях.