rom-rb 4.0 - Moscow, RailsClub 2017

E864e5088627498df8f9b911a9bc3219?s=47 Piotr Solnica
September 23, 2017

rom-rb 4.0 - Moscow, RailsClub 2017

E864e5088627498df8f9b911a9bc3219?s=128

Piotr Solnica

September 23, 2017
Tweet

Transcript

  1. rom-rb 4.0 Piotr Solnica >> Moscow >> RailsClub 2017

  2. Piotr Solnica → ! Cracow, Poland → github.com/solnic → @_solnic_

    → solnic.eu
  3. Tech Lead at Icelab

  4. Tech Lead at Icelab

  5. rom-rb 4.0 is coming rom-rb 4.0 is here

  6. sort of

  7. 4.0.0.rc1 was released a week ago

  8. What's rom-rb?

  9. an open-source persistence and mapping toolkit for Ruby built for

    speed and simplicity
  10. Database-agnostic

  11. Flexible

  12. Extendible

  13. Fast

  14. Simple

  15. Sounds like some crazy object relational mapper, eh?

  16. rom-rb is not an ORM

  17. Typical ORM ! that we DO NOT HAVE → Relying

    on mutable objects → Complex association model → Leaky abstractions → [ActiveRecord] tight coupling with database schema → [ActiveRecord] n+1 query problem → [ActiveRecord] callbacks → [ActiveRecord] huge API
  18. Why does it even exist?

  19. A TRUE ALTERNATIVE to Active Record

  20. rom-rb provides a way to separate persistence concerns from application

    domain
  21. What's the deal with rom-rb 4.0? (aka: serious business)

  22. Big, BIG, challenge

  23. Make rom-rb simple to use (comparable with Active Record)

  24. What's so awesome about Active Record?

  25. No boilerplate class User < ActiveRecord::Base end

  26. Persistence, so simple user = User.create(name: "Jane")

  27. Queries, so simple User.where(name: "Jane")

  28. Making changes, so simple User.where(name: "Jane").update_all(name: "Jane Doe")

  29. Ease of use → Little code to write to get

    started → A lot of functionality OOTB → No boilerplate
  30. This was a real challenge for rom-rb!

  31. Dynamic query interface not tied to the database schema

  32. Explicit representation of data structures returned by relations

  33. Mapping to struct objects decoupled from the database

  34. No concept of lazy-loadable associations

  35. This is fine

  36. We made it (⊃ꙏ•́‿•̀ ꙏ)⊃━☆ꚍ.*ꙓꙏꚍ

  37. Relations and Structs

  38. class Users < ROM::Relation[:sql] schema(infer: true) end

  39. ✅ No boilerplate

  40. users.changeset(:create, name: "Jane").commit # => #<ROM::Struct::User id=1 name="Jane">

  41. ✅ Persistence, so simple

  42. users.where(name: "Jane").one # => #<ROM::Struct::User id=1 name="Jane">

  43. ✅ Queries, so simple

  44. users.where(name: "Jane").changeset(:update, name: "Jane Doe").commit

  45. ✅ Making changes, so simple

  46. BUT...

  47. users.first # => #<ROM::Struct::User id=1 name="Jane"> users.select(:name).first # => #<ROM::Struct::User

    name="Jane">
  48. users.first.class.schema.keys # [:id, :name] users.select(:name).first.class.schema.keys # [:name]

  49. People want their own methods, obviously

  50. class User < ActiveRecord::Base def first_name name.split(' ').first end def

    last_name name.split(' ').last end end
  51. rom-rb: custom struct namespace

  52. class Users < ROM::Relation[:sql] struct_namespace Entities schema(infer: true) end

  53. class Users < ROM::Relation[:sql] struct_namespace Entities schema(infer: true) end

  54. module Entities class User < ROM::Struct def first_name name.split(' ').first

    end def last_name name.split(' ').last end end end
  55. module Entities class User < ROM::Struct def first_name name.split(' ').first

    end def last_name name.split(' ').last end end end
  56. user = users.first => #<Entities::User id=1 name="Jane Doe"> user.first_name #

    "Jane" user.last_name # "Doe"
  57. user = users.select(:name).first => #<Entities::User name="Jane Doe"> user.first_name # "Jane"

    user.last_name # "Doe"
  58. What about associations?

  59. class Users < ROM::Relation[:sql] schema(infer: true) do associations do has_many

    :tasks end end end
  60. users.combine(:tasks).first # => #<Entities::User id=1 name="Jane" tasks=[ # #<Entities::Task id=2

    user_id=1 title="Do something"> # ]>
  61. module Entities class User < ROM::Struct def has_tasks? !tasks.empty? end

    end end user = users.combine(:tasks).first user.has_tasks? # true
  62. user = users.first user.has_tasks? # ROM::Struct::MissingAttribute: # undefined method `tasks'

    for # #<Entities::User id=1 name="Jane"> (attribute not loaded?)
  63. What does this mean, really?

  64. → Dynamic query interface is maintained → Custom methods are

    maintained → Objects decoupled from the database are maintained too
  65. So, this is awesome

  66. Ability to start simple without even defining any struct classes

  67. Ability to provide your own methods that rely on various

    attributes in various contexts
  68. AR-like convenience without tight coupling with the database

  69. BUT...

  70. This is not enforced by the library!

  71. At any point in time, you can define structs with

    explicit attributes and ask rom-rb to load them
  72. You can establish different models based on various domain concepts

  73. However...this requires time and good understanding of the application domain

  74. It's a process and rom-rb fully supports it

  75. show-off mode: on

  76. 10 things you can do with rom-rb

  77. …that Active Record can't do

  78. 1. Use it with non-SQL databases

  79. class Users < ROM::Relation[:mongo] schema do attribute :_id, Types::ObjectID attribute

    :name, Types::String end end
  80. 2. Define cross-database associations

  81. class Users < ROM::Relation[:sql] schema(infer: true) do associations do has_many

    :tasks, override: true, view: :for_users end end end class Tasks < ROM::Relation[:yaml] gateway :external schema(infer: true) def for_users(users) tasks.restrict(UserId: users.pluck(:id)) end end
  82. class Users < ROM::Relation[:sql] schema(infer: true) do associations do has_many

    :tasks, override: true, view: :for_users end end end class Tasks < ROM::Relation[:yaml] gateway :external schema(infer: true) def for_users(users) tasks.restrict(UserId: users.pluck(:id)) end end
  83. 3. Map to any model

  84. class MyUser < MySuperModelLibrary end users.map_to(MyUser)

  85. 4. Project data easily and get back convenient structs

  86. posts. select(:title). select_append { meta.get_text(:comment_count, :integer).as(:comments) }. first # #<ROM::Struct::Post

    title="Hello World" comments=12>
  87. 5. Easily construct complex conditions

  88. users.where { admin.is(true).or(moderator.is(true)) } users.where { !admin.is(true) } posts.where {

    meta.get_text("comments_count", :integer) > 1 }
  89. 6. Easily use SQL functions

  90. users.select { str::first_name.concat(last_name).as(:full_name) }.first # #<ROM::Struct::User full_name="Jane Doe">

  91. 7. Persist nested data without special model configuration

  92. users. combine(:tasks). changeset(:create, name: "Joe", tasks: [{ title: "Task 1"

    }]). commit # => #<Entities::User id=4 name="Joe" tasks=[ # #<Entities::Task id=3 user_id=4 title="Task 1"> # ]>
  93. 8. Use it with legacy schemas without yelling at your

    screen too much
  94. class Users < ROM::Relation[:sql] schema(:SomehorriblyNamedUseRtable, as: :users) do attribute :UserIdentifier,

    Serial.meta(alias: :id) attribute :UserName, String.meta(alias: :name) end end users.first # #<ROM::Struct::User id=1, name="Jane">
  95. 9. Define custom data mappers

  96. class EncryptionMapper < ROM::Mapper register_as :encryption def call(relation) relation.map {

    |tuple| # do whatever you want } end end users.map_with(:encryption)
  97. 10. Use changesets to transform data before passing it to

    the database
  98. class NewUser < ROM::Changeset::Create map do rename_keys user_name: :name end

    end users.changeset(NewUser, user_name: "Jane").commit # #<ROM::Struct::User id=1 name="Jane">
  99. there’s more

  100. rom 4.0.0.rc1 is out

  101. rom-rb.org @rom_rb

  102. Questions?

  103. Thank you