"Meet rom_rb & dry_rb" by Piotr Solnica
-
Upload
pivorak-meetup -
Category
Software
-
view
127 -
download
0
Transcript of "Meet rom_rb & dry_rb" by Piotr Solnica
MEET ROM-RB & DRY-RBPIOTR 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) endend
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 :oneend
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 endend
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_idend
user = user_repo.create(name: "Jane", email: "[email protected]")
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::Mixinend
# register a singletonMyContainer.register(:user_repo, UserRepo.new)
# or resolve as a new objectMyContainer.register(:user_repo) { UserRepo.new }
# simply access your registered objectsMyContainer[:user_repo]
class PersistUser attr_reader :user_repo
def initialize(user_repo) @user_repo = user_repo end
def call(user) user_repo.create(user) endend
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) endend
MyContainer.register(:persist_user) do PersistUser.newend
DRY-COMPONENT
MANAGING STATE
require 'dry/component/container'
class Application < Dry::Component::Container configure do |config| config.root = Pathname.new('./my/app') endend
# now you can register a loggerrequire 'logger'Application.register('utils.logger', Logger.new($stdout))
# and access itApplication['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 putclass Logger # some neat logger implementationend
# we can finalize the container which triggers auto-registrationApplication.finalize!
# the logger becomes availableApplication[‘logger']
BOOTING 3RD-PARTY DEPS
Application.finalize(:persistence) do require '3rd-party/database'
container.register('database') do # some code which initializes this thing endend
class Application < Dry::Component::Container configure do |config| config.root = '/my/app' endend
# requires needed files and boots your persistence componentApplication.boot!(:persistence)
# and now `database` becomes availableApplication['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.moduleend
Types::Coercible::String[1] # => "1"
Types::Form::Nil[""] # => nilTypes::Form::Float["1.23"] # 1.23
NilOrString = Types::Strict::Nil | Types::Strict::String
NilOrString[nil] # nilNilOrString["foo"] # "foo"
DefaultAge = Types::Strict::Int.default(33)
DefaultAge[nil] # 33Default[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::Stringend
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!
dry-rb.org/resources/reddotrubyconf-2016/
More resources!
THANK YOU :)