Элегантная схема для регистрации действий пользователей - PullRequest
2 голосов
/ 23 июня 2011

У меня есть схема базы данных для регистрации операций, которые пользователи выполняют в моем веб-приложении:

Log
---
Id
Log_Type_Id
Performed_by_Person_Id
Performed_to_Person_Id
Comment_Id
Story_Id
Photo_Id
etc_Id

Person_Log
----------
Person_Id
Log_Id

Таким образом, я могу уведомлять пользователей о записях в их журнале с подробной информацией о том, что именно произошло.Проблема заключается в том, что таблица «Журнал» должна содержать все возможные типы пользовательских операций (они изменяли историю или комментарий, создавали историю или комментарий или фотографию, обновляли профиль и т. Д.).И почти все эти поля обязательно будут нулевыми для каждой записи.

В идеале у меня есть отдельные таблицы журнала, на которые ссылается общий журнал, может быть что-то вроде:

Log
---
Id
Performed_by_Person_Id

Log_Comment
-----------
Id
Log_Id
Comment_Id

Log_Photo
---------
Id
Log_Id
Photo_Id

Person_Log
----------
Person_Id
Log_Id

Проблема в томчто у меня тогда нет простого способа уведомить пользователей о вещах, происходящих с ними.Я легко получаю запись в журнале для них, но затем мне нужно запросить каждую «дочернюю» таблицу, чтобы увидеть особенности ... Я могу сохранить имя дочерней таблицы журнала в журнале, но это выглядит так не элегантно.Есть ли лучший, более реляционный способ сделать это, который также хорошо работает с системами ORM?

Ответы [ 4 ]

2 голосов
/ 23 июня 2011

Ваш случай выглядит как образец шаблона проектирования Gen-Spec. Gen-spec знакома объектно-ориентированным программистам по иерархии суперкласс-подкласс. К сожалению, введение в проектирование реляционных баз данных, как правило, пропускает, как разрабатывать таблицы для ситуации Gen-Spec. К счастью, это хорошо понято. Поиск в Google по «Специализации обобщения реляционных баз данных» даст несколько статей на эту тему. Или вы можете посмотреть следующее предыдущее обсуждение .

Хитрость в том, как назначается PK для таблиц подкласса (специализированных). Он не генерируется какой-либо функцией автонумерации. Вместо этого это копия PK в суперклассовой (обобщенной) таблице, и, следовательно, ссылка на нее FK.

Таким образом, если бы речь шла о транспортных средствах, грузовиках и седанах, у каждого грузовика или седана была бы запись в таблице транспортных средств, у грузовиков также была бы запись в таблице грузовых автомобилей с PK, который является копией соответствующего PK в Стол транспортных средств. Аналогично для седанов. Легко определить, является ли транспортное средство грузовым автомобилем или седаном, просто выполнив объединения, и в любом случае обычно требуется объединить данные в запросе такого типа.

2 голосов
/ 23 июня 2011

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

Модель, которую я использую чаще всего, - Тематические карты (хотя эта информация может быть не самой простой в использовании, чтобы понять, о чем я говорю), где вместо таблицы для каждой сущности, есть один, который содержит все, и несколько дополнительных, чтобы иметь дело с типизацией и отношениями между ними. Вам не нужно идти с этим до конца, но, возможно, используйте его специально для вашего случая использования. Вот статья, которую я написал об этом почти 10 лет назад, и еще одна статья Марка де Грауу , в которой также рассматривается конкретный взгляд на СУБД.

Вернуться к вашему вопросу. Для примера использования тематических карт сначала нужны таблицы;

Topic
-----
id
name
type
meta_date_created
meta_date_created_topic_ref
meta_date_updated
meta_date_updated_topic_ref
meta_date_deleted
meta_date_deleted_topic_ref

Assoc (relationship)
--------------------
id
type

Assoc member
------------
id
topic_ref
role_topic_ref

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

Каждый человек будет иметь запись в «Теме», пример;

