Skip to content

Commit 63c21a0

Browse files
committed
New docs version — 2.17.0
1 parent a3cf540 commit 63c21a0

39 files changed

+4621
-0
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
title: Bi-Temporal EventSourcing
3+
---
4+
5+
Sometimes in the Event-Sourced world knowing when a specific event happened is not enough. In some business cases, there's also a necessity to know at which point in time a particular event was valid.
6+
This approach is called Bi-Temporal EventSourcing.
7+
8+
## Example
9+
10+
Consider you're an HR person responsible for dealing with employee salaries. Your tool of choice to gather the data is an Excel sheet and email.
11+
When you gather the information from managers, you put them into an Excel sheet and mail them to payroll. Payroll is taking care of the money getting into employees' bank accounts. You also import the Excel sheet into your HR system, which happens to be Event Sourced.
12+
13+
Usually, things go great and we build our stream of salary-tracking events like this:
14+
![BiTemporalEventSourcingWhenThingsGoSmoothly](/images/bi_temporal_event_sourcing_when_things_go_smoothly.jpg)
15+
However an error might happen. The error might happen when gathering the data from manager or putting it into an Excel sheet.
16+
17+
Instead of asking a developer to modify the event data (events should always be immutable!), you could use a bi-temporal event. Consider the example below:
18+
![BiTemporalEventSourcingValidAt](/images/bi_temporal_valid_at_event_sourcing.jpg)
19+
In this example, the salary was raised on January 1, 2020, and it was also paid out on February 1, 2020. Then the mistake was detected. Instead of modifying the history of events, the new one has been published. The new event describes the proper value of the salary and specifies when the salary is valid.
20+
21+
## Usage
22+
23+
If you decide to use the Bi-Temporal EventSourcing approach for a stream, you have to include the `valid_at` property to the event's metadata.
24+
`valid_at` describes when the event was actually valid, despite when it occurred. Such an event is also often called a Retroactive event.
25+
26+
```ruby
27+
event_store.publish(Event.new(data: {}, metadata: {valid_at: Time.utc(2020,1,1)}))
28+
```
29+
30+
When reading a stream with bi-temporal events you can either read the events by using:
31+
32+
* `as_at` scope, which orders events by `timestamp`, which is the time of appending the event to the stream
33+
* `as_of` scope, which orders events by `valid_at`, which is the time of when the event was actually valid
34+
35+
```ruby
36+
event_store.read.stream("my-stream").as_at.to_a # ordered by time of appending (timestamp)
37+
event_store.read.stream("my-stream").as_of.to_a # ordered by validity time (valid_at)
38+
```
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
---
2+
title: Command Bus
3+
---
4+
5+
_Command Pattern - decoupling what is done from who does it._
6+
7+
## Usage
8+
9+
### Registering commands and their handlers
10+
11+
```ruby
12+
require "arkency/command_bus"
13+
14+
command_bus = Arkency::CommandBus.new
15+
register = command_bus.method(:register)
16+
17+
{ FooCommand => FooService.new(event_store: event_store).method(:foo), BarCommand => BarService.new }.map(&register)
18+
```
19+
20+
### Invoking command bus with a command
21+
22+
```ruby
23+
command_bus.(FooCommand.new)
24+
```
25+
26+
Will call `FooService#foo` method with the command you just passed.
27+
28+
## New instance of a service for every invoked command
29+
30+
If you need a new instance of a service, every time it is called with a command, or you want to lazily load the responsible services, use `Proc` when registering commands.
31+
32+
```ruby
33+
command_bus = Arkency::CommandBus.new
34+
command_bus.register(FooCommand, ->(foo_cmd) { FooService.new(dependency: dep).foo(foo_cmd) })
35+
command_bus.register(BarCommand, ->(bar_cmd) { BarService.new.call(bar_cmd) })
36+
```
37+
38+
## Working with Rails development mode
39+
40+
In Rails `development` mode when you change a registered class, it is reloaded, and a new class with same name is constructed.
41+
42+
```ruby
43+
a = User
44+
a.object_id
45+
# => 40737760
46+
47+
reload!
48+
# Reloading...
49+
50+
b = User
51+
b.object_id
52+
# => 48425300
53+
54+
h = { a => 1, b => 2 }
55+
h[User]
56+
# => 2
57+
58+
a == b
59+
# => false
60+
```
61+
62+
so your `Hash` with mapping from command class to service may not find the new version of reloaded class.
63+
64+
To workaround this problem you can use [`to_prepare`](http://api.rubyonrails.org/classes/Rails/Railtie/Configuration.html#method-i-to_prepare) callback which is executed before every code reload in development, and once in production.
65+
66+
```ruby
67+
config.to_prepare do
68+
config.command_bus = Arkency::CommandBus.new
69+
register = command_bus.method(:register)
70+
71+
{ FooCommand => FooService.new(event_store: event_store).method(:foo), BarCommand => BarService.new }.map(&register)
72+
end
73+
```
74+
75+
and call it with
76+
77+
```ruby
78+
Rails.configuration.command_bus.call(FooCommand.new)
79+
```
80+
81+
## Convenience alias
82+
83+
```ruby
84+
require "arkency/command_bus/alias"
85+
```
86+
87+
From now on you can use top-level `CommandBus` instead of `Arkency::CommandBus`.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
---
2+
title: Custom Repository
3+
---
4+
5+
## Installation with Bundler
6+
7+
By default RailsEventStore will use the ActiveRecord event repository. If you want to use another event repository without loading unnecessary ActiveRecord dependency, you'll need to do:
8+
9+
```ruby
10+
gem "rails_event_store", require: "rails_event_store/all"
11+
gem "your_custom_repository"
12+
```
13+
14+
After running `bundle install`, Rails Event Store should be ready to be used.
15+
See custom repository README to learn how to setup its data store.
16+
17+
## Configure custom repository
18+
19+
```ruby
20+
Rails.application.configure do
21+
config.to_prepare do
22+
Rails.configuration.event_store = RailsEventStore::Client.new(repository: YourCustomRepository.new)
23+
end
24+
end
25+
```
26+
27+
## Community event repositories
28+
29+
Those repositories were written by community members and are not guaranteed to be up to date with latest Rails event store.
30+
31+
- [rails_event_store_mongoid](https://github.com/gottfrois/rails_event_store_mongoid) by [Pierre-Louis Gottfrois](https://github.com/gottfrois)
32+
33+
## Writing your own repository
34+
35+
If you want to write your own repository, we provide [a suite of tests that you can re-use](https://github.com/RailsEventStore/rails_event_store/blob/master/ruby_event_store/lib/ruby_event_store/spec/event_repository_lint.rb). Just [require](https://github.com/RailsEventStore/rails_event_store/blob/a6ffb8a535373023296222bbbb5dd6ee131a6792/rails_event_store_active_record/spec/event_repository_spec.rb#L3) and [include it](https://github.com/RailsEventStore/rails_event_store/blob/a6ffb8a535373023296222bbbb5dd6ee131a6792/rails_event_store_active_record/spec/event_repository_spec.rb#L26) in your repository spec. Make sure to meditate on which [expected_version option](./../core-concepts/expected-version/) you are going to support and how.
36+
37+
## Using RubyEventStore::InMemoryRepository for faster tests
38+
39+
RubyEventStore comes with `RubyEventStore::InMemoryRepository` that you can use in tests instead of the default one. `InMemoryRepository` does not persist events but offers the same characteristics as `RailsEventStoreActiveRecord::EventRepository`. It is tested with the same test suite and raises identical exceptions.
40+
41+
```ruby
42+
RSpec.configure do |c|
43+
c.around(:each)
44+
Rails.configuration.event_store = RailsEventStore::Client.new(
45+
repository: RubyEventStore::InMemoryRepository.new
46+
)
47+
# add subscribers here
48+
end
49+
end
50+
```
51+
52+
If you want even faster tests you can additionally skip event's serialization.
53+
54+
```ruby
55+
RSpec.configure do |c|
56+
c.around(:each)
57+
Rails.configuration.event_store = RailsEventStore::Client.new(
58+
repository: RubyEventStore::InMemoryRepository.new,
59+
mapper: RubyEventStore::Mappers::NullMapper.new,
60+
)
61+
# add subscribers here
62+
end
63+
end
64+
```
65+
66+
We don't recommend using `InMemoryRepository` in production even if you don't need to persist events because the repository keeps all published events in memory. This is acceptable in testing because you can throw the instance away for every test and garbage collector reclaims the memory. In production, your memory would keep growing until you restart the application server.
67+
68+
## Using Ruby Object Mapper (ROM) for SQL without ActiveRecord or Rails
69+
70+
RubyEventStore comes with `RubyEventStore::ROM::EventRepository` that you can use with a SQL database without requiring ActiveRecord or when not using Rails altogether. It is tested with the same test suite as the ActiveRecord implementation and raises identical exceptions.
71+
72+
See [Using Ruby Event Store without Rails](./without-rails) for information on how to use ROM (and Sequel).
73+
74+
## Using PgLinearizedEventRepository for linearized writes
75+
76+
`rails_event_store_active_record` comes with additional version of repository named `RailsEventStoreActiveRecord::PgLinearizedEventRepository`.
77+
It is almost the same as regular active record repository, but has linearized writes to the database and is only restricted to work in `PostgreSQL` database (as the name suggests).
78+
79+
There are usecases, where you may want to use event store as a queue. For example, you may want to build some read models on separate server and in order to build them correctly, you need to process the facts in the order they were written. In general case it is not that easy, because SQL databases auto-increment rows in the moment of insertion, not commit. So that allows event numbered 42 be already committed, but event numbered 40 still be somewhere in transaction, not readable from outside world. Therefore, the easiest implementation of such queue: "Remember the last processed event id" would not work in that case.
80+
81+
There are many subtleties in this topic, but one of the simplest solutions is to linearize all writes to event store. That's what `RailsEventStoreActiveRecord::PgLinearizedEventRepository` is for. Of course by linearizing your writes you lose performance and you make it impossible to scale your application above certain level. As usually, your mileage may vary, but such solution is undoubtedly the simplest and _good enough_ in some usecases.
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
---
2+
title: Event serialization formats
3+
---
4+
5+
By default RailsEventStore will use `YAML` as a
6+
serialization format. The reason is that `YAML` is available out of box
7+
and can serialize and deserialize data types which are not easily
8+
handled in other formats.
9+
10+
However, if you don't like `YAML` or you have different needs you can
11+
choose to use different serializers or even replace mappers entirely.
12+
13+
## Configuring a different serializer
14+
15+
You can pass a different `serializer` as a dependency when [instantiating
16+
the client](../getting-started/install).
17+
18+
Here is an example on how to configure RailsEventStore to serialize
19+
events' `data` and `metadata` using `Marshal`.
20+
21+
```ruby
22+
# config/environments/*.rb
23+
24+
Rails.application.configure do
25+
config.to_prepare do
26+
Rails.configuration.event_store = RailsEventStore::Client.new(
27+
repository: RailsEventStoreActiveRecord::EventRepository.new(serializer: Marshal)
28+
)
29+
end
30+
end
31+
```
32+
33+
The provided `serializer` must respond to `load` and `dump`.
34+
35+
Serialization is needed not only when writing to and reading from storage, but also when scheduling events for background processing by async handlers:
36+
37+
```ruby
38+
Rails.configuration.event_store = RailsEventStore::Client.new(
39+
message_broker: RubyEventStore::Broker.new(
40+
dispatcher: RubyEventStore::ComposedDispatcher.new(
41+
RailsEventStore::AfterCommitAsyncDispatcher.new(scheduler: ActiveJobScheduler.new(serializer: Marshal)),
42+
RubyEventStore::Dispatcher.new
43+
)
44+
)
45+
)
46+
```
47+
48+
```ruby
49+
class SomeHandler < ActiveJob::Base
50+
include RailsEventStore::AsyncHandler.with(serializer: Marshal)
51+
52+
def perform(event)
53+
# ...
54+
end
55+
end
56+
```
57+
58+
## Configuring for Postgres JSON/B data type
59+
60+
In Postgres database, you can store your events data and metadata in json or jsonb format.
61+
62+
To generate migration containing event table schemas run
63+
64+
```console
65+
$ rails generate rails_event_store_active_record:migration --data-type=jsonb
66+
```
67+
68+
Next, configure your event store client to the JSON client:
69+
70+
```ruby
71+
Rails.configuration.event_store = RailsEventStore::JSONClient.new
72+
```
73+
74+
If you need additional configuration beyond the included JSON client, continue from here. In your `RailsEventStore::Client` initialization, set repository serialization to ` RailsEventStoreActiveRecord::EventRepository.new(serializer: RubyEventStore::NULL)`
75+
76+
```ruby
77+
# config/environments/*.rb
78+
79+
Rails.application.configure do
80+
config.to_prepare do
81+
Rails.configuration.event_store = RailsEventStore::Client.new(
82+
repository: RailsEventStoreActiveRecord::EventRepository.new(serializer: RubyEventStore::NULL)
83+
)
84+
end
85+
end
86+
```
87+
88+
Using the `RubyEventStore::NULL` serializer will prevent the event store from serializing the event data and metadata. This is necessary because the Active Record will handle serialization before putting the data into the database. And will do otherwise when reading. Database itself expect data to be json already.
89+
90+
<div class="px-4 py-1 text-blue-600 bg-blue-100 border-l-4 border-blue-500" role="alert">
91+
<p class="text-base font-bold">Note that <code>JSON</code> converts symbols to strings. Ensure your code accounts for this when retrieving events.</p>
92+
93+
```ruby
94+
JSON.load(JSON.dump({foo: :bar}))
95+
=> {"foo"=>"bar"}
96+
```
97+
98+
One way to approach this is to have your own event adapter, specific for the project you're working on.
99+
100+
```ruby
101+
class MyEvent < RailsEventStore::Event
102+
def data
103+
ActiveSupport::HashWithIndifferentAccess.new(super)
104+
end
105+
end
106+
107+
OrderPlaced = Class.new(MyEvent)
108+
```
109+
110+
111+
That shields you from data keys being transformed from symbols into strings. It doesn't do anything with data values associated to those keys.
112+
113+
```ruby
114+
event_store.publish(OrderPlaced.new(event_id: 'e34fc19a-a92f-4c21-8932-a10f6fb2602b', data: { foo: :bar }))
115+
event = event_store.read.event('e34fc19a-a92f-4c21-8932-a10f6fb2602b')
116+
117+
event.data[:foo]
118+
\# => "bar"
119+
120+
event.data['foo']
121+
\# => "bar"
122+
```
123+
124+
Another way to achieve that could be define your own <a href="../advanced-topics/mappers#custom-mapper">custom mapper and transformation</a>
125+
126+
</div>

0 commit comments

Comments
 (0)