Модель неизменяемых реляционных данных (v1.0.0)

Модель неизменяемых реляционных данных (v1.0.0)

Черновик №1

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

На корневую запись можно сослаться только по её ключу - добавить в ссылающуюся запись атрибут, который в качестве значения хранит ключ ссылаемой записи.

На вложенную запись сослаться нельзя, её можно только включить по значению - добавить во внешнюю запись атрибут, который в качестве значения хранит непосредственно саму запись.

Экземпляры записей организованы в коллекции - наборы пар ключ-значение, где ключами выступают ключевые атрибуты записей, а значениями - сами записи. Соответственно в коллекции организованы только корневые записи.

Если взять классический пример с заказами, их позициями и продуктам, то:

  1. Заказ и продукт будут корневыми записями;

  2. Позиция заказа будет вложенной записью;

  3. Атрибут "позиции" в записи заказа - будет атрибутом включения вложенной записи;

  4. Атрибут "ид продукта" в записи позиции заказа будет ссылкой по ключу.

  5. Экземпляры записей заказ и продукт хранятся в соответствующих коллекциях.

До сих пор, функциональная модель информации мало чем отличается от объектной модели информации. И вообще ни чем не отличается от модели информации DDD со ссылки между агрегатами через ИДы.

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

И тут возникает резонное возражение - заказ, это же сущность, он должен меняться!

Для того чтобы справиться с этим несовершенством реального мира, функциональные программисты-практики идут на компромисс - они допускают появление в системе изменяемых объектов, но минимизируют количество их экземпляров, и делают изменяемыми не сами записи, а их коллекции (репозитории).

И в этом случае сущность обретает для себя вполне конкретный дом - ячейку в коллекции идентифицируемой ключём этой сущности. То есть сущность инкапсулирована внутри коллекции - вы можете запросить её текущее состояние, но изменить это состояние вы можете только через коллекцию.

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

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

Черновик №2

Я довольно часто слышу, что отсутствие связей Many-To-Many в SDJ является проблемой. На самом деле, SDJ поддерживает связи Many-To-Many, но не в том, виде в котором привыкли разработчики.

А исключение привычного вида на самом деле является решением, а не проблемой. Но для того чтобы объяснить проблему, которую решает исключение таких связей - надо зайти из далека.

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

  1. В ER-модели это идентифицирующие и обычные отношения;

  2. В Си и "необъектных" языках это вложенные структуры и указатели на структуры;

  3. В иерархических БД это вложенные документы и ссылки на документы;

  4. В хранилищах ключ-значение это составные значения и ключи в атрибутах составных значений;

  5. В Datomic (БД с "мощной и гибкой моделью информации") это атрибуты-компоненты и атрибуты-ссылки;

  6. В REST API это составные представления ресурсов и URI;

  7. В DDD это связи по ссылкам внутри агрегата и связи

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

И если вы меняете вложенную запись - вы всегда знаете, что это изменение отразится только на сложной записи, в которую эта запись вложена. А если вы меняете самостоятельную запись, с которой ассоциированы другие записи - вы всегда знаете, что это изменение отразится на всех ассоциированных записях.

И все эти модели - концептуального представления (ER и DDD), хранения (иерархические БД, хранилища ключ-значение, Datomic, REST API) и обработки ("предобъектные" и функциональные языки) данных прекрасно согласуются друг с другом и естественным образом выводятся друг из друга.

Наш мир мог бы быть совсем другим и намного более приятным для разработчика. Если бы не пришли они. Реляционная и объектная модель.

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

А связи One to Many наоборот обязаны быть ассоциациями, даже если фактически они являются связями включения.

В результате модель превращается в граф плоских однородных записей, связанных отношениями типа "ассоциация", по которому очень сложно понять, где заканчивается одна составная запись и начинается новая.

Объектная модель, в свою очередь, наоборот технически поддерживает связи типа "ассоциация", но сформировавшиеся вокруг неё традиции и практики не включают в себя использование такого типа связей. А в случае Java ОО ещё и де-факто исключает связь "включает" - даже если запись-верхушка содержит прямую ссылку на "включённую запись", эта же запись может быть так же "включена" в другую сложную запись и изменена неожиданным для вас образом в обход верхушки.

В результате объектная модель превращается…​ Сложно даже сказать во что. Вроде бы это так же граф однородных записей, в котором очень сложно понять где заканчивается одна составная запись и начинается новая. Но в отличие от реляционной модели, синтаксически это всё выглядит как одна огромная составная запись с тысячей "входов" и в которой не требуется явно операции "разименовывания указателя". А технически этот граф является графом плоских однородных записей, связанных отношениями типа "ассоциация".

И вот то, что самая распространённая модель данных на синтаксическом уровне исключила один вид связей, на техническом уровне исключила второй вид связей и получившийся франкинштейн не ложится ни на то, как данные хранятся в системах хранения данных, ни на то как данные передаются между памятью программы и системами хранения данных - это на мой взгляд самая большая трагедия разработки информационных систем.

Именно из этой трагедии, на мой взгляд, вытекают все проблемы современных кодовых баз информационных систем на базе ОРМов:

  1. Огромные монолитные графы модели данных из-за "не модности и низкоуровеновсти" связей типа "ассоциация" в ОО-модели;

  2. Огромные плоские таблицы в реляционных БД и (по подобию) огромные плоские классы в доменной модели из-за отсутствия связей типа "включает" в реляционной модели (да, я знаю про jsonb в реляционной модели и @Embedded в JPA, но я не вижу их повсеместного применения на практике);

  3. Дикие тормоза из-за сотен запросов к БД из-за лёгкости и незаметности "разыменовывания указателя" (ленивой загрузки) в ОО-модели;

  4. Печальный для многих разработчиков опыт потери данных, из-за того что разработчик недоразбирался с тем, как изменения в списке реализующим связь будут отражены в БД.

Вот теперь можно вернуться к SDJ. SDJ возвращает в нормальную практику разработки бакэндов на Java (и Kotlin) разделение связей "включает" и "ассоциируется". И (в случае следования рекомендации из оф. доки к SDJ и реализации сущностей неизменяемыми) SDJ возвращает в нормальную практику разработки бакэндов на Java (и Kotlin) настоящую связь "включает" - если сложная запись включает в себя неизменяемую запись, то никто не сможет её изменить неожиданным для вас образом.

Наконец, какую проблему решает SDJ исключая ОО-версию Many-to-Many - версию, когда синтаксически связь выглядит как включение (private List<Author> bookAuthors - найдите 10 отличий от private List<Chapter> bookChapters), а семантически она работает как ассоциация - удаление автора из списка не влечёт за собой его исчезновение из системы.

ОО-Many-To-Many - это те отношения, которые порождают целый ворох проблем:

  1. Сшивают домен - ядро кодовой базы - в один большой ком грязи, на который намотается весь остальной код системы;

  2. Создают возможность сгенерировать десяток запросов к БД одним не осторожным движением;

  3. Смешивают семантику "включает" и "ассоциируется", что нередко приводит к потере или порче данных.

И 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.