MEET ROM-RB & DRY-RB
PIOTR SOLNICA
HI PIVORAK!
I’M SOLNIC :)
mojotech.com
rom-rb.org dry-rb.org
THE STORY
DATAMAPPER
2010
DATAMAPPER
▸ Active Record ORM with explicit model property
definitions
▸ Advanced query API in 2009 already (lazy queries, lazy
properties, advanced query operators etc.)
▸ Custom data types for properties
▸ Database-agnostic through adapters
VIRTUS
2011
VIRTUS
▸ Typed attribute definitions in PORO extracted from
DataMapper
▸ Advanced coercion mechanism
▸ Fully configurable and flexible
▸ Form objects, data objects (ie loaded from JSON),
coercion backends etc.
▸ For some reason people loved it…
DATAMAPPER 2
2012
DATAMAPPER 2
▸ Active Record => Data Mapper (patterns)
▸ Adapters based on pure relational algebra backend
▸ Advanced Session with Unit of Work for state tracking and
data synchronization
▸ Didn’t really work out ¯_(⊙_ʖ⊙)_/¯
▸ However…
EXPERIMENTATION BEHIND
DM2 HAS BEEN FRUITFUL!
RESULTS OF THE DATAMAPPER 2 EXPERIMENT
▸ Immutability-oriented object design!
▸ Focus on simpler, foundational abstractions
▸ Embracing small, composable libraries
▸ Lots of interesting new patterns in Ruby
ROM-RB
2013->2014
ROM-RB 2013->2014
▸ DataMapper 2 renamed to rom-rb (Ruby Object Mapper)
in 2013
▸ More work has been put into its relational algebra
backend and optimizing sql queries
▸ Prototype of Session with Unit of Work
▸ Still not production ready ¯_(⊙_ʖ⊙)_/¯
( ಠ ಠ)
“ORM is one of the most complex things that you can ever touch…and we
chose it over and over again, without thinking at all, because everybody is
doing it”
Rich Hickey
ROM-RB
2014+
ROM-RB
PERSISTENCE & MAPPING
TOOLKIT FOR RUBY
NO COMPLEX
& LEAKY ABSTRACTIONS
REDUCES GLOBAL
STATE TO THE MINIMUM
FIGHTS WITH
MUTABLE STATE
MIXES FP WITH OO
CORE CONCEPTS
▸ Configuration
▸ Relations
▸ Pipelines
▸ Commands
CONFIGURATION
require 'rom'
config = ROM::Configuration.new(
:sql, 'postgres://localhost/rom_repository'
)
rom = ROM.container(config)
rom.gateways[:default]
# #<ROM::SQL::Gateway:0x007fde7a087158>
RELATIONS
class Users < ROM::Relation[:sql]
def sorted
order(:name)
end
end
JUST DATA
QUERY LOGIC
rom.relations[:users].sorted
# [{:id=>1, :name=>"Jane"}
RELATIONS ARE CALLABLE
rom.relations[:users].call
# [{:id=>1, :name=>"Jane"}]
rom.relations[:users].where(name: 'Jane').call
# [{:id=>1, :name=>"Jane"}]
rom.relations[:users].where(name: 'Joe').call
# []
PIPELINE
map_names = -> users { users.map { |user| user[:name] } }
user_names = rom.relations[:users] >> map_names
user_names.call
# ["Jane"]
PIPELINE OPERATOR
COMMANDS
class CreateUser < ROM::Commands::Create[:sql]
relation :users
register_as :create
result :one
end
create_user = rom.commands[:users][:create]
create_user.call(name: 'Joe')
# [{:id=>2, :name=>"Joe"}
ROM-REPOSITORY
class UserRepo < ROM::Repository
relations :users
def all
users.select(:name).to_a
end
end
user_repo = UserRepo.new(rom)
user_repo.all
# [#<ROM::Struct name="Jane">, #<ROM::Struct name="Joe">]
UPCOMING
COMMAND SUPPORT
class UserRepo < ROM::Repository[:users]
commands :create, update: :by_id
end
user = user_repo.create(name: "Jane", email: "jane@doe.org")
user_repo.update(user.id, name: "Jane Doe")
ROM 2.0 IS AROUND
THE CORNER!!!
ROM 2.0 + ROM-SQL + ROM-REPOSITORY UPDATES
▸ type-safe and flexible relation schema DSL
▸ association support in schemas
▸ simplified, automatic eager-loading based on association
definitions
▸ simplified interface for returning aggregate objects in
repositories
▸ support for persisting nested data into multiple relations
DRY-RB
‣Low level building blocks
‣High-level abstractions
DRY-CONTAINER
LIGHTWEIGHT IOC
require 'dry-container'
class MyContainer
extend Dry::Container::Mixin
end
# register a singleton
MyContainer.register(:user_repo, UserRepo.new)
# or resolve as a new object
MyContainer.register(:user_repo) { UserRepo.new }
# simply access your registered objects
MyContainer[:user_repo]
class PersistUser
attr_reader :user_repo
def initialize(user_repo)
@user_repo = user_repo
end
def call(user)
user_repo.create(user)
end
end
PersistUser.new(MyContainer[:user_repo])
MyContainer.register(:persist_user) do
PersistUser.new(MyContainer[:user_repo])
end
MyContainer[:persist_user].call(user_data)
DRY-AUTO_INJECT
CONSTRUCTOR
INJECTION MIXIN
require 'dry-auto_inject'
Inject = Dry::AutoInject(MyContainer)
class PersistUser
include Inject[:user_repo]
def call(user)
user_repo.create(user)
end
end
MyContainer.register(:persist_user) do
PersistUser.new
end
DRY-COMPONENT
MANAGING STATE
require 'dry/component/container'
class Application < Dry::Component::Container
configure do |config|
config.root = Pathname.new('./my/app')
end
end
# now you can register a logger
require 'logger'
Application.register('utils.logger', Logger.new($stdout))
# and access it
Application['utils.logger']
AUTO-
REGISTRATION
require 'dry/component/container'
class Application < Dry::Component::Container
configure do |config|
config.root = '/my/app'
config.auto_register = 'lib'
end
load_paths!('lib')
end
# under /my/app/lib/logger.rb we put
class Logger
# some neat logger implementation
end
# we can finalize the container which triggers auto-registration
Application.finalize!
# the logger becomes available
Application[‘logger']
BOOTING
3RD-PARTY DEPS
Application.finalize(:persistence) do
require '3rd-party/database'
container.register('database') do
# some code which initializes this thing
end
end
class Application < Dry::Component::Container
configure do |config|
config.root = '/my/app'
end
end
# requires needed files and boots your persistence component
Application.boot!(:persistence)
# and now `database` becomes available
Application['database']
DRY-TYPES
A TYPE SYSTEM FOR RUBY
WITH COERCIONS AND
CONSTRAINTS
DRY-TYPES TURNS
RUBY INTO HASKELL
NOT REALLY :)
BUT TYPE-SAFETY
MATTERS
require 'dry-types'
module Types
include Dry::Types.module
end
Types::Coercible::String[1] # => "1"
Types::Form::Nil[""] # => nil
Types::Form::Float["1.23"] # 1.23
NilOrString = Types::Strict::Nil | Types::Strict::String
NilOrString[nil] # nil
NilOrString["foo"] # "foo"
DefaultAge = Types::Strict::Int.default(33)
DefaultAge[nil] # 33
Default[35] # 35
DefaultAge["35"]
# Dry::Types::ConstraintError: "35" violates constraints (type?
(Integer) failed)
UserAge = Types::Strict::Int.constrained(gt: 18)
UserAge[19]
# 19
UserAge[18]
# Dry::Types::ConstraintError: 17 violates constraints (gt?(18)
failed)
class User < Dry::Types::Struct
attribute :id, Types::Strict::Int
attribute :name, Types::Strict::String
end
User.new(id: 1, name: "Jane")
0.8.0 IS AROUND
THE CORNER
DRY-VALIDATION
FAST, POWERFUL
VALIDATION WITH TYPE-
SAFETY & SAFE COERCIONS
VALIDATION
SCHEMAS
require 'dry-validation'
UserSchema = Dry::Validation.Schema do
required(:login).filled(:str?, size?: 2..64)
required(:age).maybe(:int?, gt?: 18)
end
UserSchema.(login: "j", age: 19).messages
# {:login=>["length must be within 2 - 64"]}
UserSchema.(login: "jane", age: nil).messages
# {}
SCHEMAS WITH
COERCIONS
require 'dry-validation'
UserSchema = Dry::Validation.Form do
required(:login).filled(:str?, size?: 2..64)
required(:age).maybe(:int?, gt?: 18)
end
UserSchema.(login: "jane", age: '19').to_h
# {:login=>"jane", :age=>19}
UserSchema.(login: "jane", age: '').to_h
# {:login=>"jane", :age=>nil}
UserSchema.(login: "j", age: '19').messages
# {:login=>["length must be within 2 - 64"]}
UserSchema.(login: "j", age: 'oops').messages
# {:age=>["must be an integer", "must be greater than 18"]}
UserSchema.(login: "jane", age: '').messages
# {}
UserSchema.(login: "j", age: 'oops').messages
# {:age=>["must be an integer", "must be greater than 18"]}
VALIDATION HINT
VALIDATION ERROR
0.8.0 IS AROUND
THE CORNER
OMG MORE DRY-RB GEMS!
▸ dry-transaction
▸ dry-monads
▸ dry-initializer
▸ dry-web
▸ dry-web-roda (we plan to add support for more!)
▸ more gems are planned!
@rom_rb
rom-rb.org
dry-rb.org
@dry_rb
@_solnic_
solnic.eu
Piotr Solnica
dry-rb.org/resources/reddotrubyconf-2016/
More resources!
THANK YOU :)

"Meet rom_rb & dry_rb" by Piotr Solnica