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.

E864e5088627498df8f9b911a9bc3219?s=128

Piotr Solnica

June 13, 2015
Tweet

Transcript

  1. 4.
  2. 9.

    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)
  3. 11.

    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
  4. 15.

    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
  5. 16.

    executor = -> str, meth { str.send(meth) } operations =

    [:upcase] upcaser = StringTransformer.new(executor, operations) upcaser.call('hello world') # HELLO WORLD
  6. 18.

    add_to_one.call(2) # => 3 add = proc { |i, j|

    i + j } add_to_one = add.curry.call(1)
  7. 20.

    add.(1, 2) add = proc { |i, j| i +

    j } add.call(1, 2) add[1, 2]
  8. 21.

    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?
  9. 22.

    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 `.()` -
  10. 23.
  11. 29.

    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
  12. 33.

    users_by_name[all_users, 'Jane'] # [#<User name="Jane", email="jane@doe.org">] User = Class.new(OpenStruct) map_to_users

    = proc { |arr, name| arr.map { |el| User.new(el) } } all_users = [ { name: 'Jane', email: 'jane@doe.org' }, { name: 'John', email: 'john@doe.org' } ] 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 } }
  13. 34.
  14. 35.

    class Users < ROM::Relation forward :select, :<< def by_name(name) select

    { |user| user[:name] == name } end end dataset = [ { name: 'Jane', email: 'jane@doe.org' }, { name: 'John', email: 'john@doe.org' } ] users = Users.new(dataset) users.by_name('Jane') # #<Users dataset=[{:name=>"Jane", :email=>"jane@doe.org"}]>
  15. 36.
  16. 37.

    mapper.call(dataset) # [ # #<User name="Jane", email=“jane@doe.org">, # #<User name="John",

    email=“john@doe.org"> # ] class UserMapper < ROM::Mapper model User attribute :name attribute :email end mapper = UserMapper.build
  17. 39.

    create_user.call([{ name: 'Jane', email: 'jane@doe.org' }]) users # #<Users dataset=[{:name=>"Jane",

    :email=>"jane@doe.org"}]> 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)
  18. 41.

    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="jane@doe.org">] Wraps relation in a lazy-loading proxy The data pipeline operator
  19. 43.

    `with` curries the command create_mapped_users = create_user .with([{ name: 'Jade',

    email: 'jade@doe.org' }]) >> mapper `>>` sends results through the mapper create_user = CreateUser.build(users) create_mapped_users.call # [#<User name="Jade", email="jade@doe.org">]
  20. 45.

    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
  21. 46.

    users = Users.new(user_dataset).to_lazy tasks = Tasks.new(task_dataset).to_lazy user_dataset = [ {

    name: 'Jane', email: 'jane@doe.org' }, { name: 'John', email: 'john@doe.org' } ] task_dataset = [ { user: 'Jane', title: 'Task One' }, { user: 'John', email: 'Task Two' } ]
  22. 47.

    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
  23. 49.

    jane_with_tasks.one # #<User name="Jane", email="jane@doe.org", 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
  24. 51.

    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=>”jane@doe.org"}]>
  25. 52.

    user_entities.to_a # [#<User id=6, name="Jane", email="jane@doe.org"] 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)
  26. 53.

    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: 'jane@doe.org') # {:id=>1, :name=>"Jane", :email=>"jane@doe.org"}
  27. 55.

    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
  28. 57.

    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