Какими средствами обеспечивается целостность данных в реляционных СУБД и как это реализуется в Go?

Ответ

Целостность данных в реляционных СУБД — это гарантия точности и согласованности данных. Она обеспечивается на нескольких уровнях:

  1. Ограничения (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
    );
  2. Транзакции: Группа операций, которая выполняется как единое целое. Транзакции обладают свойствами 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()
    }
  3. Нормализация: Процесс организации таблиц и столбцов для минимизации избыточности данных и улучшения их целостности.

  4. Триггеры: Хранимые процедуры, которые автоматически выполняются в ответ на определенные события (INSERT, UPDATE, DELETE) в таблице. Используются для реализации сложной бизнес-логики на уровне БД.

  5. Целостность на уровне приложения: Помимо механизмов БД, бизнес-логика в 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 пишут, как мантру, а смысла не объясняют. Смысл в том, чтобы твоё приложение не просрало деньги пользователей из-за дурацкой ошибки.