В этой главе мы обсудим, как работать с реляционными базами данных из Clojure. Большую часть описания займет библиотека clojure.java.jdbc и ее надстройки. Вы узнаете, какие проблемы обычно сопровождают доступ к базам и как их решать в Clojure.

Реляционные БД

В разработке на бекенде базы данных занимают важное место. Говоря упрощенно, любая программа сводится к обработке данных. Конечно, данные могут поступать не только из баз, но и сети и файлов. Однако в целом доступ к информации регулируют базы данных — специальные программы, сложные, но с богатыми возможностями.

Базы данных, или сокращенно БД, бывают разных видов. Они различаются архитектурой, способом хранения информации, протоколом работы с клиентом. Некоторые базы работают только на клиенте, потому что не предлагают сетевой интерфейс. Другие хранят только текст и оставляют вывод типов на усмотрение клиента. В этой главе мы не ставим цель охватить как можно больше СУБД и способов для работы с ними. Наоборот, сфокусируем внимание на том, что вас ждет в реальном проекте. Скорей всего это будет классическая реляционная БД вроде PostgreSQL или MySQL. О них мы и будем говорить.

Реляционные базы данных называют так из за модели реляционной алгебры. Это изящный математический аппарат с набором операций, например выборкой, проекцией, декартовым произведением и другими. Из аппарата следуют строгие правила о том, как работает та или иная операция. Важно помнить, что базы данных работают не спонтанно, а по четким алгебраическим правилам. Из этих же правил следуют нормальные формы (первая, вторая и третья), доказать которые можно аналитически, а не на глаз.

Мы не ставим цель учить реляционную алгебру с самых азов. Обратитесь к статье в Википедии и другим книгам, где ее рассматривают без привязки к конкретной БД.

Устройство

Перейдем к понятиям, более привычным программисту. Базы хранят содержимое в таблицах. Запись в таблице называется кортежем и состоит из отдельных полей. Поля могут быть разного типа. Состав полей и их порядок одинаков в рамках таблицы. Не может быть так, что в первой записи два поля, а во второй три. Если нужно указать, что в поле нет значения, в него пишут специальное “пустое”, чаще всего NULL.

У записи есть особое поле, которое называют первичным ключом, Primary key. Ключ однозначно идентифицирует запись в таблице. Не может быть двух записей с одинаковым ключом. Чаще всего роль ключа играет число с автонумерацией, но может быть и почтовым адресом или полными именем. В редких случаях ключ может быть составным, то есть определяться парой полей, например полным именем и годом рождения. В этом случае мы допускаем, что в таблице могут быть полные тезки разных годов рождения или люди одного года с разными именами, но не то и другое вместе.

Внешним ключом называется поле, которые ссылается на первичный ключ другой таблицы. Для краткости его называют ссылкой. Примером ссылки может быть поле user_id в таблице профилей, которое указывает на поле id из таблицы пользователей.

create table users(
    id   serial primary key,
    name text not null
);

create table profiles(
    id      serial primary key,
    user_id integer not null references users(id),
    avatar  text
);

В данном случае ссылка отличается от простого числового поля. Ссылка гарантирует, что такой пользователь действительно существует. Если бы это было просто число, гарантии целостности бы не было.

Кроме целостности, внешний ключ поддерживает реакцию на удаление. По умолчанию нельзя удалить сущность, на которую кто-то ссылается. Однако можно задать правило, что связанная сущность тоже удаляется. Так, при удалении пользователя удалится и его профиль, ведь он не нужен нам сам по себе. В другом случае ссылка на удаленную сущность станет NULL, если это разрешено в свойствах поля.

Из университета мы знаем, что связи бывают разных типов: один к одному или ко многим, многие ко многим. Профиль, который ссылается на пользователя — пример связи один к одному. Несколько заказов у пользователя — один ко многим. Тип связи легко задать ограничением на поле ссылки. Если в таблице профилей сделать поле user_id уникальным, не получится создать два профиля с одинаковым user_id. Это именно то, что нам требуется — чтобы у одного пользователя не могло быть два профиля.

Запросы

База данных обращается с миром через SQL — структурированный язык запросов. Это текст, в котором описаны наши намерения — прочитать таблицу, добавить запись, обновить поле. Запросы имеют четкую структуру, которая чаще всего зависит от главного оператора. К ним относятся SELECT, INSERT, UPDATE, DELETE — стандартные CRUD-операции над записями.

Существуют несколько стандартов SQL, обозначенных годами, когда они были приняты: SQL’92, ‘99, ‘2003 и другие. Как правило, каждая база данных поддерживает стандарт определенного года вдобавок с расширениями — возможностями, которые не входят в стандарт. При чтении документации обращайте внимание на то, относится ли функциональность к стандарту или же это частное решение конкретной БД.

В аббревиатуре SQL последняя буква означает language, язык. Важно помнить, что это не язык программирования: в нем нельзя описать что-либо алгоритмом или переменной. Говоря точнее, SQL не является полным по Тьюрингу языком: вы не сможете построить SQL-выражение на нем самом. Поэтому SQL-запросы часто приходится строить по частям в полноценном (полным по Тьюрингу) языке, например Java, Python, Clojure.

Современные базы данных предлагают встроенные языки и операторы, чтобы это исправить. Так, в PostgreSQL вам доступен блок DO, где работают переменные, циклы и даже перехват исключений. Но это частное решение, которое не входит в стандарт.

(Продолжение следует)