[Из песочницы] Проектирование в PostgreSQL документо-ориентированного API (Часть 1)
Данная статья является переводом, оригинальная статья находится вот здесь, автор Rob Conery.
Postgres, как многие знают, поддерживает JSON как тип хранения данных, а с выходом 9.4, Postgres теперь поддерживает хранение JSON в виде jsonb — бинарного формата.
Это прекрасные новости для тех, кто хочет шагнуть дальше простого «хранения JSON как текста». jsonb теперь поддерживает индексирование с использованием GIN индекса, а также имеет специальный оператор запросов, который позволяет получить преимущества GIN индекса.
Кого это заботит?
Было весело открыть для себя jsonb в Postgres и увидеть на что он способен. Что, по-своему, является проблемой: это только знакомство и размышления, чтобы выполнить какую-то работу, этого мало.
Подразумевается тот факт, что в других системах (таких как RethinkDB) имеется огромный, уже встроенный функционал, чтобы помочь Вам сохранять документы, посылать запросы к этим документам и проводить оптимизацию. У Postgres тоже имеются некоторые интересные возможности в этом направлении, но написание запросов «из коробки» самую малость… недостаточно, если быть честным.
Посмотрим на этот запрос:
select document_field -> 'my_key' from my_docs
where document_field @> '{"some_key" : "some_value"}';
Он слегка приоткрывает странность того момента, когда дело доходит до JSON и Postgres: это все строки. Очевидно, SQL не умеет распознавать JSON, поэтому придется форматировать его как строку. Что в свою очередь означает, что работать с JSON напрямую в SQL это боль. Конечно же, если у вас есть хорошее средство составления запросов, то проблема упрощается в определенной степени…, но она все еще существует.
Более того, хранение документа довольно свободно. Использовать одно поле, которое является jsonb? Или несколько полей в более крупной табличной структуре? Это все зависит только от Вас, что, конечно же, неплохо, но слишком большая свобода выбора тоже может быть парализующим фактором.
Так почему же стоит беспокоиться по этому поводу? Если Вы хотите использовать документо-ориентированную базу данных, то используйте документо-ориентированную базу данных. Я согласен с этим…, но существует одна по-настоящему неотразимая причина использовать Postgres (как минимум для меня)…
Postgres ACID-совместима. Значит можно рассчитывать что она запишет Ваши данные и, вполне вероятно, не потеряет их.
Кроме того, Postgres является реляционной базой данных, что в свою очередь означает, что при желании со временем перейти на более строгую схему возможно. Есть определенное количество причин, по которым может возникнуть желание выбрать Postgres, на данный же момент, предположим что выбор сделан и пришло время начать работать с документами и jsonb.
Лучший API
Что касается меня, я хотел бы видеть больше функций, которые поддерживают идею работы с документами. На данный момент мы имеем встроенные инструменты, которые позволяют иметь дело с JSON типами, но ничего, что поддерживает более высокий уровень абстракции.
Это вовсе не означает, что мы не сможем построить такой API своими руками… Как это сделал я. Начинается…
Документо-ориентированная таблица
Я хочу хранить документы в таблице, которая содержит мета-данные, равно как и дополнительные пути работы с информацией, а именно: Полнотекстовый поиск (Full Text Search).
Структура самой таблицы может варьироваться — почему бы нам не построить эту абстракцию! Давайте начнем с этого:
create table my_docs(
id serial primary key,
body jsonb not null,
search tsvector,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
)
Здесь будет присутствовать определенное дублирование. Документ, сам по себе, будет храниться в поле body, включая id, который, в свою очередь, хранится как первичный ключ (это необходимо, так как это все еще Postgres). Я использую дублирование, впрочем, по следующим причинам:
- Этот API принадлежит мне и я могу быть уверен что все синхронизировано
- Так делается в документо-ориентированных системах
Сохраняя документ
Чего бы я хотел от save_document функции…
- Создавать таблицы на лету
- Создавать подобающие индексы
- Создавать timestamp’ы и поисковое поле (для полнотекстового индекса)
Этого можно достичь, сделав свою собственную функцию save_document и, ради веселья, я буду использовать PLV8 — javascript внутри базы данных. На самом деле я создам две функции — одна будет специфическим образом создавать мою таблицу, другая же будет сохранять сам документ.
Первая, create_document_table:
create function create_document_table(name varchar, out boolean)
as $$
var sql = "create table " + name + "(" +
"id serial primary key," +
"body jsonb not null," +
"search tsvector," +
"created_at timestamptz default now() not null," +
"updated_at timestamptz default now() not null);";
plv8.execute(sql);
plv8.execute("create index idx_" + name + " on docs using GIN(body jsonb_path_ops)");
plv8.execute("create index idx_" + name + "_search on docs using GIN(search)");
return true;
$$ language plv8;
Эта функция создает таблицу и подобающие индексы — один для jsonb поля в нашей документо-ориентированной таблице, другой для tsvector полнотекстового индекса. Обратите внимание, что я строю SQL строки на лету и выполняю с помощью plv8 — вот как стоит вести себя с javascript в Postgres.
Далее, давайте создадим нашу save_document функцию:
create function save_document(tbl varchar, doc_string jsonb)
returns jsonb
as $$
var doc = JSON.parse(doc_string);
var result = null;
var id = doc.id;
var exists = plv8.execute("select table_name from information_schema.tables where table_name = $1", tbl)[0];
if(!exists){
plv8.execute("select create_document_table('" + tbl + "');");
}
if(id){
result = plv8.execute("update " + tbl + " set body=$1, updated_at = now() where id=$2 returning *;",doc_string,id);
}else{
result = plv8.execute("insert into " + tbl + "(body) values($1) returning *;", doc_string);
id = result[0].id;
doc.id = id;
result = plv8.execute("update " + tbl + " set body=$1 where id=$2 returning *",JSON.stringify(doc),id);
}
return result[0] ? result[0].body : null;
$$ language plv8;
Я уверен что эта функция выглядит немного странно, но если прочитать ее построчно, можно понять некоторые вещи. Но зачем вызывается JSON.parse ()?
Это связано с тем, что Postgres'овский тип jsonb здесь не является JSONом — он строка. За пределами нашего PLV8 участка это все еще мир Postgres и он работает с JSON как со строкой (храня его в jsonb в бинарном формате). Таким образом, когда наш документ попадает в нашу функцию в виде строки, которую необходимо парсить, если мы хотим работать с ней, как с JSON объектом в javascript’е.
В случае insert’a можно заметить, что мне приходится синхронизировать ID документа с первичным ключом, который был создан. Немного громоздко, но это хорошо работает.
В итоге, можно заметить, что при insert’е оригинала, так же как и при update, в качестве входного аргумента для plv8.execute подается doc_string. Это так же связано с тем, что с JSON значениями необходимо обращаться как со строками в Postgres.
Это действительно может сбивать с толку. Если я попробую подать на вход doc (наш JSON.parsed объект), то он будет преобразован plv8 в [Object object]. Что странно.
Более того, если я попробую вернуть javascript объект от этой функции (предположим, нашу doc переменную) — я получу ошибку, что это неправильный формат для типа JSON. Что загоняет в ступор.
В качестве результата я просто возвращаю данные из результата выполнения запроса — и это строка, хотите — верьте, хотите — нет, и я просто могу передать ее напрямую как результат. Здесь стоит отметить, что все результаты plv8.execute возвращаются в виде элементов, с которыми можно работать как с javascript объектами.
Результат
Работает реально очень хорошо! И быстро. Если Вы хотите попробовать его в деле — Вам надо будет установить PLV8 расширение и затем написать ваш запрос согласно:
create extension plv8;
select * from save_document('test_run', '{"name" : "Test"}');
Вы должны увидеть новую таблицу и новую запись в этой таблице:
Планы на будущее
В следующей статье я добавлю некоторые дополнительные особенности, а именно:
- Автоматический update search поля
- Вставка множества документов, используя массивы
Это хорошее начало!