Статьи

Rails Disco: Встань с источником событий

Рельсы-диско-журнал

В сторону: я чувствую, что вы должны сыграть в « Bee Gees » «Вы должны танцевать» прямо сейчас. Как только это произойдет в фоновом режиме, читайте дальше …

Вот несколько вопросов для вас:

  • Как выглядели ваши данные в прошлом году? Прошлая неделя?
  • Сколько разных клиентских приложений используют ваше единственное хранилище данных? Сколько преобразования данных они делают, чтобы использовать данные?
  • Вы когда-нибудь меняли свою модель, приводя к утомительной миграции данных, чтобы добавить новые атрибуты к существующим данным?
  • Вы когда-нибудь хотели изменить технологии для ваших данных (например, хранилище данных), чтобы сказать «Нет», потому что миграция была слишком сложной?

Недавно мне пришлось отвечать на эти вопросы, и мои ответы были ужасными. Итак, я задал себе другой вопрос: что я могу сделать, чтобы в будущем получить лучшие ответы на эти (и другие) вопросы?

После некоторого исследования, ответ, который появился, был «Event Sourcing».

Event Sourcing 101

Что такое Event Sourcing? Краткий ответ — сохранение произошедших событий, чтобы привести наши данные / приложение в его текущее состояние. Вот что говорит Мартин Фаулер:

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

Эта ссылка стоит прочитать. «Большой мозг» в области Event Sourcing — Грег Янг, и у него есть несколько фантастических ресурсов по Event Sourcing, включая:

Быстрый Google «Greg Young Event Sourcing» принесет еще больше ресурсов.

В представленной выше презентации г-на Янга упоминается CQRS, что означает разделение ответственности за командные запросы. Фундаментальная концепция CQRS — это отделить вещи, которые изменяют ваши данные (команды), от вещей, которые запрашивают ваши данные (запросы). Разделение может быть разных классов или совершенно разных приложений. Опять обратимся к мистеру Фаулеру :

[CQRS] В основе лежит простое понятие, что вы можете использовать другую модель для обновления информации, чем модель, которую вы используете для чтения информации. Под отдельными моделями мы обычно подразумеваем разные объектные модели, возможно, работающие в разных логических процессах, возможно, на отдельном оборудовании.

Event Sourcing и CQRS — это горох и морковь, шоколад и арахисовое масло. Я настоятельно рекомендую вам немного ознакомиться с этими двумя понятиями, прежде чем продолжить эту статью. Это сделает рельсовую дискотеку легче перевариваемой.

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

При использовании Event Sourcing, когда пользователь создает команду, результатом является CreatedTeamEvent с данными команды. Когда игрок добавляется в команду, создается CreatedPlayerEvent . Магазин событий выглядит так:

 CreatedTeamEvent {"name": "Cowboys"} CreatedTeamEvent {"name": "Redskins"} CreatedPlayerEvent {"name": "Troy Aikman", "number": "8", "position": "QB", "team": "Cowboys"} DeletedTeamEvent {"team": "Redskins", "reason": "Because they are awful"} 

Вопрос в том, что приложение показывает пользователю, когда запрашиваются все команды? Это может воспроизводить события каждый раз, когда команды запрашиваются. Тем не менее, это не масштабируется. Одним из принципов Event Sourcing является НИКОГДА не удалять данные. Когда-либо. Пусть это впитывается. Вы не можете удалять события, потому что события ДОЛЖНЫ быть воспроизведены, чтобы обеспечить правильное состояние приложения. Таким образом, воспроизведение событий для каждого запроса может работать некоторое время, но с ростом числа событий запрос становится узким местом. Чем ты занимаешься?

В Event Sourcing говорят, ответ: прогнозы. «Проекция» — это скользящий снимок данных. По сути, проекция потребляет события и записывает данные в спецификацию конкретного клиента. Для приложения Rails мы могли бы создать модель Team и создать команду для каждого CreatedTeamEvent в хранилище событий. Наша проекция, в основном, подписывается на события, как канал RSS / Atom. Важно понимать, что проекция отвечает за управление собственной подпиской, опять же, как у клиента RSS / Atom.

