Статьи

Включение транзакций в Node.js с использованием доменов

[Эта статья изначально написана Ноамом Бенами.]

В Mulesoft у нас глубоко открытое программное обеспечение с открытым исходным кодом: исходный код нашего  основного продукта находится на github . У нас также есть сотни  открытых проектов  , и мы внесли свой вклад во многие проекты с открытым исходным кодом, включая   сам Node.js. Мы в восторге от Node.js, и у нас в разработке находится несколько крупных и сложных Node.js- проектов. Использование передовых функций Node.js привело к получению большого количества знаний и, что неудивительно, к большому испытанию боли.

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

Этот конкретный проект опирается на postgres для сохранения данных и предоставляет множество клиентов, включая пользовательский интерфейс AngularJS, с богатым набором API . Мы используем  RAML  для определения этих API , со схемами JSON для сущностей. Эти сущности имеют иерархические формы, в отличие от табличных данных, хранящихся в Postgres , и поэтому слой преобразования отделяет наши сервисы от наших хранилищ базы данных: как сторонники  многоуровневой архитектуры  и  SOLID , мы разработали систему, как показано в несколько надуманных примерах ниже. Обратите внимание, что все слои в системе работают асинхронно и возвращают  обещания .

Уровень контроллера

Уровень  контроллера  извлекает данные из HTTP-запроса и передает их на уровень обслуживания. Например:

  query: function (req) {
    var q = req.query;
    return hadronService.query(q.offset, q.limit, q.sort, q.ascending);
  }

Сервисный уровень

Служебный уровень возвращает сущности, имеющие форму, указанную схемой JSON в RAML API. Сервисы используют простую  подсистему отображения,  которая знает, как преобразовать строки базы данных в сущности:

  get: function () {
    return hadronRepository.get()
      .then(mapper.toEntity);
  }

Более сложные сервисы извлекают данные из нескольких источников, выполняют фильтрацию, проверку и другие задачи.

Слой репозитория

Уровень хранилища предоставляет API-интерфейсы, которые обращаются к базе данных, строят запросы, выполняют операции CRUD и предоставляют службы целостности данных, такие как транзакции :

 get: function (particleId) {
return DAL.Particles()
.where({id: particleId})
.then(function (rows) {
return rows[0] || null;
});
}

Объект  DAL  в приведенном выше коде является самым глубоким уровнем системы данных и предоставляет услуги создания запросов и мультитенантности для репозиториев.

Вот где это стало интересно: мы используем  knex  для доступа к данным, а knex обеспечивает разумную реализацию транзакций. Сервисы должны запускаться транзакцией: сервис может захотеть вставить две сущности как единое целое — родительский объект и дочерний объект, например. Для этого он вызовет два разных репозитория, и ему нужно, чтобы оба репозитория подключились к одной и той же транзакции. Тем не менее, только репозитории могут вызывать код knex, чтобы поддерживать надлежащий уровень абстракции.

Домены

Введите  домены

Домены — чрезвычайно мощная, но малоизвестная и плохо понятая особенность Node.js. Они предоставляют контекст, в котором может выполняться код и из которого могут быть получены ошибки. Мы решили использовать домен, связанный с вызовом API уровня http, для хранения наших транзакций. Это привело к тому, что транзакционный код уровня обслуживания выглядит следующим образом:

transactor.run(function () {
    return self.insert(atom)
      .then(function (atom) {
        var electron = particleService.insertElectron(atom);
        return Promise.all([atom, electron]);
      }).spread(function (atom, electron) {
        atom.electrons = [electron];
        return atom;
      });
  });

Транзактный метод выглядит так:

transact: function (action) {
      //If there is an ongoing transaction, use it. This is required to
      //support nested transactions.
      if (domainContext.transaction) {
        return action();
      }

      return db.transaction(function (tx) {
        domainContext.transaction = tx;
        //Execute the action
        return action()
          .then(function (result) {
            //Commit on success
            tx.commit(result);
          }, function (err) {
            //Rollback on failure
            tx.rollback(err);
          });
      }).finally(function () {
          //Clear out the transaction from the domain context!
          domainContext.transaction = null;
        });
    }

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

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

Нет похожих сообщений.