Ответ
Целостность данных в реляционных СУБД — это гарантия точности и согласованности данных. Она обеспечивается на нескольких уровнях:
-
Ограничения (Constraints): Правила, заданные на уровне схемы данных.
PRIMARY KEY: Уникально идентифицирует каждую запись в таблице.FOREIGN KEY: Связывает данные в одной таблице с данными в другой, обеспечивая ссылочную целостность.UNIQUE: Гарантирует, что все значения в столбце или группе столбцов уникальны.NOT NULL: Требует, чтобы столбец не содержалNULLзначений.CHECK: Проверяет, что значения в столбце соответствуют определенному условию.
CREATE TABLE users ( id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, age INT CHECK (age >= 18) ); CREATE TABLE orders ( id SERIAL PRIMARY KEY, user_id INT NOT NULL, -- При удалении пользователя, его заказы также будут удалены FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); -
Транзакции: Группа операций, которая выполняется как единое целое. Транзакции обладают свойствами ACID (Atomicity, Consistency, Isolation, Durability), которые гарантируют, что база данных переходит из одного согласованного состояния в другое.
В Go транзакции управляются через пакет
database/sql. Использованиеdefer tx.Rollback()является идиоматичным и безопасным подходом.func transferMoney(db *sql.DB, fromID, toID int, amount float64) error { tx, err := db.Begin() if err != nil { return err } // defer tx.Rollback() гарантирует откат, если что-то пойдет не так. // Если tx.Commit() выполнится успешно, Rollback() вернет ошибку, которую можно проигнорировать. defer tx.Rollback() _, err = tx.Exec("UPDATE accounts SET balance = balance - $1 WHERE id = $2", amount, fromID) if err != nil { return err } _, err = tx.Exec("UPDATE accounts SET balance = balance + $1 WHERE id = $2", amount, toID) if err != nil { return err } // Если все операции успешны, фиксируем транзакцию return tx.Commit() } -
Нормализация: Процесс организации таблиц и столбцов для минимизации избыточности данных и улучшения их целостности.
-
Триггеры: Хранимые процедуры, которые автоматически выполняются в ответ на определенные события (INSERT, UPDATE, DELETE) в таблице. Используются для реализации сложной бизнес-логики на уровне БД.
-
Целостность на уровне приложения: Помимо механизмов БД, бизнес-логика в Go-приложении также должна проводить валидацию данных перед их записью в базу.
Ответ 18+ 🔞
Давай разберём эту тему про целостность данных, а то некоторые думают, что это просто так, накидал табличек и поехали. Нет, блядь, это целая наука, чтобы твоя база не превратилась в свалку, где заказ привязан к удалённому пользователю, а возраст клиента минус пять лет.
Смотри, как это всё работает, на самом деле.
Первая линия обороны — это ограничения (Constraints). Это такие железные правила прямо в схеме таблицы. База сама будет орать, если их нарушить.
PRIMARY KEY— это типа паспорта для записи. Два одинаковых — нихуя, ошибка.FOREIGN KEY— вот это важная хуйня! Связывает таблицы. Можно сделать так, что если удаляешь пользователя, то все его заказы автоматом летят в корзину (ON DELETE CASCADE). Или наоборот, не даст удалить, пока заказы есть. Красота!UNIQUE— чтобы, например, почта не повторялась. Одна жопа — один аккаунт.NOT NULL— очевидная вещь, но сколько раз видел, что поле "имя" вдругNULL. Кто ты, призрак?CHECK— вот тут можно свою логику впихнуть. "Возраст >= 18". Попробуй запиши семнадцать — получишь в бубен.
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL, -- и не NULL, и уникальная, сука
age INT CHECK (age >= 18) -- только для взрослых, детский сад нахуй
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL,
-- Магия! Удалили юзера — и заказы его подчистую!
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
Вторая штука — транзакции. Это, блядь, святое. Представь: нужно перевести деньги с одного счёта на другой. Если списал с первого, а на второй зачислить не успел — свет отключили. Деньги испарились, пипец. Вот чтобы такого не было, нужны транзакции с их ACID-приблудами (атомарность, согласованность, изоляция, долговечность). Всё или ничего.
В Go это выглядит примерно так. Запомни идиому с defer tx.Rollback() — это гениально. Сначала готовим откат, а если всё прошло ок, то делаем коммит. Rollback после успешного коммита просто вернёт ошибку, её можно проигнорировать. Зато если посередине операции пиздец случится — всё откатится само, как по маслу.
func transferMoney(db *sql.DB, fromID, toID int, amount float64) error {
tx, err := db.Begin()
if err != nil {
return err
}
// Страховка от пиздеца! Ставим откат сразу.
defer tx.Rollback()
_, err = tx.Exec("UPDATE accounts SET balance = balance - $1 WHERE id = $2", amount, fromID)
if err != nil {
return err // Тут сработает defer и откатит списание
}
_, err = tx.Exec("UPDATE accounts SET balance = balance + $1 WHERE id = $2", amount, toID)
if err != nil {
return err // И тут тоже откатит
}
// Если дошли сюда — всё чики-пуки, фиксируем.
return tx.Commit()
}
Ну и дальше по списку:
- Нормализация — это чтобы не хранить одно и то же в десяти местах. Адрес клиента в заказе, в профиле, в доставке... Потом он поменял — и бегай, обновляй везде. Нахуй так делать? Разнеси по разным таблицам, свяжи ключами.
- Триггеры — мощная, но опасная хуйня. Автоматическая процедура, которая сработает при обновлении записи. Например, запись в лог или пересчёт какого-то общего поля. Пользоваться осторожно, а то можно незаметно такую логику накрутить, что потом сам не разберёшься.
- Целостность на уровне приложения — это вообще основа. База — последний рубеж. Всю валидацию ("почта похожа на почту", "сумма заказа положительная") нужно делать в своём коде на Go, до того как сунешь данные в SQL-запрос. Представь, что база — это строгий бухгалтер, который только правила проверяет. А твоё приложение — это менеджер, который должен принести ему уже правильные и подписанные бумажки.
Вот так, коротко и без лишнего пафоса. А то про ACID пишут, как мантру, а смысла не объясняют. Смысл в том, чтобы твоё приложение не просрало деньги пользователей из-за дурацкой ошибки.