Прогнозы могут быть добавлены для ответа на многие распространенные вопросы / сценарии:

  • Хотите перейти на новое хранилище данных для вашего приложения Rails? Создайте проекцию, которая использует события и записывает данные в новое хранилище. Затем начните с события 0 и пройдите все события.
  • Хотите отдельное хранилище данных для отчетов, которые используют более ненормализованную структуру данных? Выполнено.
  • Хотите знать, какие игроки были в команде прошлым летом? Выполнено.
  • Нужно внести радикальные изменения в вашу модель данных? Сделайте это, измените проекцию для обработки новой модели и запустите события, начиная с события 0.

Возможности действительно далеко, вне поля зрения, DY-NO-MITE!

Рельсы Диско

Теперь, когда вы понимаете основы Event Sourcing, как можно надеть Rubyist с бездельником? Ответ — жемчужина диско от фанк-кошек в Hicknhack Software .

Rails Disco — это «распределенная вечеринка с командами, событиями и проекциями». Он состоит из 3 камней:

rdcomp

(Изображение предоставлено Хикнхаком. Они в Германии.)

  • Active Domain — «Каркас, который позволяет писать сервер домена, который обрабатывает команды и создает события».
  • Активное событие — содержит события, команды и проверки для событий, которые попадают в хранилище событий.
  • Активная проекция — проекции для потребления событий и создания моделей базы данных Rails.

Rails Disco предполагает использование определенной инфраструктуры:

  • Распределенный сервер Ruby (DRb) используется для передачи команд в домен.
  • Обмен RabbitMQ используется для публикации событий из домена в проекцию. Таким образом, у вас должен быть установлен и запущен RabbitMQ .
  • Необходимо использовать потоковый веб-сервер (например, Puma или Thin). Изменения источника событий сообщаются клиенту с использованием потоковой передачи. По умолчанию дискотека добавит Puma в ваш Gemfile.

Rails Disco поставляется с исполняемым disco файлом, который позволяет разработчику Rails генерировать «дискотечный каркас» (кстати, я люблю помещать «disco» перед другими словами. Favorite. Gem name. Ever.). По сути, он также добавляет несколько генераторов. вызывается командой disco которую я буду использовать для настройки приложения.

Высокий уровень потока

rdflow
(Изображение предоставлено Hicknhack. Nutzer означает «пользователь». Disco Nutzer.)

Стандартный поток данных через приложение Rails Disco выглядит следующим образом:

  1. Пользователь выдает запрос, который изменяет данные, например, «создать команду».
  2. Контроллер выдает команду Domain для изменения.
  3. Команда Domain передает себя на сервер DRb.
  4. Сервер DRb сопоставляет команду с командным процессором, который сохраняет событие в домене событий.
  5. Командный процессор публикует событие на сервере событий.
  6. Сервер событий отбрасывает сообщение при обмене событиями RabbitMQ.
  7. Сервер приложений Projection получает сообщение от RabbitMQ.
  8. Проекционный сервер создает модель домена (в данном случае, команду)
  9. Пользователь выдает запрос на просмотр новых данных, например, «получить команды»
  10. Стандартная последовательность просмотра модели контроллера Rails происходит, чтобы представить данные клиенту.

Демо-приложение

Демонстрационное приложение сегодня — это сборщик команд, как описано ранее. Приложение позволяет создавать команды и добавлять игроков в эти команды. Я использую Ruby 2.1.2, Rails 4.1.6 и Disco 0.5.3.

Установите gem rails-disco ( gem install rails-disco ) и введите disco new team-builder чтобы получить настройки приложения. Вывод будет очень похож на то, что выкладывает rails new . Основными отличиями являются некоторые дополнительные папки ( domain , app/commands , app/projections ), новый исполняемый файл ( disco ), новый контроллер ( event_source_controller ), а также некоторые другие незначительные дополнения.

Перейдите в каталог приложения team-builder . Прежде чем мы начнем делать приятную дискотеку, давайте внесем некоторые изменения. Во-первых, я собираюсь использовать PostgreSQL, поэтому добавьте гем ‘pg’, bundle и измените конфигурацию базы данных, чтобы использовать ее.

