1 year with rom on production
TRANSCRIPT
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 channelhttps://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
Configurationrequire '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}
Commandsclass 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=>"[email protected]"} # {: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)>}
Mappersclass 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
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 dataclass 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
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)