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 full-size slide

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

    View full-size slide

  3. DataMapper 2.0
    (discontinued)

    View full-size slide

  4. Ruby Object Mapper

    View full-size slide

  5. FP + OO + Ruby

    View full-size slide

  6. 8
    “Immutable” objects

    View full-size slide

  7. 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 full-size slide

  8. 10
    “call” interface

    View full-size slide

  9. 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 full-size slide

  10. 12
    no side-effects!

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  13. 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 full-size slide

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

    View full-size slide

  15. 17
    Procs
    are
    awesome

    View full-size slide

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

    View full-size slide

  17. 19
    Common ways
    to call things

    View full-size slide

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

    View full-size slide

  19. 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 full-size slide

  20. 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 full-size slide

  21. Gentle Introduction

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  24. Relation Mapper Command
    Persistence Layer

    View full-size slide

  25. The ecosystem

    View full-size slide

  26. 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 full-size slide

  27. Core interface

    View full-size slide

  28. ROM explained
    as 3 procs

    View full-size slide

  29. 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 full-size slide

  30. 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 full-size slide

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

    View full-size slide

  32. 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 full-size slide

  33. 40
    Data Pipeline

    View full-size slide

  34. 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 full-size slide

  35. 42
    Works with both
    Relations and Commands

    View full-size slide

  36. `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 full-size slide

  37. 44
    Piping through relations

    View full-size slide

  38. 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 full-size slide

  39. 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 full-size slide

  40. 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 full-size slide

  41. 48
    Combining relations

    View full-size slide

  42. 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 full-size slide

  43. 50
    Surface of ROM

    View full-size slide

  44. 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 full-size slide

  45. 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 full-size slide

  46. 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 full-size slide

  47. Why it’s not an ORM?

    View full-size slide

  48. 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 full-size slide

  49. Try it out (:

    View full-size slide

  50. 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 full-size slide

  51. Thank you <3<3<3

    View full-size slide

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

    View full-size slide