база данных

Rails Disco имеет свой собственный файл конфигурации. Этот файл содержит информацию для хранилища данных о событиях в домене, сервера DRb и соединения и обмена RabbitMQ.

discoconfig

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

Со всей конфигурацией пришло время использовать команду disco для создания нашей платформы. Rails disco предоставляет команду, которая отражает команду rails scaffold :

 disco g scaffold team name:string 

Это приводит к значительному выводу, поэтому я просто выделю элементы, которые добавляет Disco.

 invoke model invoke projection create app/projections/team_projection.rb create test/projections/team_projection_test.rb .... invoke command create app/commands/create_team_command.rb create app/events/created_team_event.rb invoke command_processor create domain/command_processors/domain/team_processor.rb insert domain/command_processors/domain/team_processor.rb insert app/projections/team_projection.rb create app/commands/update_team_command.rb create app/events/updated_team_event.rb invoke command_processor skip domain/command_processors/domain/team_processor.rb insert domain/command_processors/domain/team_processor.rb insert app/projections/team_projection.rb create app/commands/delete_team_command.rb create app/events/deleted_team_event.rb invoke command_processor skip domain/command_processors/domain/team_processor.rb insert domain/command_processors/domain/team_processor.rb insert app/projections/team_projection.rb ... prepend app/views/teams/index.html.erb prepend app/views/teams/show.html.erb 

Генератор диско (человек, я ЛЮБЛЮ его печатать) добавляет проекцию вместе с последовательностью команд-событий-процессоров для каждого действия CRUD. В конце он также касается помощника view, event_source и event_source .

Все стандартные действия контроллера есть, но они не вызывают обычные методы ActiveRecord.

Изменения контроллера

Помните из нашего потока выше, контроллеры запускают команду домена. Команда проверяется, как и модель, и затем отправляется на сервер DRb для обработки.

 class TeamsController < ApplicationController include EventSource def index @teams = Team.all end def show @team = Team.find(id_param) end def new @team = CreateTeamCommand.new end def edit @team = UpdateTeamCommand.new Team.find(id_param).attributes end def create @team = CreateTeamCommand.new team_params if store_event_id Domain.run_command(@team) redirect_to @team, notice: 'Team was successfully created.' else render action: 'new' end end def update @team = UpdateTeamCommand.new team_params.merge(id: id_param) if store_event_id Domain.run_command(@team) redirect_to @team, notice: 'Team was successfully updated.' else render action: 'edit' end end def destroy delete_team = DeleteTeamCommand.new(id: id_param) if store_event_id Domain.run_command(delete_team) redirect_to teams_url, notice: 'Team was successfully destroyed.' else redirect_to team_url(id: id_param), alert: 'Team could not be deleted.' end end private def team_params params.require(:team).permit(:name) end def id_param params.require(:id).to_i end end 

Первое заметное изменение — это включение модуля EventSource , который поступает из каталога app / беспокойства и просто добавляет несколько методов для хранения идентификатора события в сеансе. Мы создаем модели, поэтому мы не можем перенаправить на новую модель при ее создании. Rails Disco делает изящные вещи, используя идентификатор события, чтобы получить окончательный идентификатор модели.

Действия find и show остаются без изменений. Командные действия ( create , update , destroy ) — вот где фанк становится фанк. Глядя на действие create , он создает CreateTeamCommand и передает его в Domain . Что делает команда?

команды

Команды довольно просты. Команда отвечает за проверку и передачу данных в формы.

 class CreateTeamCommand include ActiveModel::Model include ActiveEvent::Command form_name 'Team' attributes :name validates :name, presence: true end 

Как видите, команда simple содержит атрибуты и проверки для объекта. Можно использовать стандартные методы проверки ActiveModel, что приятно.

Если команда действительна, она передается в Domain через DRb и в конечном итоге обрабатывается ActiveDomain::CommandProcessor .

Командные процессоры

