如何在 Rails 中从零开始构建事件溯源模式
总结一下:什么是事件溯源?
本文涵盖的内容
启动并运行我们的 Rails 应用程序
搭建测试环境以测试我们的事件
什么是事件?
这Events::BaseEvent
桌子user_events,以及Events::User::BaseEvent
创建新用户Events::User::Created
使用以下方式销毁用户Events::User::Destroyed
结论
接下来
参考
此演示的所有代码都可以在此 GitHub 仓库中找到:
https://github.com/isalevine/event-sourcing-user-app
总结一下:什么是事件溯源?
事件溯源是一种系统设计模式,它强调通过不可变事件记录数据的变化。
换句话说:每次数据发生变化时,您都需要将详细信息保存到数据库中。
这些事件永远不会改变或消失。这样,您就拥有了数据如何达到当前状态的永久、不变的历史记录!
本文涵盖的内容
我们将主要借鉴Kickstarter 的活动筹资案例。
要创建我们的事件模式,我们将采取以下步骤:
- 启动并运行我们的 Rails 应用
User模型和控制器- 我们的数据库是 PostgreSQL。
- 设置测试环境以测试我们的事件
- Postico 将检查我们的数据库
- REST 客户端失眠症
- 添加我们的事件模式
- 什么是事件?我们将把哪些事件数据存储在数据库中?
- 其他 Event 类将继承的 BaseEvent
Events::User::CreatedEvents::User::Destroyed
启动并运行我们的 Rails 应用程序
接下来,我们来创建新的 Rails 应用。
我们将使用 PostgreSQL 设置数据库,--database=postgresql并使用 跳过测试--skip-test,因为我们稍后将手动添加 RSpec。
rails new event-sourcing-user-app --database=postgresql --skip-test
让我们添加我们的User模型
我们的User模型将包含多个字段:
name细绳,email细绳,password_digest字符串(用于 bcrypt)deleted布尔值(请记住,事件溯源的一部分原则是我们永远不会删除数据——相反,我们会将某些用户标记为已删除,并相应地限定查询范围)- 此字段也需要是
null: false,并且设置为default: false
- 此字段也需要是
我们将从 Rails 单行命令开始:
rails g model User name email password_digest deleted:boolean
在新迁移中,调整t.boolean :deleted为null: false:default: false
# db/migrate/20200502025357_create_users.rb
class CreateUsers < ActiveRecord::Migration[6.0]
def change
create_table :users do |t|
t.string :name
t.string :email
t.string :password_digest
t.boolean :deleted, null: false, default: false
t.timestamps
end
end
end
添加User控制器和路由
我们的用户控制器需要有两个操作,一个是事件处理操作,另一个create是destroy事件创建操作,用来处理我们想要创建的事件。
让我们手动创建控制器,因为我们不需要生成任何视图。在 `<controller>` 标签中app/controllers,创建一个 `<controller>` 标签users_controller并添加`<action> def create` 和def destroy`<action>` 操作:
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
end
def destroy
end
end
由于我们尚未实现身份验证,因此我们还会添加一个skip_before_action钩子,以便更轻松地测试我们的代码:
# app/controllers/users_controller.rb
class UsersController < ApplicationController
skip_before_action :verify_authenticity_token, only: [:create, :destroy]
def create
end
def destroy
end
end
接下来,让我们手动添加 POST 和 DELETE 路由,它们将分别跳转到控制器中的`get`create和 `get`操作:destroy
# config/routes.rb
Rails.application.routes.draw do
post 'users/create', to: 'users#create'
delete 'users/destroy', to: 'users#destroy'
end
在控制台中运行rails routes以下命令,查看路由是否已正确设置:
[13:29:44] (master) event-sourcing-user-app
// ♥ rails routes
Prefix Verb URI Pattern Controller#Action
users_create POST /users/create(.:format) users#create
users_destroy DELETE /users/destroy(.:format) users#destroy
运行数据库迁移
现在,让我们按照通常的两步流程创建数据库并运行迁移:
rails db:create
rails db:migrate
搭建测试环境以测试我们的事件
设置 Postico 以查看我们的 PostgreSQL 数据库
如果您还不熟悉Postico,它是一款用于 PostgreSQL 的数据库管理工具和查看器,并提供很棒的免费试用版。
从他们的网站下载并安装,然后打开。接下来,使用其默认设置Connect进行操作:localhost
点击localhost顶部的按钮,即可查看可用数据库列表:
现在,我们应该可以选择我们的开发数据库了:
选择我们的users餐桌:
太好了——我们的用户模型就在这里,它有四个字段:
配置 Insomnia 发送 HTTP 请求
同样,如果您不熟悉Insomnia,它是一个用于发送 HTTP 请求以测试 RESTful API 的工具。我们将使用Insomnia Core。
下载、安装并打开它:
为我们的项目创建一个文件夹event-sourcing-user-app:
让我们创建第一个请求。我们将使用 POST 请求,并将其用于创建用户路由:
最后,我们将目标 URL 设置为localhost:3000/users/create稍后进行测试的地址:
太好了,现在 Insomnia 已经准备就绪!创建好事件后,我们只需要在请求正文中添加一个哈希值即可。
测试该create动作与byebug失眠
byebug您可以通过在控制器操作中添加以下代码来测试路由:
# app/controllers/users_controller.rb
def create
byebug
end
启动Insomnia rails s,并向目标服务器发送 POST 请求localhost:3000/users/create。在控制台中,您将看到byebug会话信息:
太好了,我们的路线运行正常!
现在,我们准备构建事件模式!
什么是事件?
在我们的事件溯源系统中,每个 Event 都是一个 Rails 模型,用于存储有关数据更改的信息。
我们的目标是举办两项赛事:
Events::User::Created——这将记录:payload:包含用于创建用户的参数的name哈希表emailpassworduser_id:创建的用户,在其关系id中使用belongs_toevent_type:一个字符串,表示这user_event是“已创建”类型- 时间戳
Events::User::Destroyed——这将记录:payloadid:包含要标记为已删除用户的哈希值user_id:目标用户的,在其关系id中使用belongs_toevent_type:一个字符串,表示这user_event是“已销毁”类型- 时间戳
当我们的 Rails 应用创建或销毁用户时,也会触发创建一个新的事件。
这些事件将被保存到我们的数据库中,并且不可更改,作为永久变更日志。
由于我们最终可能会有很多与用户相关的事件,因此我们也在event_type用户事件中添加了该字段,以便我们可以将它们全部存储在一个user_events表中——并且以后可以轻松地添加更多!
这Events::BaseEvent
我们的事件将通过继承构建。在继承链的顶端,我们将定义Events::BaseEvent大部分事件功能所在的位置。
由于我们所有的事件都将是 Rails 模型,请继续在内部创建一个新/events目录app/models。
现在我们可以创建基础事件了:
# app/models/events/base_event.rb
class Events::BaseEvent < ActiveRecord::Base
end
abstract_class
由于 BaseEvent 仅用于继承,我们可以将其设为非abstract_class空类型,以便 Rails 知道不要尝试为其加载任何记录:
# app/models/events/base_event.rb
class Events::BaseEvent < ActiveRecord::Base
self.abstract_class = true
end
apply(aggregate)和apply_and_persist
每个事件都需要定义自己的apply方法。该方法会接收一个aggregate对象(在本例中是 User 模型),并更新其属性。
(该术语aggregate源自 Kickstarter 的众筹系统,您可以在这里了解更多信息。简而言之,aggregates对象是接收更改的模型events。)
在 BaseEvent 上,我们只需引发一个异常NotImplementedError。这将强制我们在每个事件上显式定义它,从而通过继承覆盖错误。
BaseEvent 还会有一个before_create钩子调用 `getUpdate() apply_and_persist`。这将调用 `getUpdate()` apply,然后save!更新数据库。
(它还会设置事件的 `id` aggregate_id,特别是对于 Created 事件,因为在调用 `getUpdate id()` 之前 `id` 并不存在save!。)
我们来看一下要添加的代码:
# app/models/events/base_event.rb
before_create :apply_and_persist
def apply(aggregate)
raise NotImplementedError
end
private def apply_and_persist
# Lock the database row! (OK because we're in an ActiveRecord callback chain transaction)
aggregate.lock! if aggregate.persisted?
# Apply!
self.aggregate = apply(aggregate)
#Persist!
aggregate.save!
# Update aggregate_id with id from newly created User
self.aggregate_id = aggregate.id if aggregate_id.nil?
end
after_initialize和event_type
无论我们实例化哪种类型的事件,都有几个属性需要立即设置:
event_typeuser_events— 每个事件都需要明确分类,才能作为BaseEvent记录存储在表中。payload— 由于我们始终期望payload以哈希形式访问(并以 JSON 格式存储在 PostgreSQL 数据库中),因此我们将添加一个运算符,用于在事件不接受任何参数时||=将其设置为 true。{}
所以,我们将添加一个after_initialize钩子来设置这些属性:
# app/models/events/base_event.rb
after_initialize do
self.event_type = event_type
self.payload ||= {}
end
def event_type
self.attributes["event_type"] || self.class.to_s.split("::").last
end
上面我们定义了,如果从我们的数据库加载,event_type则可以通过快速访问其自身类型attributes;或者在首次创建时,从 Event 类的名称推断其类型。
self.payload_attributes(*attributes)
在我们创建的每个事件类中,我们都希望可以选择定义payload_attributes我们想要记录的内容。
在 BaseEvent 事件中,self.payload_attributes我们将创建有效负载字段的 getter 和 setter:
# app/models/events/base_event.rb
def self.payload_attributes(*attributes)
@payload_attributes ||= []
attributes.map(&:to_s).each do |attribute|
@payload_attributes << attribute unless @payload_attributes.include?(attribute)
define_method attribute do
self.payload ||= {}
self.payload[attribute]
end
define_method "#{attribute}=" do |argument|
self.payload ||= {}
self.payload[attribute] = argument
end
end
@payload_attributes
end
最终,这将允许我们在每个新的 Event 类的顶部定义如下属性:payload_attributes :name, :email, :password
find_or_build_aggregate
我们希望事件能够感知其聚合体(在本例中为目标用户),并能够查找该聚合体或创建一个新的聚合体。
我们将添加一个钩子(在生命周期的早期阶段before_validation调用),该钩子将根据事件的初始化参数中是否提供了 a 来查找或创建聚合:.createuser_id
# app/models/events/base_event.rb
before_validation :find_or_build_aggregate
private def find_or_build_aggregate
self.aggregate = find_aggregate if aggregate_id.present?
self.aggregate = build_aggregate if self.aggregate.nil?
end
def find_aggregate
klass = aggregate_name.to_s.classify.constantize
klass.find(aggregate_id)
end
def build_aggregate
public_send "build_#{aggregate_name}"
end
aggregate设置者、获取者和获取其名称者
为了完善事件的功能,我们需要一些设置器和获取器,以及一些可以轻松返回其类型或类名的方法:
aggregate=(model)并将aggregate设置并获取用户我们的事件目标aggregate_id=(id)并将aggregate_id映射到user_id我们user_events桌子上的字段。self.aggregate_name使 Event 类能够感知其belongs_to关系的关联目标类(#=> User)delegate :aggregate_name, to: :class将返回聚合类名的符号(#=> :user)def event_klass会将我们的 Event 类的::BaseEvent命名空间转换为其相应的事件类型(#=> Events::User::Created)
# app/models/events/base_event.rb
def aggregate=(model)
public_send "#{aggregate_name}=", model
end
def aggregate
public_send aggregate_name
end
def aggregate_id=(id)
public_send "#{aggregate_name}_id=", id
end
def aggregate_id
public_send "#{aggregate_name}_id"
end
def self.aggregate_name
inferred_aggregate = reflect_on_all_associations(:belongs_to).first
raise "Events must belong to an aggregate" if inferred_aggregate.nil?
inferred_aggregate.name
end
delegate :aggregate_name, to: :class
def event_klass
klass = self.class.to_s.split("::")
klass[-1] = event_type
klass.join('::').constantize
end
好的,让我们看看整个过程Events::BaseEvent!
# app/models/events/base_event.rb
# Kickstarter code reference:
# https://github.com/pcreux/event-sourcing-rails-todo-app-demo/blob/master/app/models/lib/base_event.rb
class Events::BaseEvent < ActiveRecord::Base
before_validation :find_or_build_aggregate
before_create :apply_and_persist
self.abstract_class = true
def apply(aggregate)
raise NotImplementedError
end
after_initialize do
self.event_type = event_type
self.payload ||= {}
end
def self.payload_attributes(*attributes)
@payload_attributes ||= []
attributes.map(&:to_s).each do |attribute|
@payload_attributes << attribute unless @payload_attributes.include?(attribute)
define_method attribute do
self.payload ||= {}
self.payload[attribute]
end
define_method "#{attribute}=" do |argument|
self.payload ||= {}
self.payload[attribute] = argument
end
end
@payload_attributes
end
private def find_or_build_aggregate
self.aggregate = find_aggregate if aggregate_id.present?
self.aggregate = build_aggregate if self.aggregate.nil?
end
def find_aggregate
klass = aggregate_name.to_s.classify.constantize
klass.find(aggregate_id)
end
def build_aggregate
public_send "build_#{aggregate_name}"
end
private def apply_and_persist
# Lock the database row! (OK because we're in an ActiveRecord callback chain transaction)
aggregate.lock! if aggregate.persisted?
# Apply!
self.aggregate = apply(aggregate)
#Persist!
aggregate.save!
self.aggregate_id = aggregate.id if aggregate_id.nil?
end
def aggregate=(model)
public_send "#{aggregate_name}=", model
end
def aggregate
public_send aggregate_name
end
def aggregate_id=(id)
public_send "#{aggregate_name}_id=", id
end
def aggregate_id
public_send "#{aggregate_name}_id"
end
def self.aggregate_name
inferred_aggregate = reflect_on_all_associations(:belongs_to).first
raise "Events must belong to an aggregate" if inferred_aggregate.nil?
inferred_aggregate.name
end
delegate :aggregate_name, to: :class
def event_type
self.attributes["event_type"] || self.class.to_s.split("::").last
end
def event_klass
klass = self.class.to_s.split("::")
klass[-1] = event_type
klass.join('::').constantize
end
end
桌子user_events,以及Events::User::BaseEvent
我们之前提到过,我们将把多种类型的用户相关事件存储在一个user_events表中。
为了实现这一点,并方便我们以后添加更多事件,我们将创建一个Events::User::BaseEvent事件管理器,指示Events::User::命名空间中的所有事件保存到user_events表中。我们还将belongs_to在此处定义与用户的关系。
user_events桌子
接下来,我们将user_events在数据库中创建表格。
Kickstarter 的事件溯源示例描述了每个aggregate用户都有一个事件表user_events。这些事件表将具有类似的结构——我们将对其进行一些调整以符合我们的描述:
每个聚合(例如:订阅)都关联一个事件表(例如:subscription_events)。
……
与某个聚合相关的所有事件都存储在同一个表中。所有事件表都具有类似的模式:id, aggregate_id, type, data (json), metadata (json), created_at
我们将对代码进行一些调整:
aggregate_id将被取代user_idtype将被event_type(为了更明确地说明)取代data将被替换为payload,并且仍为 JSON 类型。metadata由于我们的活动相对简单,因此目前暂不包含在内。created_at不会包含在内,因为我们将直接使用 ActiveRecord 的默认时间戳。
我们将user_events使用 Rails 迁移来创建表格:
rails g migration CreateUserEvents
这将创建一个已create_table为我们设置的迁移块:
# db/migrate/20200502192018_create_user_events.rb
class CreateUserEvents < ActiveRecord::Migration[6.0]
def change
create_table :user_events do |t|
end
end
end
我们想添加四个字段:
belongs_to与……的关系:user- 一个
event_type字符串 - 一个
payloadJSON - 时间戳
# db/migrate/20200502192018_create_user_events.rb
class CreateUserEvents < ActiveRecord::Migration[6.0]
def change
create_table :user_events do |t|
t.belongs_to :user, null: false, foreign_key: true
t.string :event_type
t.json :payload
t.timestamps
end
end
end
运行迁移:
rails db:migrate
打开 Postico 查看新user_events表格:
我们的餐桌和田地都准备就绪了!
Events::User::BaseEvent
在我们的目录下app/models/events,创建一个新user目录。
在该目录下创建一个新文件base_event.rb。这将为我们提供创建此类所需的命名空间:
# app/models/events/user/base_event.rb
class Events::User::BaseEvent < Events::BaseEvent
self.table_name = "user_events"
end
有了它self.table_name = “user_events”,我们创建的任何继承自该类的新 Event 类Events::User::BaseEvent都会自动保存并从user_events表中检索!
belongs_to :user和has_many :events
由于我们所有与用户相关的事件都以用户为目标,因此has_many / belongs_to在命名空间中创建用户和事件之间的关系是有意义的Events::User::。
由于我们身处一个使用了相同名称的命名空间深处User,为了让 Rails 查找常规的顶级User模型,我们需要::在类名前添加 `@classnames`。这会告诉我们的 `@classnames`has_many和 `@ belongs_toclassnames` 关系在当前命名空间之外查找。
让我们更新我们的类Events::User::BaseEvent,User使其包含这些关系:
# app/models/events/user/base_event.rb
class Events::User::BaseEvent < Events::BaseEvent
self.table_name = "user_events"
belongs_to :user, class_name: "::User"
end
# app/models/user.rb
class User < ApplicationRecord
has_many :events, class_name: "Events::User::BaseEvent"
end
太好了!现在,当我们把用户加载到一个user变量中时,我们就可以调用函数user.events从表中加载所有相关的事件了user_events。
我们现在已经准备好创建一些真正可用的活动了!
创建新用户Events::User::Created
有了 BaseEvent 模式,我们现在可以构建我们的第一个事件了!
Events::User::Created将记录用于创建用户的参数、新用户的 ID 以及事件的时间戳。
构建Events::User::Created班级
在 中app/models/events/user,创建一个新created.rb文件。我们的类将继承自Events::User::BaseEvent同一目录下的 类:
# app/models/events/user/created.rb
class Events::User::Created < Events::User::BaseEvent
end
正如我们在顶层定义的那样Events::BaseEvent,我们必须定义一个apply方法,该方法将接受一个 User 实例作为其aggregate参数:
# app/models/events/user/created.rb
class Events::User::Created < Events::User::BaseEvent
def apply(user)
end
end
既然我们知道创建用户需要参数,包括 getter name、 setteremail和passwordgett,我们也可以将它们作为符号列表添加到payload_attributesgetter 和 setter 中:
# app/models/events/user/created.rb
class Events::User::Created < Events::User::BaseEvent
payload_attributes :name, :email, :password
def apply(user)
end
end
向该apply方法添加逻辑
事件apply方法中的逻辑是事件强大之处的所在。它:
- 接收一个用户实例
- 将更改应用于由用户实例提供的用户实例。
payload_attributes - 返回已修改的 User 实例 =>顶级 BaseEvent 在这里接收 User 实例,并调用函数
save!将更改持久化到数据库中!
由于我们收到了传递给 `getUser()` 的属性列表payload_attributes,我们可以直接在方法内部调用这些属性apply来更新 User 实例:
# app/models/events/user/created.rb
payload_attributes :name, :email, :password
def apply(user)
user.name = name
user.email = email
user.password_digest = password
user
end
完美!现在,我们只需要告诉 Insomnia 传递包含 `<string>`、`<string>` 和字符串的参数name,email我们password的事件就会将它们映射到 User 模型的 `<string>` name、email`<string>` 和 ` password_digest<string>` 字段。
(记住: `<string> password_digest` 与功能相关bcrypt,我们将在另一篇文章中探讨。)
更新控制器,使其能够创建事件并使用强参数。
回到我们的系统中users_controller,我们需要更新两件事:
create需要采取行动Events::User::Created.create(payload: user_params)- 添加强参数以保护
user_params我们将要传递的内容.create(payload: user_params)
对于强参数,我们将要求键中user_params包含 ` name, email` 和 `,`,并且password这些参数嵌套在user键中:
# app/controllers/users_controller.rb
private def user_params
params.require(:user).permit(:name, :email, :password)
end
现在我们可以安全地进入user_params行动Events::User::Created.create(payload: user_params)阶段create了:
# app/controllers/users_controller.rb
def create
Events::User::Created.create(payload: user_params)
end
private def user_params
params.require(:user).permit(:name, :email, :password)
end
让我们用 Insomnia 和 Postico 来测试一下我们的活动吧!
如果我们通过 POST 请求发送正确的参数localhost:3000/users/create,我们预期会出现以下几种情况:
- 表格中新增一条记录
user_events,内容如下:event_type “Created”payload和user_params- 请注意,数据
password将以明文形式存储 =>这是不安全的行为,因为我们尚未实现 bcrypt 加密!
- 请注意,数据
user_id使用新创建的用户id
- 表格中新增一条记录
user,内容如下:- 正确的
name - 正确的
email password_digest这是明文password=>这是不安全的行为,因为我们还没有实现 bcrypt 加密!
- 正确的
我们来测试一下!
启动rails s并打开失眠症。
在我们的Create User请求中,将请求体设置为 JSON:
然后,创建一个 JSON 哈希,其中包含一个键,该键”user”指向一个包含以下元素的”name”哈希”email”:”password”
现在点击Send,让我们来看看数据库表!
首先,让我们看看user_events表中是否有事件记录:
目前一切顺利!
(请记住:以明文形式存储密码是不安全的行为,这是因为我们尚未实现 bcrypt 加密!)
现在,我们来看一下users表格:
太棒了!我们现在有了新用户,ongo_gablogian以及创建他的事件和参数记录!
好了!我们的事件溯源系统现在可以捕获数据的变化了!
只要我们不更改表格中的数据user_events,我们就能可靠地记录数据是如何达到当前状态的!
使用以下方式销毁用户Events::User::Destroyed
现在我们已经有了模式,创建一个新事件并将其记录到我们的user_events表中就非常简单了!
由于我们不希望丢失数据,因此我们deleted在 User 模型中实现了一个布尔字段。当创建一个新用户时,该字段的默认值为 true false。
让我们创建一个新事件,Events::User::Destroyed该事件会将deleted字段设置为true!
创建app/models/events/user/destroyed.rb文件
在与我们类相同的目录下Events::User::Created,创建一个等效的Events::User::Destroyed类:
# app/models/events/user/destroyed.rb
class Events::User::Destroyed < Events::User::BaseEvent
def apply(user)
user
end
end
上面,我们首先介绍一个简单的apply方法,该方法仅返回传入的 User 实例。
要删除用户,我们只需要一个删除操作id。让我们添加payload_attributes删除操作:
# app/models/events/user/destroyed.rb
class Events::User::Destroyed < Events::User::BaseEvent
payload_attributes :id
end
我们将使apply方法更新传入的 Userdeleted字段为true:
# app/models/events/user/destroyed.rb
class Events::User::Destroyed < Events::User::BaseEvent
payload_attributes :id
def apply(user)
user.deleted = true
user
end
end
好了——我们的新活动完成了!
更新destroy操作users_controller
在我们的代码中users_controller,我们将destroy通过创建一个新Events::User::Destroyed事件来执行我们的操作。
由于我们在顶层 BaseEvent 中定义了find_or_build_aggregateand方法,如果提供了参数,此事件将自动查找 User 。aggregate_id”Destroyed”user_id
首先,让我们id在强参数列表中添加以下内容user_params:
# app/controllers/users_controller.rb
private def user_params
params.require(:user).permit(:name, :email, :password, :id)
end
现在,我们的控制器destroy操作可以接受一个user_params包含必要信息的事件id。我们还将使用它user_params[:id],以便事件可以查找目标用户的记录:
# app/controllers/users_controller.rb
def destroy
Events::User::Destroyed.create(user_id: user_params[:id], payload: user_params)
end
我们准备开始对失眠症进行测试!
在 Insomnia 中测试 DELETE 请求
让我们开始吧rails s。
在 Insomnia 中,创建一个名为Destroy User“delete”的新请求:
将其目标 URL 设置为localhost:3000/users/destroy:
将正文类型设置为 JSON,并添加一个哈希表,该”user”哈希表的键指向包含冒号的哈希表”id”。
点击“发送”,然后检查数据库以查看事件是否已创建:
最后,让我们检查一下数据库,看看我们的用户是否已deleted设置为true:
太好了!我们既保留了用户记录,又实现了双重目标deleted——鱼与熊掌兼得!
只需这些步骤即可将新活动添加到我们的活动资源系统中!
结论
哇,我们讲了很多内容!让我们回顾一下我们实现事件溯源系统所采取的步骤:
- 创建一个新的 Rails 应用,包含 User 模型和控制器,并使用 PostgreSQL 作为数据库。
- 创建一个
Events::BaseEvent类app/models/events来处理事件逻辑:- 查找或创建聚合(用户)
- 创建 getter 和 setter
payload_attributes - 推断其自身
event_type - 用于自动应用更改并保存到数据库的钩子
- 创建
user_events表迁移 - 创建一个表
Events::User::BaseEvent,将命名空间中的所有事件保存Events::User::到user_events表中。 - 创建一个
Events::User::Created将应用于user_params新用户实例的事件 - 创建一个
Events::User::Destroyed事件,该事件将查找 Userid并将其deleted字段设置为true
这个极简系统使我们能够做到以下几点:
- 记录创建和销毁用户的事件
- 永久保留所有用户数据,并可
deleted根据需要限定保留范围。 - 这种模式允许我们轻松添加新事件,并将这些事件保存到同一张
user_events表中。
此演示的所有代码都可以在此 GitHub 仓库中找到:
https://github.com/isalevine/event-sourcing-user-app
接下来
我们还有很多工作可以做,以改进我们的事件溯源系统,尤其是在安全性和数据验证方面!下一篇文章我们将探讨以下内容:
- 在 Event 中安全地存储敏感信息
payloads,例如密码 - 根据 Kickstarter 的示例
Commands,将创建事件封装在 `<Event>`标签内。 - 添加验证
Commands
参考
特别感谢Philippe Creux和Kickstarter分享他们的 Event Sourcing 示例。
感谢Arkency为RailsEventStore 库所做的出色工作。
最后,感谢 Dev.to 用户Alfredo Motta多年前分享了这篇文章(并一直保留着,让我能够及时了解!)。
文章来源:https://dev.to/isalevine/building-an-event-source-pattern-in-rails-from-scratch-355h























