Временное проектирование базы данных, с поворотом (живые и черновые строки)
Я смотрю на реализацию версионирования объектов с дополнительным поворотом необходимости иметь как живые, так и черновые объекты, и мог бы использовать информацию, полученную от кого-то, имеющего опыт в этом, так как я начинаю задумываться, возможно ли это вообще без потенциально ужасных хаков.
Я расскажу об этом в статьях с тегами для примера, но мой пример использования немного более общий (включает медленно меняющиеся измерения - http://en.wikipedia.org/wiki/Slowly_changing_dimension).
Предположим, у вас есть таблица сообщений, таблица тегов и таблица post2tag:
posts (
id
)
tags (
id
)
post2tag (
post_id fkey posts(id),
tag_id fkey tags(id)
)
Мне нужна пара вещей:
- Возможность точно показать, как выглядело сообщение в произвольный момент времени, в том числе для удаленных строк.
- Следите за тем, кто что редактирует, для полного аудита.
- Необходим набор материализованных представлений ("живых" таблиц) для сохранения ссылочной целостности (т. Е. Регистрация должна быть прозрачной для разработчиков).
- Должен быть соответственно быстрым для живых и последних черновиков строк.
- Возможность иметь черновик поста вместе с живым постом.
Я изучал различные варианты. Пока что лучшее, что я придумал (без точек #4/#5), немного похоже на гибридную установку типа 6 SCD, но вместо текущего логического значения есть материализованное представление для текущей строки. Для всех намерений и целей это выглядит так:
posts (
id pkey,
public,
created_at,
updated_at,
updated_by
)
post_revs (
id,
rev pkey,
public,
created_at,
created_by,
deleted_at
)
tags (
id pkey,
public,
created_at,
updated_at,
updated_by
)
tag_revs (
id,
public,
rev pkey,
created_at,
created_by,
deleted_at
)
post2tag (
post_id fkey posts(id),
tag_id fkey tags(id),
public,
created_at,
updated_at,
updated_by
)
post2tag_revs (
post_id,
tag_id,
post_rev fkey post_revs(rev), -- the rev when the relation started
tag_rev fkey tag_revs(rev), -- the rev when the relation started
public,
created_at,
created_by,
deleted_at,
pkey (post_rev, tag_rev)
)
Я использую pg_temporal для поддержания индексов по периоду (made_at, Удаленный_at). И я синхронизирую различные таблицы с помощью триггеров. Яда, яда, яда... Я создал триггеры, которые позволяют отменить редактирование сообщений / тегов таким образом, чтобы черновик сохранялся в оборотах без публикации. Работает отлично.
За исключением случаев, когда мне нужно беспокоиться об отношениях, связанных с черновиками, на post2tag. В этом случае весь ад разваливается, и это намекает мне на то, что у меня там какая-то проблема с дизайном. Но у меня заканчиваются идеи...
Я рассмотрел введение дублирования данных (то есть n строк post2tag, представленных для каждой черновой ревизии). Это работает, но, как правило, медленнее, чем хотелось бы.
Я подумал о введении таблиц черновиков для "последнего черновика", но это быстро становится очень и очень уродливым.
Я рассмотрел все виды флагов...
Итак, вопрос: есть ли общепринятые средства управления живыми и неживыми строками в среде с управлением версиями строк? А если нет, то что вы пробовали и были достаточно успешны?
5 ответов
Я думаю, что прибил это. По сути, вы добавляете (уникальное) черновое поле к соответствующим таблицам и работаете с черновиками, как если бы они были новым сообщением / тегом / и т. Д.:
posts (
id pkey,
public,
created_at stamptz,
updated_at stamptz,
updated_by int,
draft int fkey posts (id) unique
)
post_revs (
id,
public,
created_at,
created_by,
deleted_at,
pkey (id, created_at)
)
tags (
id pkey,
public,
created_at,
updated_at,
updated_by,
draft fkey tags (id) unique
)
tag_revs (
id,
public,
created_at,
created_by,
deleted_at,
pkey (id, created_at)
)
post2tag (
post_id fkey posts(id),
tag_id fkey tags(id),
public,
created_at,
updated_at,
updated_by,
pkey (post_id, tag_id)
)
post2tag_revs (
post_id,
tag_id,
public,
created_at,
created_by,
deleted_at,
pkey (post_id, tag_id, created_at)
)
Якорное моделирование - хороший способ реализовать временный дБ - см. Также статью в Википедии. Требуется некоторое время, чтобы привыкнуть, но работать приятно. Существует онлайн-инструмент моделирования, и если вы загружаете предоставленный файл XML [File -> Load Model from Local File]
вы должны увидеть что-то вроде этого - также используйте [Layout --> Togle Names]
,
[Generate --> SQL Code]
создаст DDL для таблиц, представлений и функций на определенный момент времени. Код довольно длинный, поэтому я не размещаю его здесь. Проверьте код - вам может понадобиться изменить его для вашей БД.
Вот файл для загрузки в инструмент моделирования.
<schema>
<knot mnemonic="EXP" descriptor="Expired" identity="smallint" dataRange="char(1)">
<identity generator="true"/>
<layout x="713.96" y="511.22" fixed="true"/>
</knot>
<anchor mnemonic="US" descriptor="User" identity="int">
<identity generator="true"/>
<attribute mnemonic="USN" descriptor="UserName" dataRange="varchar(32)">
<layout x="923.38" y="206.54" fixed="true"/>
</attribute>
<layout x="891.00" y="242.00" fixed="true"/>
</anchor>
<anchor mnemonic="PO" descriptor="Post" identity="int">
<identity generator="true"/>
<attribute mnemonic="TIT" descriptor="Title" dataRange="varchar(2)">
<layout x="828.00" y="562.00" fixed="true"/>
</attribute>
<layout x="855.00" y="471.00" fixed="true"/>
</anchor>
<anchor mnemonic="TG" descriptor="Tag" identity="int">
<identity generator="true"/>
<attribute mnemonic="TGT" descriptor="TagText" dataRange="varchar(32)">
<layout x="551.26" y="331.69" fixed="true"/>
</attribute>
<layout x="637.29" y="263.43" fixed="true"/>
</anchor>
<anchor mnemonic="BO" descriptor="Body" identity="int">
<identity generator="true"/>
<attribute mnemonic="BOT" descriptor="BodyText" dataRange="varchar(max)">
<layout x="1161.00" y="491.00" fixed="true"/>
</attribute>
<layout x="1052.00" y="465.00" fixed="true"/>
</anchor>
<tie timeRange="datetime">
<anchorRole role="IsTagged" type="PO" identifier="true"/>
<anchorRole role="IsAttached" type="TG" identifier="true"/>
<anchorRole role="BYAuthor" type="US" identifier="false"/>
<knotRole role="Until" type="EXP" identifier="false"/>
<layout x="722.00" y="397.00" fixed="true"/>
</tie>
<tie timeRange="datetime">
<anchorRole role="Contains" type="PO" identifier="true"/>
<anchorRole role="ContainedIn" type="BO" identifier="false"/>
<layout x="975.00" y="576.00" fixed="true"/>
</tie>
<tie>
<anchorRole role="CreatedBy" type="TG" identifier="true"/>
<anchorRole role="Author" type="US" identifier="false"/>
<layout x="755.10" y="195.17" fixed="true"/>
</tie>
<tie>
<anchorRole role="CreatedBy" type="PO" identifier="true"/>
<anchorRole role="Author" type="US" identifier="false"/>
<layout x="890.69" y="369.09" fixed="true"/>
</tie>
<tie>
<anchorRole role="ModifiedBy" type="BO" identifier="true"/>
<anchorRole role="Author" type="US" identifier="false"/>
<layout x="1061.81" y="322.34" fixed="true"/>
</tie>
</schema>
Я реализовал временную базу данных, используя SCD типа 2 и правила и триггеры PostgreSQL, и упаковал ее в автономный пакет для ActiveRecord: http://github.com/ifad/chronomodel
Однако дизайн не зависит от языка / структуры - вы можете создавать правила и триггеры вручную, а обо всем остальном позаботится база данных. Загляните на https://github.com/ifad/chronomodel/blob/master/README.sql.
Также в качестве бонуса включена эффективная индексация и запрос временных данных с использованием геометрических операторов.:-)
Проблема post2tag_revs в том, что она пытается выразить 2 принципиально разных понятия.
Тег, примененный к черновому варианту публикации, применим только к этой одной редакции, если только редакция не опубликована.
После публикации тега (т. Е. Связанного с опубликованной ревизией поста) он применяется к каждой будущей ревизии поста, пока он не будет отозван.
И связывание с опубликованной ревизией, или неассоциация, не обязательно совпадает с опубликованной ревизией, если только вы искусственно не применяете это путем клонирования ревизии только для того, чтобы можно было связывать добавления или удаления тегов...
Я бы изменил модель, сделав post2tag_revs.post_rev релевантным только для черновиков тегов. После того, как ревизия будет опубликована (а тэг будет действующим), я буду использовать столбец отметки времени, чтобы отметить начало и конец опубликованной действительности. Вы можете или не хотите, чтобы новая запись post2tag_revs представляла это изменение.
Как вы указываете, это делает эти отношения временными. Вы можете улучшить производительность в "нормальном" случае, добавив логическое значение в post2tag, чтобы указать, что тег в настоящее время связан с сообщением.
Используйте только 3 таблицы: записи, теги и post2tag.
Добавьте столбцы start_time и end_time во все таблицы. Добавьте уникальный индекс для ключа, start_time и end_time. Добавьте уникальный индекс для ключа, где end_time равен нулю. Добавьте триггеры.
Для текущего:
SELECT ... WHERE end_time IS NULL
Вовремя:
WHERE (SELECT CASE WHEN end_time IS NULL
THEN (start_time <= at_time)
ELSE (start_time <= at_time AND end_time > at_time)
END)
Поиск текущих данных не медленный из-за функционального индекса.
Редактировать:
CREATE UNIQUE INDEX ... ON post2tag (post_id, tag_id) WHERE end_time IS NULL;
CREATE UNIQUE INDEX ... ON post2tag (post_id, tag_id, start_time, end_time);
FOREIGN KEY (post_id, start_time, end_time) REFERENCES posts (post_id, start_time, end_time) ON DELETE CASCADE ON UPDATE CASCADE;
FOREIGN KEY (tag_id, start_time, end_time) REFERENCES tags (tag_id, start_time, end_time) ON DELETE CASCADE ON UPDATE CASCADE;