Командные процессоры отвечают за создание событий нашего домена из команд. Командные процессоры находятся в домене / command_processors / domain . Там вы найдете team_processor , который выглядит так:

 module Domain class TeamProcessor include ActiveDomain::CommandProcessor process DeleteTeamCommand do |command| if command.valid? event DeletedTeamEvent.new command.to_hash end end process UpdateTeamCommand do |command| if command.valid? event UpdatedTeamEvent.new command.to_hash end end process CreateTeamCommand do |command| if command.valid? id = ActiveDomain::UniqueCommandIdRepository.new_for command.class.name event CreatedTeamEvent.new command.to_hash.merge(id: id) end end end end 

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

Прогнозы

Проекционный сервер, который запускается с приложением Rails, прослушивает события в обмене событиями RabbitMQ. Для каждого события сервер проекций будет проходить через зарегистрированные проекции и вызывать событие. Стоит отметить, что не все прогнозы будут вносить изменения для всех событий. В приложении может быть несколько проекций, каждая из которых обрабатывает подмножество событий.

Файл app / projection / team_projection.rb выглядит следующим образом:

 class TeamProjection include ActiveProjection::ProjectionType def deleted_team_event(event) Team.find(event.id).destroy! end def updated_team_event(event) Team.find(event.id).update! event.values end def created_team_event(event) Team.create! event.to_hash end end 

Если вы помните, проекция обрабатывает событие, превращая его в то, что может прочитать наше приложение. Для приложения Rails это просто наши вызовы ActiveRecord.

Это в основном это. Если вы запустите команду add и перейдете в /teams/new вы можете добавить новую команду и посмотреть логи. Вы увидите сообщения вроде:

 2014-10-20 20:27:15 -0400: [Domain Server][DEBUG]: Published CreatedTeamEvent with {"name":"Cowboys","id":1} 2014-10-20 20:27:15 -0400: [Projection Server][DEBUG]: Received CreatedTeamEvent with {"name":"Cowboys","id":1} 2014-10-20 20:27:15 -0400: [Projection Server][DEBUG]: [TeamProjection]: successfully processed CreatedTeamEvent[1] 

Игроки будут играть

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

Игроки имеют имя, должность, номер и должны быть в команде:

 disco g scaffold player name:string position:string number:integer references:team 

Вы уже знаете, что это будет делать, но нам нужно кое-что настроить, чтобы игрок всегда был в контексте команды.

Во-первых, измените config / rout.rb , переместив resources :players так, чтобы он был под командами:

 resources :teams do resources :players end 

Что касается PlayersController , нам нужно захватить команду перед каждым действием.

 class PlayersController < ApplicationController include EventSource before_action team ...existing code... def create # Change the command to include the team_id @player = CreatePlayerCommand.new player_params.merge(team_param) ...the rest is the same... end ...existing code... def team_param params.require(:team_id).to_i end def team @team = Team.find(params[:team_id]) end end 

Мы добавили before_action и изменили метод create для передачи идентификатора команды в CreatePlayerCommand .

Как насчет этой команды? Он выглядит так же, за исключением того, что он имеет атрибут references из генератора. Я собираюсь изменить это на team_id :

 class CreatePlayerCommand include ActiveModel::Model include ActiveEvent::Command form_name 'Player' attributes :name, :position, :number, :team_id end 

Сделайте то же самое для CreatePlayerEvent :

 class CreatedPlayerEvent include ActiveEvent::EventType attributes :id, :name, :position, :number, :team_id def values attributes_except :id end end 

Последнее связанное с Rails Disco изменение находится в PlayerProjection . Поскольку игроки добавляются в команду, необходимо найти команду, прежде чем мы сможем создать игрока:

 class PlayerProjection include ActiveProjection::ProjectionType def deleted_player_event(event) Player.find(event.id).destroy! end def updated_player_event(event) Player.find(event.id).update! event.values end def created_player_event(event) attrs = event.to_hash team_id = attrs.delete(:team_id) return if team_id.nil? # Or raise team = Team.find(team_id) return if team.nil? # Or raise team.players.create! event.to_hash end end 

Мы добавили модель, которая принадлежит другой модели, поэтому необходимо внести стандартные изменения Rails & ActiveRecord:

приложение / модели / team.rb

 class Team < ActiveRecord::Base self.table_name = 'teams' has_many :players end 

