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(
mapper: RubyEventStore::Mappers::Default.new(
serializer: Marshal
)
)
end
end
The provided serializer
must respond to load
and dump
.
Configuring a different mapper
JSON is a popular serialization choice when paired with JSON/JSONB storage on Potgres. To make full use of it, set:
# config/environments/*.rb
Rails.application.configure do
config.to_prepare do
Rails.configuration.event_store = RailsEventStore::Client.new(
mapper: RubyEventStore::Mappers::JSONMapper.new
)
end
end
Additionally, if you are using Postgres, you can tell the migration to use JSON or JSONB data types for the data
and metadata
fields by
passing in a --data-type
to the generator.
$ rails generate rails_event_store_active_record:migration --data-type=jsonb
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:
serializer:
(described above)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 transformationserializer:
(described above)
- works with
Custom mapper
Mapper is defined as a pipeline of transformations that transforms domain event object into serialized record and back from serialized recoed to domain event. I.e. DefaultMapper
is implemented as a set of 3 transformations. Transformations works on transformation item objects, it 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 pipeline mapper needs to be given 2 edge transformations:
- to transform domain event to/from transformation item (default
RubyEventStore::Mappers::Transformation::DomainEvent
) - to transform serialized record to/from transformation item (default
RubyEventStore::Mappers::Transformation::SerializedRecord
)
Extended implementation of DefaultMapper with explicit default arguments:
module RubyEventStore
module Mappers
class Default < PipelineMapper
def initialize(serializer: RubyEventStore::Serializers::YAML, events_class_remapping: {})
super(Pipeline.new(
to_domain_event: Transformation::DomainEvent.new,
to_serialized_record: Transformation::Record.new,
transformations: [
Transformation::EventClassRemapper.new(events_class_remapping),
Transformation::SymbolizeMetadataKeys.new,
Transformation::Serialization.new(serializer: serializer),
]
))
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(item)
& load(item)
. Both methods take a RubyEventStore::Mappers::Transformation::Item
and returns new instance of it, transformed. Transformation shall not modify given transformation item and shall return new instance as a result.
If you use different classes to represent domain event & serialized record you shall also define edge transformations for your pipeline definition.
Example of custom mapper build based on mappers pipeline:
require 'msgpack'
class MessagePackSerialization
def dump(item)
RubyEventStore::Mappers::Transformation::Item.new(
event_id: item.event_id,
metadata: item.metadata.to_msg_pack,
data: item.data.to_msg_pack,
event_type: item.event_type
)
end
def load(item)
RubyEventStore::Mappers::Transformation::Item.new(
event_id: item.event_id,
metadata: MessagePack.unpack(item.metadata),
data: MessagePack.unpack(item.data),
event_type: item.event_type
)
end
end
class MyHashToMessagePackMapper < RubyEventStore::Mappers::PipelineMapper
def initialize
super(RubyEventStore::Mappers::Pipeline.new(
transformations: [
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')