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. Deep Dive Into ROM
    Piotr Solnica
    RubyNation 2015, Washington DC

    View Slide

  2. @_solnic_
    github.com/solnic
    solnic.eu

    View Slide

  3. DataMapper

    View Slide

  4. Virtus

    View Slide

  5. DataMapper 2.0
    (discontinued)

    View Slide

  6. Ruby Object Mapper

    View Slide

  7. FP + OO + Ruby

    View Slide

  8. 8
    “Immutable” objects

    View Slide

  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)

    View Slide

  10. 10
    “call” interface

    View Slide

  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

    View Slide

  12. 12
    no side-effects!

    View Slide

  13. class AddTo
    attr_reader :value
    def initialize(value)
    @value = value
    end
    def call(other)
    other += value
    end
    end

    View Slide

  14. 14
    No mutable state!
    only collaborator(s)
    and
    optional configuration

    View Slide

  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

    View Slide

  16. executor = -> str, meth { str.send(meth) }
    operations = [:upcase]
    upcaser = StringTransformer.new(executor, operations)
    upcaser.call('hello world') # HELLO WORLD

    View Slide

  17. 17
    Procs
    are
    awesome

    View Slide

  18. add_to_one.call(2) # => 3
    add = proc { |i, j| i + j }
    add_to_one = add.curry.call(1)

    View Slide

  19. 19
    Common ways
    to call things

    View Slide

  20. add.(1, 2)
    add = proc { |i, j| i + j }
    add.call(1, 2)
    add[1, 2]

    View Slide

  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?

    View Slide

  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 `.()` -

    View Slide

  23. R . O . M

    View Slide

  24. Gentle Introduction

    View Slide

  25. Data Mapping and Persistence Toolkit
    Data Adapter Relation Mapper Domain

    View Slide

  26. Data Mapping and Persistence Toolkit
    Command Input
    Data Relation
    Adapter

    View Slide

  27. Relation Mapper Command
    Persistence Layer

    View Slide

  28. The ecosystem

    View Slide

  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

    View Slide

  30. Core interface

    View Slide

  31. #call
    #each

    View Slide

  32. ROM explained
    as 3 procs

    View Slide

  33. users_by_name[all_users, 'Jane']
    # [#]
    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 }
    }

    View Slide

  34. Relation

    View Slide

  35. 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')
    # #"Jane", :email=>"[email protected]"}]>

    View Slide

  36. 36
    Mapper

    View Slide

  37. mapper.call(dataset)
    # [
    # #,
    # #
    # ]
    class UserMapper < ROM::Mapper
    model User
    attribute :name
    attribute :email
    end
    mapper = UserMapper.build

    View Slide

  38. 38
    Command

    View Slide

  39. create_user.call([{ name: 'Jane', email: '[email protected]' }])
    users
    # #"Jane", :email=>"[email protected]"}]>
    class CreateUser < ROM::Commands::Create
    def execute(tuples)
    tuples.each { |tuple| relation << tuple }
    end
    end
    users = Users.new([])
    # #
    create_user = CreateUser.build(users)

    View Slide

  40. 40
    Data Pipeline

    View Slide

  41. mapper = UserMapper.build
    users = Users.new(dataset).to_lazy
    mapped_users = users.by_name('Jane') >> mapper
    mapped_users.call.to_a
    # [#]
    Wraps relation in
    a lazy-loading proxy
    The data pipeline
    operator

    View Slide

  42. 42
    Works with both
    Relations and Commands

    View Slide

  43. `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
    # [#]

    View Slide

  44. 44
    Piping through relations

    View Slide

  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

    View Slide

  46. 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' }
    ]

    View Slide

  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

    View Slide

  48. 48
    Combining relations

    View Slide

  49. jane_with_tasks.one
    # #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

    View Slide

  50. 50
    Surface of ROM

    View Slide

  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
    # ## @source=## "SELECT * FROM "users" WHERE ("name" = ‘Jane')">>,
    # @collection=[
    # {:id=>1, :name=>”Jane",
    # :email=>”[email protected]"}]>

    View Slide

  52. user_entities.to_a
    # [#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)

    View Slide

  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: '[email protected]')
    # {:id=>1, :name=>"Jane", :email=>"[email protected]"}

    View Slide

  54. Why it’s not an ORM?

    View Slide

  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

    View Slide

  56. Try it out (:

    View Slide

  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

    View Slide

  58. Thank you <3<3<3

    View Slide

  59. Resources
    • rom-rb.org
    • github.com/rom-rb/rom
    • github.com/solnic/transproc

    View Slide