приложение / модели / players.rb

 class Player < ActiveRecord::Base self.table_name = 'players' belongs_to :team end 

Виды игроков (в приложении / views / Players ) должны быть изменены, чтобы отражать команду. По сути, везде, где вы видите players_path измените его на team_players_path(@team) . Вы, вероятно, хотите добавить ссылку «Новый игрок» в teams/show.html.erb . Эти изменения — всего лишь Rails, и я предполагаю, что вы можете решить все остальное. Если нет, посмотрите на репозиторий Github, чтобы увидеть, что я сделал.

Вы можете увидеть все это в действии, если вы идете в команду (вы уже добавили одну, верно?) Страница нового игрока ( /teams/1/player/new team /teams/1/player/new , при условии, что идентификатор команды равен 1), затем заполните и отправьте форму. Вот что я вижу в журналах:

 Started POST "/teams/1/players" for 127.0.0.1 at 2014-10-27 19:44:10 -0400 Processing by PlayersController#create as HTML Parameters: {"utf8"=>"✓", "authenticity_token"=>"kA23feXUmm6P3paIRO8IAeBt/YAmiQrmhjZZKaiwOhU=", "player"=>{"name"=>"Troy Aikman", "position"=>"QB", "number"=>"8", "team_id"=>"1"}, "commit"=>"Create Player", "team_id"=>"2"} Team Load (0.3ms) SELECT "teams".* FROM "teams" WHERE "teams"."id" = $1 LIMIT 1 [["id", 1]] 2014-10-27 19:44:10 -0400: [Domain Server][DEBUG]: Published CreatedPlayerEvent with {"name":"Troy Aikman","position":"QB","number":"8","team_id":1,"id":1} 2014-10-27 19:44:10 -0400: [Projection Server][DEBUG]: Received CreatedPlayerEvent with {"name":"Troy Aikman","position":"QB","number":"8","team_id":1,"id":1} Redirected to http://localhost:3000/teams/1/players Completed 302 Found in 54ms (ActiveRecord: 0.3ms) Started GET "/teams/1/players" for 127.0.0.1 at 2014-10-27 19:44:10 -0400 Processing by PlayersController#index as HTML Parameters: {"team_id"=>"2"} Team Load (0.3ms) SELECT "teams".* FROM "teams" WHERE "teams"."id" = $1 LIMIT 1 [["id", 2]] Player Load (0.2ms) SELECT "players".* FROM "players" Rendered players/index.html.erb within layouts/application (1.5ms) Completed 200 OK in 26ms (Views: 24.9ms | ActiveRecord: 0.5ms) 2014-10-27 19:44:10 -0400: [Projection Server][DEBUG]: [PlayerProjection]: successfully processed CreatedPlayerEvent[4] 2014-10-27 19:44:10 -0400: [Projection Server][DEBUG]: [TeamProjection]: successfully processed CreatedPlayerEvent[4] 

Вы можете увидеть, как событие публикуется, принимается и обрабатывается. Обратите внимание, что и TeamProjection и PlayerProjection получают событие, но только PlayerProjection что-то с ним делает.

События домена

Теперь, когда у нас есть пара команд и игроков, давайте посмотрим на данные нашего домена. Вот записи в таблице domain_events :

Я БЫ Событие Данные
1 CreatedTeamEvent { «Имя»:»Ковбои»,»идентификатор»: 1}
2 CreatedTeamEvent { «Имя»:»краснокожих»,»идентификатор»: 2}
3 CreatedPlayerEvent

Возвращаясь к нашим данным Rails, в таблице projections есть 2 записи:

Я БЫ class_name last_id твердый
1 TeamProjection 3 правда
2 PlayerProjection 3 правда

Как видите, каждая проекция отвечает за управление обработанными событиями.

В Rails Disco есть пара других таблиц, но приведенные выше данные являются наиболее важными для базовой концепции.

Воспроизведение проекции

Одним из основных моментов Event Sourcing является предоставление возможности воспроизводить / воспроизводить прогнозы по мере возникновения новых требований и обстоятельств. Мы можем увидеть это на простом примере в нашей демонстрации.

