Slide 1

Slide 1 text

Deep Dive Into ROM Piotr Solnica RubyNation 2015, Washington DC

Slide 2

Slide 2 text

@_solnic_ github.com/solnic solnic.eu

Slide 3

Slide 3 text

DataMapper

Slide 4

Slide 4 text

Virtus

Slide 5

Slide 5 text

DataMapper 2.0 (discontinued)

Slide 6

Slide 6 text

Ruby Object Mapper

Slide 7

Slide 7 text

FP + OO + Ruby

Slide 8

Slide 8 text

8 “Immutable” objects

Slide 9

Slide 9 text

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)

Slide 10

Slide 10 text

10 “call” interface

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

12 no side-effects!

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

17 Procs are awesome

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

19 Common ways to call things

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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?

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

R . O . M

Slide 24

Slide 24 text

Gentle Introduction

Slide 25

Slide 25 text

Data Mapping and Persistence Toolkit Data Adapter Relation Mapper Domain

Slide 26

Slide 26 text

Data Mapping and Persistence Toolkit Command Input Data Relation Adapter

Slide 27

Slide 27 text

Relation Mapper Command Persistence Layer

Slide 28

Slide 28 text

The ecosystem

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

Core interface

Slide 31

Slide 31 text

#call #each

Slide 32

Slide 32 text

ROM explained as 3 procs

Slide 33

Slide 33 text

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 } }

Slide 34

Slide 34 text

Relation

Slide 35

Slide 35 text

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]"}]>

Slide 36

Slide 36 text

36 Mapper

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

38 Command

Slide 39

Slide 39 text

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)

Slide 40

Slide 40 text

40 Data Pipeline

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

42 Works with both Relations and Commands

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

44 Piping through relations

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

48 Combining relations

Slide 49

Slide 49 text

jane_with_tasks.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

Slide 50

Slide 50 text

50 Surface of ROM

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

user_entities.to_a # [#

Slide 53

Slide 53 text

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]"}

Slide 54

Slide 54 text

Why it’s not an ORM?

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

Try it out (:

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

Thank you <3<3<3

Slide 59

Slide 59 text

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