Event serialization formats
By default RailsEventStore will use YAML
as a
serialization format. The reason is that YAML
is available out of box
and can serialize and deserialize data types which are not easily
handled in other formats.
However, if you don't like YAML
or you have different needs you can
choose to use different serializers or even replace mappers entirely.
Configuring a different serializer
You can pass a different serializer
as a dependency when instantiating
the client.
Here is an example on how to configure RailsEventStore to serialize
events' data
and metadata
using Marshal
.
# config/environments/*.rb
Rails.application.configure do
config.to_prepare do
Rails.configuration.event_store = RailsEventStore::Client.new(
repository: RailsEventStoreActiveRecord::EventRepository.new(serializer: Marshal)
)
end
end
The provided serializer
must respond to load
and dump
.
Serialization is needed not only when writing to and reading from storage, but also when scheduling events for background processing by async handlers:
Rails.configuration.event_store = RailsEventStore::Client.new(
dispatcher: RubyEventStore::ComposedDispatcher.new(
RailsEventStore::AfterCommitAsyncDispatcher.new(scheduler: ActiveJobScheduler.new(serializer: Marshal)),
RubyEventStore::Dispatcher.new
)
)
class SomeHandler < ActiveJob::Base
include RailsEventStore::AsyncHandler.with(serializer: Marshal)
def perform(event)
# ...
end
end
Configuring a different mapper
JSON is a popular serialization choice when paired with JSON/JSONB storage on Postgres. To make full use of it set or migrate your data
and metadata
columns to jsonb
first.
$ rails generate rails_event_store_active_record:migration --data-type=jsonb
Next set repository serialization to RailsEventStoreActiveRecord::EventRepository.new(serializer: RubyEventStore::NULL)
# config/environments/*.rb
Rails.application.configure do
config.to_prepare do
Rails.configuration.event_store = RailsEventStore::Client.new(
event_repository: RubyEventStore::Mappers::JSONMapper.new
)
end
end
Bear in mind that JSON
will convert symbols to strings and you have to prepare for that when retrieving events.
JSON.load(JSON.dump({foo: :bar}))
=> {"foo"=>"bar"}
One way to approach this is to have your own event adapter, specific for the project you're working on.
class MyEvent < RailsEventStore::Event
def data
ActiveSupport::HashWithIndifferentAccess.new(super)
end
end
OrderPlaced = Class.new(MyEvent)
That shields you from data keys being transformed from symbols into strings. It doesn't do anything with data values associated to those keys.
event_store.publish(OrderPlaced.new(event_id: 'e34fc19a-a92f-4c21-8932-a10f6fb2602b', data: { foo: :bar }))
event = event_store.read.event('e34fc19a-a92f-4c21-8932-a10f6fb2602b')
event.data[:foo]
# => "bar"
event.data['foo']
# => "bar"
Another way to achive that could be define your own custom mapper and transformation
Available mappers
RubyEventStore::Mappers::Default
- works with
RubyEventStore::Event
orRailsEventStore::Event
- constructor takes named arguments:
events_class_remapping:
- which can be used for mapping old event names to new ones after you refactored your codebase.
- works with
RubyEventStore::Mappers::Protobuf
- works with Ruby classes generated by
google-protobuf
gem
- works with Ruby classes generated by
RubyEventStore::Mappers::NullMapper
- works with
RubyEventStore::Event
orRailsEventStore::Event
- no transformations, useful in tests
- works with
RubyEventStore::Mappers::EncryptionMapper
- works with
RubyEventStore::Event
orRailsEventStore::Event
- constructor takes named arguments:
key_repository
- which is responsible for providing encryption keys used to encrypt/descrypt payload by encryption transformation
- works with
Custom mapper
Mapper is defined as a pipeline of transformations that transforms domain event object into record and back from record to domain event. I.e. Default
is implemented as a set of 2 transformations. Transformation works on Record objects. Such Record
is given to transformation methods (it is expected that load
& dump
methods are implemented) and it is result of any transformation performed. Except transformations, each PipelineMapper
needs to be given 1 edge transformation:
- to transform domain event to/from record (default
RubyEventStore::Mappers::Transformation::DomainEvent
)
Extended implementation of Default with explicit default arguments:
module RubyEventStore
module Mappers
class Default < PipelineMapper
def initialize(events_class_remapping: {})
super(Pipeline.new(
Transformation::EventClassRemapper.new(events_class_remapping),
Transformation::SymbolizeMetadataKeys.new,
to_domain_event: Transformation::DomainEvent.new
))
end
end
end
end
When you define new custom mapper you could use mapper PipelineMapper as base class and provide your transformations pipeline.
Each transformation must implement 2 methods: dump(record)
& load(record)
. Both methods take a RubyEventStore::Record
and return new instance of it, transformed. Transformation shall not modify given record and shall return new instance as a result.
If you use different class to represent domain event you shall also define edge transformation for your pipeline definition.
Example of custom mapper build based on mappers pipeline:
require 'msgpack'
class MessagePackSerialization
def dump(record)
RubyEventStore::Record.new(
event_id: record.event_id,
metadata: record.metadata.to_msg_pack,
data: record.data.to_msg_pack,
event_type: record.event_type,
timestamp: record.timestamp,
valid_at: record.valid_at
)
end
def load(record)
RubyEventStore::Record.new(
event_id: record.event_id,
metadata: MessagePack.unpack(record.metadata),
data: MessagePack.unpack(record.data),
event_type: record.event_type,
timestamp: record.timestamp,
valid_at: record.valid_at
)
end
end
class MyHashToMessagePackMapper < RubyEventStore::Mappers::PipelineMapper
def initialize
super(RubyEventStore::Mappers::Pipeline.new(
MessagePackSerialization.new
))
end
end
You could build your own transformations or use existing ones combined with the ones build by you. We strongly encourage you to share your transformations as a part of RES-contrib.
Check out the code of our mappers and transformations on github for examples on how to implement mappers & transformations.
You can pass a different mapper
as a dependency when instantiating the client.
# config/environments/*.rb
Rails.application.configure do
config.to_prepare do
Rails.configuration.event_store = RailsEventStore::Client.new(
mapper: MyHashToMessagePackMapper.new
)
end
end
Now you should be able to publish your events:
class OrderPlaced < RubyEventStore::Event
end
event_store = Rails.configuration.event_store
event_store.publish(OrderPlaced.new(data: {
'event_id' => SecureRandom.uuid,
'order_id' => 1,
'order_amount' => BigDecimal.new('120.55'),
}), stream_name: 'Order$1')