Team.delete_all консоль Rails и удалите все команды и игроков ( Team.delete_all , Player.delete_all ). В этот момент мы должны сообщить проекции, чтобы она воспроизводила все события, last_id значение last_id в id начального события. Итак, если ваше первое событие в таблице domain_events равно 1, откройте SQL-запрос (я использую pgAdmin3, FWIW) и введите:

 update projections set last_id=0 

Более быстрый способ сделать это — просто rake db:drop и rake db:migrate .

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

 2014-10-28 05:18:29 -0400: [Domain Server][DEBUG]: received resend request with id 1 2014-10-28 05:18:29 -0400: [Domain Server][DEBUG]: Republished CreatedTeamEvent with {"name":"Cowboys","id":1} 2014-10-28 05:18:29 -0400: [Domain Server][DEBUG]: Republished CreatedTeamEvent with {"name":"Redskins","id":2} 2014-10-28 05:18:29 -0400: [Domain Server][DEBUG]: Republished CreatedPlayerEvent with {"name":"Troy Aikman","position":"QB","number":"8","team_id":1,"id":1} 2014-10-28 05:18:29 -0400: [Projection Server][DEBUG]: Received CreatedTeamEvent with {"name":"Cowboys","id":1} 2014-10-28 05:18:29 -0400: [Projection Server][DEBUG]: [PlayerProjection]: successfully processed CreatedTeamEvent[1] 2014-10-28 05:18:29 -0400: [Projection Server][DEBUG]: [TeamProjection]: successfully processed CreatedTeamEvent[1] 2014-10-28 05:18:29 -0400: [Projection Server][DEBUG]: Received CreatedTeamEvent with {"name":"Redskins","id":2} 2014-10-28 05:18:29 -0400: [Projection Server][DEBUG]: [PlayerProjection]: successfully processed CreatedTeamEvent[2] 2014-10-28 05:18:29 -0400: [Projection Server][DEBUG]: [TeamProjection]: successfully processed CreatedTeamEvent[2] 2014-10-28 05:18:29 -0400: [Projection Server][DEBUG]: Received CreatedPlayerEvent with {"name":"Troy Aikman","position":"QB","number":"8","team_id":1,"id":1} 2014-10-28 05:18:29 -0400: [Projection Server][DEBUG]: [PlayerProjection]: successfully processed CreatedPlayerEvent[3] 2014-10-28 05:18:29 -0400: [Projection Server][DEBUG]: [TeamProjection]: successfully processed CreatedPlayerEvent[3] 2014-10-28 05:18:29 -0400: [Projection Server][DEBUG]: All replayed events received 

И, как и в случае с дискотекой, ваши данные вернулись! (Хорошо, может быть, Диско не вернулась, но я могу мечтать, верно?)

Источник всей этой забавной магии диско находится на Github

CompletedPostOnRailsDiscoEvent

Как это часто бывает с подобными сообщениями, этот пример немного надуманный. Я попросил Андреаса Рейшука из Hicknhack Software просмотреть этот пост, и он добавил следующее:

Я хотел бы добавить одну небольшую вещь. Детали CRUD от генераторов покажут только, как работает Rails Disco. Блестящие части вступают в игру, если вы начинаете добавлять события семантической области. Для вашей демонстрации я могу вспомнить [такие события, как] PlayerChangedTeam (с участием игрока, команды и деньги), PlayerBecomesCoach и другие события, специфичные для домена.

Андреас поднимает хорошую мысль. Моя демонстрация была очень CRUDish, но у большинства доменов есть события, где логика домена более сложна. Event Sourcing является находкой в ​​таких случаях.

Это твой бурный тур по Event Sourcing и Rails Disco. Есть еще кое-что, что делает Rails Disco, но я не хотел выходить слишком далеко. Я действительно думаю, что Event Sourcing — это концепция, которую вы должны иметь в своих 17-дюймовых навыках. Rails Disco действительно привносит фанк и ритм Event Sourcing в Ruby, так что давай проверим это. Я должен расколоться, крутые коты!

Распорывается на «Вы должны быть танцы Источники, да »

Я хочу искренне поблагодарить Андреаса из HicknHack Software за рецензирование статьи.