Черновик №1
Функциональные программисты используют, вы не поверите, самую обычную модель информации, в которой информация организована в записи - наборы пар атрибут-значение, соответствующие определённым типам. Записи бывают двух видов - корневые и вложенные. Корневые имеют ключевой атрибут, уникально идентифицирующие отдельную запись в наборе всех записей определённого типа. Вложенные записи ключевых атрибутов не имеют и могут храниться только внутри одной корневой записи.
На корневую запись можно сослаться только по её ключу - добавить в ссылающуюся запись атрибут, который в качестве значения хранит ключ ссылаемой записи.
На вложенную запись сослаться нельзя, её можно только включить по значению - добавить во внешнюю запись атрибут, который в качестве значения хранит непосредственно саму запись.
Экземпляры записей организованы в коллекции - наборы пар ключ-значение, где ключами выступают ключевые атрибуты записей, а значениями - сами записи. Соответственно в коллекции организованы только корневые записи.
Если взять классический пример с заказами, их позициями и продуктам, то:
Заказ и продукт будут корневыми записями;
Позиция заказа будет вложенной записью;
Атрибут "позиции" в записи заказа - будет атрибутом включения вложенной записи;
Атрибут "ид продукта" в записи позиции заказа будет ссылкой по ключу.
Экземпляры записей заказ и продукт хранятся в соответствующих коллекциях.
До сих пор, функциональная модель информации мало чем отличается от объектной модели информации. И вообще ни чем не отличается от модели информации DDD со ссылки между агрегатами через ИДы.
Отличия начинаются с того, что в функциональной модели информации записи являются неизменяемыми - значения атрибутов записи невозможно изменить, после создания экземпляра.
И тут возникает резонное возражение - заказ, это же сущность, он должен меняться!
Для того чтобы справиться с этим несовершенством реального мира, функциональные программисты-практики идут на компромисс - они допускают появление в системе изменяемых объектов, но минимизируют количество их экземпляров, и делают изменяемыми не сами записи, а их коллекции (репозитории).
И в этом случае сущность обретает для себя вполне конкретный дом - ячейку в коллекции идентифицируемой ключём этой сущности. То есть сущность инкапсулирована внутри коллекции - вы можете запросить её текущее состояние, но изменить это состояние вы можете только через коллекцию.
Обратите внимание на то, насколько близка эта модель к тому, как действительно работают современные БД - данные хранятся на диске организованными записи, которые ссылаются друг на друга по ключу. И эти записи инкапсулированы внутри базы данных - вы можете запросить их текущее состояние в свой процесс, но это будет лишь копия не связанная с оригиналом и для того чтобы изменить оригинал, вам надо попросить об этом БД.
Итого, функциональная модель информации представляет информацию в виде изменяемых коллекций неизменяемых, потенциально составных записей, идентифицируемых и ссылающихся друг на друга по ключу. А обновление состояния сущности в систему осуществляется посредством сохранения записи с новым состоянием сущности под ключём этой сущности в соответствующей коллекции.
Черновик №2
Я довольно часто слышу, что отсутствие связей Many-To-Many в SDJ является проблемой. На самом деле, SDJ поддерживает связи Many-To-Many, но не в том, виде в котором привыкли разработчики.
А исключение привычного вида на самом деле является решением, а не проблемой. Но для того чтобы объяснить проблему, которую решает исключение таких связей - надо зайти из далека.
Во всех распространённых системах моделирования данных, модель состоит из связанных записей, идентифицируемых ключём. И практически во всех из них есть два вида связей между записями - включение, когда одна из записей является неотъемлемой частью другой, и ассоциации - когда обе записи остаются независимыми и ссылки реализуются включением поля с адресом (ключём) другой записи в одну или обе записи:
В ER-модели это идентифицирующие и обычные отношения;
В Си и "необъектных" языках это вложенные структуры и указатели на структуры;
В иерархических БД это вложенные документы и ссылки на документы;
В хранилищах ключ-значение это составные значения и ключи в атрибутах составных значений;
В Datomic (БД с "мощной и гибкой моделью информации") это атрибуты-компоненты и атрибуты-ссылки;
В REST API это составные представления ресурсов и URI;
В DDD это связи по ссылкам внутри агрегата и связи
И во всех этих случаях всегда очевидно с чем вы имеете дело - со сложной записью, или с двумя связанными, но независимыми записями. В первом случае, имея на руках, верхушку сложной записи вы можете напрямую обратится к информации во вложенной записи. Во втором, прежде чем обратиться к данным связанной записи, необходимо выполнить церемонии различного уровня сложности для "разыменовывания указателя".
И если вы меняете вложенную запись - вы всегда знаете, что это изменение отразится только на сложной записи, в которую эта запись вложена. А если вы меняете самостоятельную запись, с которой ассоциированы другие записи - вы всегда знаете, что это изменение отразится на всех ассоциированных записях.
И все эти модели - концептуального представления (ER и DDD), хранения (иерархические БД, хранилища ключ-значение, Datomic, REST API) и обработки ("предобъектные" и функциональные языки) данных прекрасно согласуются друг с другом и естественным образом выводятся друг из друга.
Наш мир мог бы быть совсем другим и намного более приятным для разработчика. Если бы не пришли они. Реляционная и объектная модель.
Реляционная модель технически хоть и поддерживает связи типа включение, но практически буквально запрещает их использование требованиями первой нормальной формы. И так как в реляционной модели операция "разыменования указателя" является достаточно тяжёлой и с синтаксической, и с вычислительной точки зрения, в практике применения этой модели это влечёт превращение записей в огромные неструктурированные пачки полей с низкой функциональной связанностью (cohesion).
А связи One to Many наоборот обязаны быть ассоциациями, даже если фактически они являются связями включения.
В результате модель превращается в граф плоских однородных записей, связанных отношениями типа "ассоциация", по которому очень сложно понять, где заканчивается одна составная запись и начинается новая.
Объектная модель, в свою очередь, наоборот технически поддерживает связи типа "ассоциация", но сформировавшиеся вокруг неё традиции и практики не включают в себя использование такого типа связей. А в случае Java ОО ещё и де-факто исключает связь "включает" - даже если запись-верхушка содержит прямую ссылку на "включённую запись", эта же запись может быть так же "включена" в другую сложную запись и изменена неожиданным для вас образом в обход верхушки.
В результате объектная модель превращается… Сложно даже сказать во что. Вроде бы это так же граф однородных записей, в котором очень сложно понять где заканчивается одна составная запись и начинается новая. Но в отличие от реляционной модели, синтаксически это всё выглядит как одна огромная составная запись с тысячей "входов" и в которой не требуется явно операции "разименовывания указателя". А технически этот граф является графом плоских однородных записей, связанных отношениями типа "ассоциация".
И вот то, что самая распространённая модель данных на синтаксическом уровне исключила один вид связей, на техническом уровне исключила второй вид связей и получившийся франкинштейн не ложится ни на то, как данные хранятся в системах хранения данных, ни на то как данные передаются между памятью программы и системами хранения данных - это на мой взгляд самая большая трагедия разработки информационных систем.
Именно из этой трагедии, на мой взгляд, вытекают все проблемы современных кодовых баз информационных систем на базе ОРМов:
Огромные монолитные графы модели данных из-за "не модности и низкоуровеновсти" связей типа "ассоциация" в ОО-модели;
Огромные плоские таблицы в реляционных БД и (по подобию) огромные плоские классы в доменной модели из-за отсутствия связей типа "включает" в реляционной модели (да, я знаю про jsonb в реляционной модели и @Embedded в JPA, но я не вижу их повсеместного применения на практике);
Дикие тормоза из-за сотен запросов к БД из-за лёгкости и незаметности "разыменовывания указателя" (ленивой загрузки) в ОО-модели;
Печальный для многих разработчиков опыт потери данных, из-за того что разработчик недоразбирался с тем, как изменения в списке реализующим связь будут отражены в БД.
Вот теперь можно вернуться к SDJ. SDJ возвращает в нормальную практику разработки бакэндов на Java (и Kotlin) разделение связей "включает" и "ассоциируется". И (в случае следования рекомендации из оф. доки к SDJ и реализации сущностей неизменяемыми) SDJ возвращает в нормальную практику разработки бакэндов на Java (и Kotlin) настоящую связь "включает" - если сложная запись включает в себя неизменяемую запись, то никто не сможет её изменить неожиданным для вас образом.
Наконец, какую проблему решает SDJ исключая ОО-версию Many-to-Many - версию, когда синтаксически связь выглядит как включение (private List<Author> bookAuthors
- найдите 10 отличий от private List<Chapter> bookChapters
), а семантически она работает как ассоциация - удаление автора из списка не влечёт за собой его исчезновение из системы.
ОО-Many-To-Many - это те отношения, которые порождают целый ворох проблем:
Сшивают домен - ядро кодовой базы - в один большой ком грязи, на который намотается весь остальной код системы;
Создают возможность сгенерировать десяток запросов к БД одним не осторожным движением;
Смешивают семантику "включает" и "ассоциируется", что нередко приводит к потере или порче данных.
И SDJ решает все эти проблемы. Исключением ОО-Many-To-Many.
Разве это не чудесно?
Черновик №3
Модель реляционных неизменяемых данных является моделью предстваления информации в виде набора неизменяемых, потенциально вложенных структур данных - неотъемлемой части функционального стиля программирования. Такая модель представляет всю БД как набор изменяемых коллекций с доступом по ключу (MutableMap<Key, Aggregate> в Kotlin), элементами которых являются неизменяемые структуры данных (data class в Kotlin).
Концептуально, модель реляционных неизменяемых данных можно проиллюстрировать следующим кодом:
data class User(val id: Long, val name: String)
data class Post(val id: Long, val userId: Long, val title: String)
class Db {
private val collections = hashMapOf<KClass<*>, MutableMap<*, *>>()
fun createTable(type: KClass<*>) {
collections.put(type, HashMap<*, *>())
}
fun upsert(aggregate: Any) {
collections[aggregate::class][aggregate.getKey()] = aggregate
}
fun <T> findById(type: KClass<T>, id: Any): T? {
return collections[type][id] as T
}
fun <T> findAll(type: KClass<T>): Iterable<T> {
return collections[type].values
}
}
val db = Db()
fun initDb() {
db.createTable(User::class)
db.createTable(Post::class)
val user = User(1, "Aleksey")
db.save(user)
val post = Post(1, user.id, "Immutable relational data model")
db.save(post)
}
fun addPost(userId: Long, postDto: PostDto) {
val newPost = Post(nextPostId(), userId, postDto.title)
db.upsert(newPost)
}
fun getPost(postId: Long): PostView {
val post = db.findById(Post::class, postId)
val user = db.findById(User::class, post.userId)
return PostView(author = user.name, title = post.title)
}
fun updatePost(postId: Long, newTitle: String) {
val post = db.findById(Post::class, postId)
val updatedPost = post.copy(title = newTitle)
db.upsert(updatedPost)
}
На практике, конечно же, данные будут храниться в каком-то хранилище постоянных данных (в этом посте - в реляционной СУБД), а в приложении база данных будет представлена в виде набора классов, реализующих шаблон Репозиторий.
Концептуальная модель
Классической концептуальной моделью информации является Entity-Relationship модель и её можно преобразовать в модель неизменяемых реляционных данных. Для упрощения перехода к физической модели, в качестве нотации ER-диаграммы лучше использовать нотацию вороньей лапки.
Обратите внимание, что в нотации вороньей лапки можно представить только бинарные (между двумя сущностями) отношения без атрибутов. А отношения, связывающие три и более сущности, а так же отношения, содержащие атрибуты, должны быть преобразованы в сущности. |
(todo: расписать элементы ЕР-диаграммы)
(todo: подраздел концептуальной модели?)
Логическая модель
Для того чтобы концептуальная модель могла быть эффективно представлена в терминах современных языков программирования и систем хранения данных, её надо немного преобразовать.
Во-первых, надо заменить отношения Многие ко Многим (N-M) на слабые сущности, связанные отношениями Многие к Одному (N-1) с изначальными сущностями. Здесь же, при использовании нотации, отличной от вороньей лапки, необходимо преобразовать в сущности отношения, связывающие более двух сущностей и/или имеющие атрибуты.
Во-вторых, надо ввести отношение 1 к Немногим (1-F), проанализировать имеющиеся отношения 1-N и пометить их как 1-F, в том случае, если они являются таковыми.
Отношение 1-F - это отношение, в которое естественным для предметной области образом вступает такое количество сущностей такого размера, которое можно эффективно загрузить из постоянного хранилища и передать на устройство конечного пользователя и с которым конечному пользователю будет комфортно работать как с единым целым (без средств поиска и пагинации). Конкретное значение количества сущностей и их размер, которые можно счесть отношением 1-F, зависит от конкретной предметной области и технологии реализации В качестве первого приближения можно взять 20 сущностей в отношении размером до 256 байт каждая.
В-третих, надо убедить, что слабые сущности вступают только в отношения 1-1 и 1-F, но не 1-N.