Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Deep Dive Into ROM

Deep Dive Into ROM

Ruby Object Mapper (ROM) started as an attempt of implementing a Data Mapper ORM for Ruby but ended up as an anti-ORM data mapping and persistence toolkit. Why on Earth did that happen? Why after over 4 years of maintaining, building and using Ruby ORMs I decided to change the direction of this project? In this talk I will give you a great insight into why ROM is not an ORM, why it's built using less conventional techniques, why it adheres to the CQRS pattern and why you may want to use it. I'll also tell you what's wrong with OOP and why I love "immutable" objects.

Piotr Solnica

June 13, 2015
Tweet

More Decks by Piotr Solnica

Other Decks in Programming

Transcript

  1. class Thing attr_reader :value def initialize(value) @value = value end

    def with(value) self.class.new(value) end end thing = Thing.new(1) other = thing.with(2)
  2. class AddTo attr_reader :value def initialize(value) @value = value end

    def call(other) other + value end end add_to = AddTo.new(1) add_to.call(1) # => 2 add_to.call(2) # => 3
  3. class StringTransformer attr_reader :executor, :operations def initialize(executor, operations) @executor =

    executor @operations = operations end def call(input) operations.reduce(input) { |a, e| executor.call(a, e) } end end
  4. executor = -> str, meth { str.send(meth) } operations =

    [:upcase] upcaser = StringTransformer.new(executor, operations) upcaser.call('hello world') # HELLO WORLD
  5. add_to_one.call(2) # => 3 add = proc { |i, j|

    i + j } add_to_one = add.curry.call(1)
  6. add.(1, 2) add = proc { |i, j| i +

    j } add.call(1, 2) add[1, 2]
  7. arr = [:a, :b] arr[0] # less weird, right? add

    = proc { |i, j| i + j } add[1, 2] # WEIRD? hash = { a: 1 } hash[:a] # less weird, right?
  8. Let’s summarize • No mutability - objects don’t change •

    No state - just collaborators and maybe some configuration • Consistent interface - just `call` • No side-effects - calling an object won’t mutate anything • Currying is neat, it really is! • Short syntax with `[]` or `.()` -
  9. The ecosystem • SQL support via rom-sql adapter backed by

    Sequel • Adapters for various databases like RethinkDB, Neo4J, InfluxDB, CouchDB • “Yesql” adapter for plain SQL queries • Adapters for “file-based” sources like YAML, CSV or Git • Framework integrations: • lotus • rails • roda
  10. users_by_name[all_users, 'Jane'] # [#<User name="Jane", email="[email protected]">] User = Class.new(OpenStruct) map_to_users

    = proc { |arr, name| arr.map { |el| User.new(el) } } all_users = [ { name: 'Jane', email: '[email protected]' }, { name: 'John', email: '[email protected]' } ] users_by_name = proc { |arr, name| map_to_users[find_by_name[all_users, name]] } find_by_name = proc { |arr, name| arr.select { |el| el[:name] == name } }
  11. class Users < ROM::Relation forward :select, :<< def by_name(name) select

    { |user| user[:name] == name } end end dataset = [ { name: 'Jane', email: '[email protected]' }, { name: 'John', email: '[email protected]' } ] users = Users.new(dataset) users.by_name('Jane') # #<Users dataset=[{:name=>"Jane", :email=>"[email protected]"}]>
  12. mapper.call(dataset) # [ # #<User name="Jane", email=“[email protected]">, # #<User name="John",

    email=“[email protected]"> # ] class UserMapper < ROM::Mapper model User attribute :name attribute :email end mapper = UserMapper.build
  13. create_user.call([{ name: 'Jane', email: '[email protected]' }]) users # #<Users dataset=[{:name=>"Jane",

    :email=>"[email protected]"}]> class CreateUser < ROM::Commands::Create def execute(tuples) tuples.each { |tuple| relation << tuple } end end users = Users.new([]) # #<Users dataset=[]> create_user = CreateUser.build(users)
  14. mapper = UserMapper.build users = Users.new(dataset).to_lazy mapped_users = users.by_name('Jane') >>

    mapper mapped_users.call.to_a # [#<User name="Jane", email="[email protected]">] Wraps relation in a lazy-loading proxy The data pipeline operator
  15. `with` curries the command create_mapped_users = create_user .with([{ name: 'Jade',

    email: '[email protected]' }]) >> mapper `>>` sends results through the mapper create_user = CreateUser.build(users) create_mapped_users.call # [#<User name="Jade", email="[email protected]">]
  16. class Users < ROM::Relation forward :select def by_name(name) select {

    |user| user[:name] == name } end end class Tasks < ROM::Relation forward :select def for_users(users) user_names = users.map { |user| user[:name] } select { |task| user_names.include?(task[:user]) } end end
  17. users = Users.new(user_dataset).to_lazy tasks = Tasks.new(task_dataset).to_lazy user_dataset = [ {

    name: 'Jane', email: '[email protected]' }, { name: 'John', email: '[email protected]' } ] task_dataset = [ { user: 'Jane', title: 'Task One' }, { user: 'John', email: 'Task Two' } ]
  18. user_tasks.call(‘Jane').to_a # [{:user=>"Jane", :title=>"Task One"}] auto-curried `by_name` relation sends its

    data to auto-curried `for_users` relation call it later on to get all tasks for users with the given name user_tasks = users.by_name >> tasks.for_users
  19. jane_with_tasks.one # #<User name="Jane", email="[email protected]", tasks=[#<Task user="Jane", title="Task One">]> class

    UserMapper < ROM::Mapper model User combine :tasks, on: { name: :user } do model Task end end users = Users.new(user_dataset).to_lazy tasks = Tasks.new(task_dataset).to_lazy mapper = UserMapper.build jane_with_tasks = users .by_name('Jane') .combine(tasks.for_users) >> mapper
  20. users = rom.relation(:users) ROM.setup(:sql, 'postgres://localhost/rom') class Users < ROM::Relation[:sql] def

    by_name(name) where(name: name) end end rom = ROM.finalize.env users.by_name('Jane').call # #<ROM::Relation::Loaded:0x007fc01bba25b0 # @source=#<Users dataset=#<Sequel::Postgres::Dataset: # "SELECT * FROM "users" WHERE ("name" = ‘Jane')">>, # @collection=[ # {:id=>1, :name=>”Jane", # :email=>”[email protected]"}]>
  21. user_entities.to_a # [#<User id=6, name="Jane", email="[email protected]"] ROM.setup(:sql, 'postgres://localhost/rom') class UserMapper

    < ROM::Mapper relation :users register_as :entity model User end rom = ROM.finalize.env user_entities = rom.relation(:users).map_with(:entity)
  22. create_user = rom.command(:users).create ROM.setup(:sql, 'postgres://localhost/rom') class CreateUser < ROM::Commands::Create[:sql] relation

    :users result :one register_as :create end rom = ROM.finalize.env create_user.call(name: 'Jane', email: '[email protected]') # {:id=>1, :name=>"Jane", :email=>"[email protected]"}
  23. Why not an ORM? • Data-centric, mapping and persistence toolkit

    • No concept of object mutation and syncing with the database • No abstract query interfaces • Functional components • OO interface on the surface
  24. Try it out! • Build your own persistence layer •

    Decouple application layer from persistence • Build data import/export tools • Build advanced data mapping tools • Write custom adapters for any data source out there