@gotar | @oskarszrajer gotar.info
Oskar Szrajer
One year with
on production
What is ROM
(Ruby Object Mapper)
https://github.com/orgs/rom-rb
PERSISTENCE & MAPPING
TOOLKIT FOR RUBY
http://rom-rb.org
Author of ROM
Piotr Solnica | @_solnic_ | http://solnic.eu
author of ROM, Virtus, Dry-*, Rodakase, ...
Community
• dozen of devs
• very active Gitter channel
https://gitter.im/rom-rb/chat
• stable API >1.0v
• battle tested on production
History of ROM
• successor of DataMapper - aka DataMapper v2
• first commit - Feb 1 2012
• first version - v0.1 - Jun 15 2013
• first public version - v0.6 - Mar 22, 2015
• v1.0 - stable API - Jan 6, 2016
Core ideas
• reduced global state to minimum
• immutability
• mix OO with FP
• blazing fast
Core concepts
• Configuration
• Relations
• Commands
• Mappers
Configuration
require 'rom'
config = ROM::Configuration.new(
:sql, 'postgres://localhost/rom_repo'
)
rom = ROM.container(config)
rom.gateways[:default]
# #<ROM::SQL::Gateway:0x007fde7a087158>
ROM::Configuration.new(
default: [:sql, 'postgres://localhost/db'],
legacy: [:sql, 'mysql://localhost/legacy_db'],
marketing: [:yesql, 'postgres://localhost/db', path: './sql'],
cache: [:redis, 'redis://localhost'],
csv: [:csv, File.expand_path('./data.csv', __FILE__],
events: [:event_store, '127.0.0.1:1113']
# ...
)
Relations
class Users < ROM::Relation[:sql]
def by_id(id)
where(id: id)
end
end
rom.relation(:users).by_id(1).to_a
# [{:id=>1, :name=>"Jane"}]
Everything is lazy
(callable)
rom.relation(:users).by_id(1)
#<Db::Relations::Users dataset=#<Sequel::Postgres::Dataset: "SELECT "id", ...
rom.relation(:users).by_id(1).call
# [{:id=>1, :name=>"Jane"}]
rom.relation(:users).by_id(1).only(:id).to_a
# [{:id=>1}]
rom.relation(:users).by_id(1).only(:id).one
# {:id=>1}
Commands
class Create < ROM::Commands::Create[:sql]
relation :users
register_as :create
result :one
end
create_user = rom.command(:users).create
create_user.call(name: 'Jack')
# {id: 2, name: 'Jack'}
class Create < ROM::Commands::Create[:sql]
relation :users
register_as :create
input NewUserParams
validator UserValidator
result :one
end
class AuthorWithBooks < ROM::Commands::Create[:sql]
#...
end
create_authors_with_books = rom.command(:authors) do |authors|
authors.create(:books)
end
create_authors_with_books[authors: evaluated_authors]
# {:id=>1, :code=>"jd001", :name=>"Jane Doe", :email=>"jane@doe.org"}
# {:id=>1, :author_id=>1, :title=>"First", :published_on=>#<Date: 2010-10-10 ((2455480j,
0s,0n),+0s,2299161j)>}
# {:id=>2, :author_id=>1, :title=>"Second", :published_on=>#<Date: 2011-11-11 ((2455877j,
0s,0n),+0s,2299161j)>}
# {:id=>3, :author_id=>1, :title=>"Third", :published_on=>#<Date: 2012-12-12 ((2456274j,
0s,0n),+0s,2299161j)>}
Mappers
class UsersMapper < ROM::Mapper
relation :users
register_as :entity
attribute :id
attribute :name
attribute :age do
rand(18..90)
end
end
users = rom.relation(:users).as(:entity)
users.call
[
{ id: 1, name: "Jane", age: 22 },
{ id: 2, name: "Jack", age: 64 }
]
Pipelines
(everything that responds to #call
can be a mapper)
only_name = -> input { input.map{|u| u[:name]} }
upcase = -> input { input.map{|x| x.upcase} }
users = rom.relation(:users)
result = users >> only_name >> upcase
result.to_a
> ["JANE", "JACK"]
Domain data types &
Repositories
class User < Dry::Types::Value
attribute :name, Types::Strict::String
end
class UserRepo < ROM::Repository
relation :users
def all
users.select(:name).as(User).to_a
end
end
user_repo = UserRepo.new(rom)
user_repo.all
> [#<User name="Jane">, #<User name="Jack">]
My story
My story
Both project use
similar technologies:
• Roda (as HTTP layer)
• ROM
• Yaks (JSON API)
Request cycle
ParamsObject >> Validator >> ROM >>
Mapper(s) >> JSON API Mapper
Mapping &
coercion layer
Mapping &
coercion layer
combine
(or change)
data
convert data to
json api format
CQRS
combining data
class UserRepository < ROM::Repository::Base
relations :users, :tasks
def with_tasks(id)
users.by_id(id).combine_children(many: tasks)
end
end
user_repo.with_tasks.to_a
# [#<ROM::Struct[User] id=1 name="Jane" tasks=[#<ROM::Struct[Task] id=2
user_id=1 title="Jane Task">]>, #<ROM::Struct[User] id=2 name="Jack"
tasks=[#<ROM::Struct[Task] id=1 user_id=2 title="Jack Task">]>]
pros
• separating persistence from application &
encouraging that boundary
• ability to choose appropriate storage (SQL, NoSQL, ...),
but keeping the same API
• possibility to pull from different data sources,
and combine everything together
pros
• easy way to map (manipulate, coerce) data
• freedom
• community
cons
• commands (right now repository do not support them,
force to use very low level API, should be done before v2)
• hard to learn (many new concepts)
• lack of documentation (community will help you)
(currently community works to fix it, and there is always
someone sitting on Gitter to help you)
Worth it?
Q&A
@gotar | @oskarszrajer gotar.info
Thx
@gotar | @oskarszrajer gotar.info

1 year with ROM on production

  • 1.
    @gotar | @oskarszrajergotar.info Oskar Szrajer
  • 2.
    One year with onproduction
  • 3.
    What is ROM (RubyObject Mapper) https://github.com/orgs/rom-rb PERSISTENCE & MAPPING TOOLKIT FOR RUBY http://rom-rb.org
  • 4.
    Author of ROM PiotrSolnica | @_solnic_ | http://solnic.eu author of ROM, Virtus, Dry-*, Rodakase, ...
  • 5.
    Community • dozen ofdevs • very active Gitter channel https://gitter.im/rom-rb/chat • stable API >1.0v • battle tested on production
  • 6.
    History of ROM •successor of DataMapper - aka DataMapper v2 • first commit - Feb 1 2012 • first version - v0.1 - Jun 15 2013 • first public version - v0.6 - Mar 22, 2015 • v1.0 - stable API - Jan 6, 2016
  • 7.
    Core ideas • reducedglobal state to minimum • immutability • mix OO with FP • blazing fast
  • 8.
    Core concepts • Configuration •Relations • Commands • Mappers
  • 9.
    Configuration require 'rom' config =ROM::Configuration.new( :sql, 'postgres://localhost/rom_repo' ) rom = ROM.container(config) rom.gateways[:default] # #<ROM::SQL::Gateway:0x007fde7a087158>
  • 10.
    ROM::Configuration.new( default: [:sql, 'postgres://localhost/db'], legacy:[:sql, 'mysql://localhost/legacy_db'], marketing: [:yesql, 'postgres://localhost/db', path: './sql'], cache: [:redis, 'redis://localhost'], csv: [:csv, File.expand_path('./data.csv', __FILE__], events: [:event_store, '127.0.0.1:1113'] # ... )
  • 11.
    Relations class Users <ROM::Relation[:sql] def by_id(id) where(id: id) end end rom.relation(:users).by_id(1).to_a # [{:id=>1, :name=>"Jane"}]
  • 12.
    Everything is lazy (callable) rom.relation(:users).by_id(1) #<Db::Relations::Usersdataset=#<Sequel::Postgres::Dataset: "SELECT "id", ... rom.relation(:users).by_id(1).call # [{:id=>1, :name=>"Jane"}] rom.relation(:users).by_id(1).only(:id).to_a # [{:id=>1}] rom.relation(:users).by_id(1).only(:id).one # {:id=>1}
  • 13.
    Commands class Create <ROM::Commands::Create[:sql] relation :users register_as :create result :one end create_user = rom.command(:users).create create_user.call(name: 'Jack') # {id: 2, name: 'Jack'}
  • 14.
    class Create <ROM::Commands::Create[:sql] relation :users register_as :create input NewUserParams validator UserValidator result :one end
  • 15.
    class AuthorWithBooks <ROM::Commands::Create[:sql] #... end create_authors_with_books = rom.command(:authors) do |authors| authors.create(:books) end create_authors_with_books[authors: evaluated_authors] # {:id=>1, :code=>"jd001", :name=>"Jane Doe", :email=>"jane@doe.org"} # {:id=>1, :author_id=>1, :title=>"First", :published_on=>#<Date: 2010-10-10 ((2455480j, 0s,0n),+0s,2299161j)>} # {:id=>2, :author_id=>1, :title=>"Second", :published_on=>#<Date: 2011-11-11 ((2455877j, 0s,0n),+0s,2299161j)>} # {:id=>3, :author_id=>1, :title=>"Third", :published_on=>#<Date: 2012-12-12 ((2456274j, 0s,0n),+0s,2299161j)>}
  • 16.
    Mappers class UsersMapper <ROM::Mapper relation :users register_as :entity attribute :id attribute :name attribute :age do rand(18..90) end end
  • 17.
    users = rom.relation(:users).as(:entity) users.call [ {id: 1, name: "Jane", age: 22 }, { id: 2, name: "Jack", age: 64 } ]
  • 18.
    Pipelines (everything that respondsto #call can be a mapper) only_name = -> input { input.map{|u| u[:name]} } upcase = -> input { input.map{|x| x.upcase} } users = rom.relation(:users) result = users >> only_name >> upcase result.to_a > ["JANE", "JACK"]
  • 19.
    Domain data types& Repositories class User < Dry::Types::Value attribute :name, Types::Strict::String end class UserRepo < ROM::Repository relation :users def all users.select(:name).as(User).to_a end end
  • 20.
    user_repo = UserRepo.new(rom) user_repo.all >[#<User name="Jane">, #<User name="Jack">]
  • 21.
  • 23.
  • 24.
    Both project use similartechnologies: • Roda (as HTTP layer) • ROM • Yaks (JSON API)
  • 25.
    Request cycle ParamsObject >>Validator >> ROM >> Mapper(s) >> JSON API Mapper Mapping & coercion layer Mapping & coercion layer combine (or change) data convert data to json api format CQRS
  • 26.
    combining data class UserRepository< ROM::Repository::Base relations :users, :tasks def with_tasks(id) users.by_id(id).combine_children(many: tasks) end end user_repo.with_tasks.to_a # [#<ROM::Struct[User] id=1 name="Jane" tasks=[#<ROM::Struct[Task] id=2 user_id=1 title="Jane Task">]>, #<ROM::Struct[User] id=2 name="Jack" tasks=[#<ROM::Struct[Task] id=1 user_id=2 title="Jack Task">]>]
  • 27.
    pros • separating persistencefrom application & encouraging that boundary • ability to choose appropriate storage (SQL, NoSQL, ...), but keeping the same API • possibility to pull from different data sources, and combine everything together
  • 28.
    pros • easy wayto map (manipulate, coerce) data • freedom • community
  • 29.
    cons • commands (rightnow repository do not support them, force to use very low level API, should be done before v2) • hard to learn (many new concepts) • lack of documentation (community will help you) (currently community works to fix it, and there is always someone sitting on Gitter to help you)
  • 30.
  • 31.
  • 32.