id: 4572349857
name: Alexander Johannesen
type: 12341234
meta_date_create: {date}
meta_date_create_topic_ref: 5656

Чтобы узнать, кто создал этого пользователя, поищите в «Теме» идентификатор «5656»;

id: 5656
name: Billy Bob
type: 12341234

Но что это за тип? Ищите в «Теме» идентификатор «12341234»;

id: 12341234
name: Person

Концептуальная основа здесь заключается в том, что каждая «вещь» (намеренно неопределенная; это может быть все, о чем вы хотите поговорить) в вашей системе получает запись, включая действия;

id: 34598067
name: Add new user
type: 56987  // another topic called 'Action', for example)

При всем этом ваш журнал в основном создает отношения между этими сущностями через таблицу «Assoc»;

id: 45673
type: 45685678

Это сама ассоциация. Идентификатор - это что угодно, не важно, но тип - это (как вы уже догадались) другая сущность в таблице «Тема»;

id: 45685678
name: Did action

Теперь вы заполняете таблицу 'Assoc member' с подробной информацией о регистрации действия;

id: {whatever}
topic_ref: 5656
role_topic_ref: 12341234

Первый участник - Билли Боб, который играет роль «Персона». Далее;

id: {whatever}
topic_ref: 34598067
role_topic_ref: 56987

Здесь тема «Добавить нового пользователя» играет роль «Действие». Вы можете расширить эту связь, добавив столько элементов, сколько вам нужно, например, добавление в предварительном состоянии, результат действия, количество попыток до сих пор, где выполнялось действие (например, если это функция, которую могут выполнять люди. количество страниц), и так далее. Создайте сущности для этих вещей в таблице Темы, создайте сущности для их отношений, и вы можете сделать это настолько сложным, насколько захотите.

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

Возможно, можно было бы обосновать производительность меньшего количества таблиц, подобных этой, но по моему опыту большинство СУБД прекрасно работают с внутренними объединениями, которые являются основным инструментом, необходимым для выполнения этой работы (все поля, которые являются идентификаторами, являются очевидными кандидатами в индексы). ), и хорошо то, что это также в основном совместимо с средствами мышления NoSQL, создавая достаточную абстракцию между вами и вашими данными, а также SQL и технической механикой, которую хочет использовать серверная часть.

1 голос
/ 23 июня 2011

Я рекомендую второй дизайн, который вы описываете. Если вы хотите получить все столбцы каждой таблицы подтипов журнала, вы можете использовать LEFT OUTER JOIN:

SELECT *
FROM Person_Log AS p
INNER JOIN Log AS l ON p.Log_ID = l.Log_ID
LEFT OUTER JOIN Log_Comment AS lc ON l.Log_Type = 'C' AND l.Log_ID = lc.Log_ID
WHERE p.Person_ID = 1234;

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

Вы также можете использовать ограничения, поэтому вы уверены, что только одна строка подтипа во всех таблицах ссылается на данную строку в журнале:

Log
---
Log_Id
Log_Type constrained to ('C', 'P', etc.)
Performed_by_Person_Id
UNIQUE KEY (Log_Id,Log_Type)

Log_Comment
-----------
Log_Id PRIMARY KEY
Log_Type constrained to only 'C'
Comment_Id
FOREIGN KEY (Log_Id,Log_Type) REFERENCES Log(Log_Id,Log_Type)

Log_Photo
---------
Log_Id PRIMARY KEY
Log_Type constrained to only 'P'
Photo_Id
FOREIGN KEY (Log_Id,Log_Type) REFERENCES Log(Log_Id,Log_Type)

Ваш комментарий:

Это в основном то же самое, что и gen-spec дизайн, который упоминает @Walter Mitty.

Это также относится к шаблону Мартина Фаулера, Наследование таблиц классов .

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

0 голосов
/ 23 июня 2011

У меня была бы только одна таблица журнала с затронутыми людьми, столбцом actionID и item_id.

Затем в своем интерфейсе вы можете отобразить уведомление на основе actionID. Например, actionID 1 может быть фотографией.так у вас item_id будет photoID

...