Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Windy City Rails 2017 - Beyond Active Record

Pan Thomakos
September 15, 2017

Windy City Rails 2017 - Beyond Active Record

https://windycityrails.com/schedule/#beyond-active-record

Active Record models can quickly become bloated messes that combine persistence logic, validations, callbacks, and business logic into god objects. These models slowly become dependencies of almost all other objects in our code base.

Several months ago, Strava confronted this situation in their mature 8 year old Rails application. In this talk, Pan will walk through the new architecture and object conventions that Strava is migrating to as a solution to unwieldy Active Record models. We’ll explore the reasoning behind each design decision so that you can decide for yourself if a similar approach is right for you. And we’ll walk through some of the benefits of this alternative architecture.

Pan Thomakos

September 15, 2017
Tweet

More Decks by Pan Thomakos

Other Decks in Programming

Transcript

  1. @panthomakos The archive of the Danish Broadcasting Corporation / DRs

    Kulturarvsprojekt / Creative Commons @panthomakos persistence
  2. @panthomakos scope :active, -> { where(retired: false) } scope :retired,

    -> { where(retired: true) } scope :default, -> { where(default: true) }
  3. @panthomakos def retire! self.retire = true save! end def frame

    Bikes::Frames[frame_type] end def mountain_bike? Bikes::Frames.mtb?(frame) end ... continues for 200 more lines
  4. @panthomakos A Pair of Ceremonial Objects or Musical Instruments /

    LACMA / Public Domain @panthomakos simple objects
  5. @panthomakos > Strava::Bikes::Bike.new.id = 5 NoMethodError: private method `id=' called

    for #<Strava::Bikes::Bike:0x00006c99871558> Did you mean? id
  6. @panthomakos class EntityReader def initialize(entity, engine = DefaultEngine) @entity =

    entity @table = ::Arel::Table.new(@entity.db_table) @engine = engine end def find(pk) query = @table.where(@table[@entity.db_primary_key].eq(pk)) select(query).first end private def project(query) query.project(@entity.db_columns.map { |c| @table[c] }) end def select(query) @engine.connection.select_all(project(query).to_sql).map do |row| @entity.new(row) end end end
  7. @panthomakos class EntityReader def initialize(entity, engine = DefaultEngine) @entity =

    entity @table = ::Arel::Table.new(@entity.db_table) @engine = engine end def find(pk) query = @table.where(@table[@entity.db_primary_key].eq(pk)) select(query).first end private def project(query) query.project(@entity.db_columns.map { |c| @table[c] }) end def select(query) @engine.connection.select_all(project(query).to_sql).map do |row| @entity.new(row) end end end
  8. @panthomakos class EntityReader def initialize(entity, engine = DefaultEngine) @entity =

    entity @table = ::Arel::Table.new(@entity.db_table) @engine = engine end def find(pk) query = @table.where(@table[@entity.db_primary_key].eq(pk)) select(query).first end private def project(query) query.project(@entity.db_columns.map { |c| @table[c] }) end def select(query) @engine.connection.select_all(project(query).to_sql).map do |row| @entity.new(row) end end end
  9. @panthomakos class EntityReader def initialize(entity, engine = DefaultEngine) @entity =

    entity @table = ::Arel::Table.new(@entity.db_table) @engine = engine end def find(pk) query = @table.where(@table[@entity.db_primary_key].eq(pk)) select(query).first end private def project(query) query.project(@entity.db_columns.map { |c| @table[c] }) end def select(query) @engine.connection.select_all(project(query).to_sql).map do |row| @entity.new(row) end end end
  10. @panthomakos class EntityReader def initialize(entity, engine = DefaultEngine) @entity =

    entity @table = ::Arel::Table.new(@entity.db_table) @engine = engine end def find(pk) query = @table.where(@table[@entity.db_primary_key].eq(pk)) select(query).first end private def project(query) query.project(@entity.db_columns.map { |c| @table[c] }) end def select(query) @engine.connection.select_all(project(query).to_sql).map do |row| @entity.new(row) end end end
  11. @panthomakos class EntityReader def initialize(entity, engine = DefaultEngine) @entity =

    entity @table = ::Arel::Table.new(@entity.db_table) @engine = engine end def find(pk) query = @table.where(@table[@entity.db_primary_key].eq(pk)) select(query).first end private def project(query) query.project(@entity.db_columns.map { |c| @table[c] }) end def select(query) @engine.connection.select_all(project(query).to_sql).map do |row| @entity.new(row) end end end
  12. @panthomakos class EntityReader def initialize(entity, engine = DefaultEngine) @entity =

    entity @table = ::Arel::Table.new(@entity.db_table) @engine = engine end def find(pk) query = @table.where(@table[@entity.db_primary_key].eq(pk)) select(query).first end private def project(query) query.project(@entity.db_columns.map { |c| @table[c] }) end def select(query) @engine.connection.select_all(project(query).to_sql).map do |row| @entity.new(row) end end end
  13. @panthomakos class EntityReader def initialize(entity, engine = DefaultEngine) @entity =

    entity @table = ::Arel::Table.new(@entity.db_table) @engine = engine end def find(pk) query = @table.where(@table[@entity.db_primary_key].eq(pk)) select(query).first end private def project(query) query.project(@entity.db_columns.map { |c| @table[c] }) end def select(query) @engine.connection.select_all(project(query).to_sql).map do |row| @entity.new(row) end end end
  14. @panthomakos class EntityReader def initialize(entity, engine = DefaultEngine) @entity =

    entity @table = ::Arel::Table.new(@entity.db_table) @engine = engine end def find(pk) query = @table.where(@table[@entity.db_primary_key].eq(pk)) select(query).first end private def project(query) query.project(@entity.db_columns.map { |c| @table[c] }) end def select(query) @engine.connection.select_all(project(query).to_sql).map do |row| @entity.new(row) end end end
  15. @panthomakos class Strava::Bikes::Db::Reader < EntityReader def initialize super(Strava::Bikes::Bike) end def

    by_athlete_id(id) select(@table.where(@table[:athlete_id].eq(id))) end end
  16. @panthomakos class Strava::Bikes::Db::Reader < EntityReader def initialize super(Strava::Bikes::Bike) end def

    by_athlete_id(id) select(@table.where(@table[:athlete_id].eq(id))) end end
  17. @panthomakos class Strava::Bikes::Db::Creator < EntityCreator def initialize super(Strava::Bikes::Bike) end def

    create(attrs) t = Time.now attrs = attrs.merge(created_at: t, updated_at: t) super(attrs) end end
  18. @panthomakos class Strava::Bikes::Db::Creator < EntityCreator def initialize super(Strava::Bikes::Bike) end def

    create(attrs) t = Time.now attrs = attrs.merge(created_at: t, updated_at: t) super(attrs) end end
  19. @panthomakos class Strava::Bikes::Db::Creator < EntityCreator def initialize super(Strava::Bikes::Bike) end def

    create(attrs) t = Time.now attrs = attrs.merge(created_at: t, updated_at: t) super(attrs) end end
  20. @panthomakos class Strava::Bikes::Db::Creator < EntityCreator def initialize super(Strava::Bikes::Bike) end def

    create(attrs) t = Time.now attrs = attrs.merge(created_at: t, updated_at: t) super(attrs) end end
  21. @panthomakos class Strava::Bikes::Db::Creator < EntityCreator def initialize super(Strava::Bikes::Bike) end def

    create(attrs) t = Time.now attrs = attrs.merge(created_at: t, updated_at: t) super(attrs) end end
  22. @panthomakos Wall made of hole bricks / Xauxa / Creative

    Commons @panthomakos framework isolation
  23. @panthomakos class Bike < ApplicationRecord def retire! self.retire = true

    save! end def frame Bikes::Frames[frame_type] end def mountain_bike? Bikes::Frames.mtb?(frame) end end
  24. @panthomakos class Bike < ApplicationRecord def retire! self.retire = true

    save! end def frame Bikes::Frames[frame_type] end def mountain_bike? Bikes::Frames.mtb?(frame) end end
  25. @panthomakos class Bike < ApplicationRecord def retire! self.retire = true

    save! end def frame Bikes::Frames[frame_type] end def mountain_bike? Bikes::Frames.mtb?(frame) end end
  26. @panthomakos ::Strava Bikes Actions Creator Reader Updater Destroyer Db Creator

    Reader Updater Destroyer Bike Validator AdminValidator ::EventLogging ::AthleteCache ::BikeFrame
  27. @panthomakos ::Strava Bikes Actions Creator Reader Updater Destroyer Db Creator

    Reader Updater Destroyer Bike Validator AdminValidator ::EventLogging ::AthleteCache ::BikeFrame
  28. @panthomakos ::Strava Bikes Actions Creator Reader Updater Destroyer Db Creator

    Reader Updater Destroyer Bike Validator AdminValidator ::EventLogging ::AthleteCache ::BikeFrame
  29. @panthomakos ::Strava Bikes Actions Creator Reader Updater Destroyer Db Creator

    Reader Updater Destroyer Bike Validator AdminValidator ::EventLogging ::AthleteCache ::BikeFrame
  30. @panthomakos ::Strava Bikes Actions Creator Reader Updater Destroyer Db Creator

    Reader Updater Destroyer Bike Validator AdminValidator ::EventLogging ::AthleteCache ::BikeFrame
  31. @panthomakos ::Strava Bikes Actions Creator Reader Updater Destroyer Db Creator

    Reader Updater Destroyer Bike Validator AdminValidator ::EventLogging ::AthleteCache ::BikeFrame
  32. @panthomakos ::Strava Bikes Actions Creator Reader Updater Destroyer Db Creator

    Reader Updater Destroyer Bike Validator AdminValidator ::EventLogging ::AthleteCache ::BikeFrame
  33. @panthomakos # controller def show @bike = Bike.find(params[:id]) unless BikePermissions.editable?(@bike,

    current_athlete) permission_denied return false end # view @bike.athlete @bike.components
  34. @panthomakos # controller def show @bike = Strava::Bikes::Actions::Reader.find(params[:id]) unless BikePermissions.editable?(@bike,

    current_athlete) permission_denied return false end @athlete = Athlete.find(@bike.athlete_id) @components = Component.where(bike_id: @bike.id).all
  35. @panthomakos # controller def show @bike = Strava::Bikes::Actions::Reader.find(params[:id]) unless BikePermissions.editable?(@bike,

    current_athlete) permission_denied return false end @athlete = Strava::Athletes::Actions::Reader.find(@bike.athlete_id) @components = Strava::Bikes::Actions::Reader.components(@bike.id)