Инструменты Node.js разработчика. Какие ODM нам нужны

    ODM - Object Document Mapper - используется преимущественно для доступа к документоориенриирвоанным базам данных, к которым относятся MongoDB, CouchDB, ArangoDB, OrientDB (последние две базы данных гибридные) и некоторые другие.

    Прежде чем перейти к рассмотрению вопроса, озвученного в названии сообщения, приведу статистику скачивания пакетов из публичного регистра npm.

    Таблица

    Статистика скачивания пакетов для работы с реляционными и документоориентированными базами данных из публичного регистра npm

    Пакет (npm)

    Количество скачиваний в неделю

    База данных

    pg

    1 660 369

    PostgreSQL

    mysql

    713 548

    MySQL

    mysql2

    581 264

    MySQL

    mongodb (из них 1 034 051 вместе с mongoose)

    1 974 992

    MongoDB

    mongoose

    1 034 051

    MongoDB

    nano

    60 793

    CouchDB

    PouchDB

    25 707

    CouchDB

    arangojs

    7 625

    ArangoDB

    orientjs

    598

    OrientDB

    Из таблицы можно сделать очень интересные выводы.

    1. Использование MongoDB в проектах на nodejs почти сравнялось с использованием реляционных баз данных, и превысило использование одной отдельно взятой реляционной базы данных PostgreSQL или MySQL.

    2. В половине проектов для доступа к MongoDB используется библиотека mongoose (ODM).

    3. В половине проектов для доступа к MongoDB не используется библиотека mongoose. А это означает, что не используется сторонняя ODM, так как другой популярной библиотеки ODM для MongoDB нет.

    4. Такие базы данных как ArangoDB, OrtintDB существенно менее популярны, чем MongoDB, и даже CoucbDB, и не имеют общепризнанных ODM.

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

    Модель данных в документоориентирвоанных базах данных близка к привычной многим в реляционных базах данных. Только вместо таблицы - коллекция, вместо строки таблицы - документ. Дает ли применение коллекции документов вместо таблицы преимущества и какие? Рассмотрим возможные варианты аргументации.

    1. Отсутствие жесткой схемы. Сейчас это уже не аргумент. Так как MySQL и PostgrSQL обзавелись типом данных json, который может то же самое и даже больше (например остается возможность запросов с JOIN). Но даже не это главное. Как показывает статистика, половина проектов на MongoDB использует mongoose, в которой задается жесткая схема документа в коллекции.

    2. Встроенные документы. Сейчас тоже не аргумент. MySQL и PostgrSQL имеют тип данных json, которые может то же самое. Надо отдать должное, что внедрение этого типа данных было ускорено развитием NoSQL.

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

    От чего придется отказаться, отказавшись от использования реляционных баз данных: как известно, ACID и JOIN. (Хотя тут есть варианты)

    Итак, с мотивацией определились. Теперь я хотел бы сказать, что именно репликация и шардирование, по моему мнению, в MongoDB как раз не самый лучший из вариантов среди NoSQL. Поэтому я всегда смотрел и на других лидеров.

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

    CouchDB, также как и реляционные базы данных, испытала влияние MongoDB, и в одной из новых версий получила более удобный язык запросов mango, который похож на язык запросов MongoDB.

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

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

    В плане репликации и шардирования ArangoDB напоминает CouchDB. И даже гарантирует ACID в Еnterprise версии продукта.

    JOIN в ArangoDB также не является проблемой. Вот так будет выглядеть классический запрос MANY-TO-MANY:

     FOR a IN author
          FOR ba IN bookauthor
          FILTER a._id == ba.author
            FOR b IN book
            FILTER b._id == ba.book
            SORT a.name, b.title
        RETURN { author: a, book: b }

    Это не выполняемый код на JavaScript с полным перебором всех записей (как может показаться на первый взгляд). Это такой очень оригинальный язык запросов, с синтаксисом JavaScript который является аналогом SQL и называется AQL. Подробности можно узнать в статье на Хабре.

    ArangoDB также позволяет публиковать REST сервисы прямо в базе данных (Foxx) и делать поисковые запросы, включая нечеткий поиск (fuzzy search), что позволяет строить поиск без Elasticsearch (Elasticsearch конечно более мощный инструмент, но проблему составляет синхронизация данных в основной базе данных и в Elasticsearch).

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

    Самое существенное, что есть в решении - это понимание того функционала, который должна решать ODM. В этом смысле я хочу сослаться на проект universal-router (см. сообщение на Хабре), который из всего многообразия функций, к которым мы уже успели привыкнуть в роутерах React.js или Vue.js, выделили главный функционал на котором построили свое решение.

    Если говорить об ODM, таким функционалом, без которого не обойтись, является по моему мнению ровно три функции:

    1. преобразование JSON (полученного из базы данных) в типизированную модель или коллекцию;

    2. преобразование типизированной модели или коллекции в JSON для сохранения в базе данных;

    3. преобразование типизированной модели или коллекции в JSON для отправки клиенту.

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

    При этом есть смысл максимально использовать возможности Typescript (например декораторы, рефлексию, метаданные).

    export class Reposytory {
    
      private db: Database;
    
      constructor() {
        this.db = db;
      }
    
      @collection(Author)
      async authorFindAll() {
        const row = await this.db.query({
          query: 'for row in authors return row',
          bindVars: {},
        });
        return row.all();
      }
    
      @model(Author)
      async authorCreate(author: Author): Promise<Author> {
        const doc = await this.db.query({
          query: 'insert @author into authors let doc = NEW return doc',
          bindVars: { 'author': author }
        });
        return doc.next();
      }
    }
    

    Код декораторов будет довольно лаконичен:

    export function collection(Constructor: new(...args: any[]) => void) {
      return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor): void => {
          const originalValue = descriptor.value;
          descriptor.value = async function(...args: any[]) {
            const plainData = await originalValue.apply(this, args)
            const data = new Array();
            plainData.forEach((item: any) => data.push(new Constructor(item)));
            return data;
          }
        }
    }
    
    export function model(Constructor: new(...args: any[]) => void) {
      return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor): void => {
          const originalValue = descriptor.value;
          descriptor.value = async function(...args: any[]) {
            const plainData = await originalValue.apply(this, args)
            return new Constructor(plainData);
          }
        }
    }

    Что касается модели документа, я построил схему модели на таких декораторах:

    attr(Type?) - атрибут - то есть то что сохраняется в базе данных и не является вычислимым параметром;

    optional(Type?) - не обязательное значение;

    array(Type?) - типизированная коллекция;

    translatable(Type?) - значение, сохраняющееся в базе данных виде объекта и возвращающееся клиенту в виде строки с выбранной локалью;

    group(...args: string[]) - список групп клиентов, которым доступно значение.

    На таких декораторах можно построить описание объекта:

    import {Model, ModelType} from '../src';
    import {optional, attr, array, getter, group, _type, translatable} from '../src';
    import {Translatable, TranslatableType} from '../src';
    
    interface AddressType extends ModelType {
        city: string,
        street: Translatable,
        house: string,
        appartment?: number,
    }
    
    interface AuthorType extends ModelType{
        name: Translatable,
        address: AddressType,
    }
    
    export class Address extends Model<AddressType> implements AddressType {
    
        @attr()
        @group('admin', 'user')
        @translatable(Translatable)
        public city!: string;
    
        @attr()
        @group('admin')
        @translatable(Translatable)
        public street!: Translatable;
    
        @attr()
        @group('admin')
        public house!: string;
    
        @attr()
        @optional()
        @group('admin')
        public appartment?: number;
    
    }
    
    export class Author extends Model<AuthorType> implements AuthorType  {
    
        @attr(Translatable)
        @translatable(Translatable)
        @group('admin', 'user')
        public name!: string;
    
        @attr(Address)
        @group('admin', 'user')
        public address!: Address
    
    }
    

    Результаты исследования представлены в репозитарии.

    Желающих обсудить приглашаю в чат https://t.me/arangodb_odm

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 1

      +2

      Статистика для MySQL не совсем верна — не учитывается пакет mysql2, поэтому общее число скачиваний около 1.2 миллионов

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое