Disposable – The Missing API of ActiveRecord

Disposable gives you Twins. Twins are non-persistent domain objects. They know nothing about persisting things, hence the gem name.

They

  • Allow me to model object graphs that reflect my domain without restricting me to the database schema.
  • Let me work on that object graph without writing to the database. Only when syncing the graph writes to its persistent model(s).
  • Provide a declarative DSL to define schemas, schemas that can be used for other data transformations, e.g. in representers or form objects.

Some of its logic and concepts might be overlapping with the excellent ROM project. I am totally open to using ROM in future and continuously having late-night/early-morning debates with Piotr Solnica about our work.

However, I needed the functionality of twins in Reform, Roar, Representable, and Trailblazer now, and most of the concepts have evolved from the Reform gem and got extracted into Disposable.

Agnostic Front.

The title of this post is misleading on purpose: First, I know that many people will read this post because it has an offending title.

Second, it mentions ActiveRecord in a negative context even though I actually love ActiveRecord as a persistence layer (and only that).

Third, Disposable doesn’t really care about ActiveRecord. The underlying models could be from any ORM or just plain Ruby objects.

Twins

Twins are classes that declare a data schema.

class AlbumTwin < Disposable::Twin
  property :title
end

Their API is ridiculously simple. They allow reading, writing, syncing, and optional saving, and that’s it.

When initializing, properties are read from the model.

album = Album.find(1)
twin  = AlbumTwin.new(album)

Reading and writing now works on the twin. The persistence layer is not touched anymore.

# twin read
twin.title #=> "TODO: add title"
# twin write
twin.title = "Run For Cover"

# model read
album.title #=> "TODO: add title"
twin.title  #=> "Run For Cover"

Once you’re done with your work, use sync to write state back to the model.

twin.sync

album.title #=> "Run For Cover"

Optionally, you can call twin.save which invokes save on all nested models. This, of course, implies your models expose a #save method.

Objects, The Way You Want It.

Everything Disposable does could be done with ActiveRecord, in a more awkward way, though.

For example, Disposable lets you do compositions really easily – a concept well approved in Reform.

class AlbumTwin < Disposable::Twin
  include Composition

  property :id,      on: :album
  property :title,   on: :album
  collection :songs, on: :cd do
    property :name
  end
  property :cd_id,   on: :cd, from: :id
end

You configure which properties you want to expose and where they come from. And: you can also rename properties using :from.

The twin now exposes the new API.

twin = AlbumTwin.new(
  album: Album.find(1),
  cd:    CD.find(2)
)
twin.cd_id #=> 2

Of course, this also lets you write.

twin.songs << Song.create(name: "Thunder Rising")

As the composition user, I do not care or know about where songs comes from or go too.

All operations will be on the twin, only. Nothing is written to the models until you say sync. This is something I am totally missing in ActiveRecord. I will talk about that in a minute.

Hash Fields.

Another pretty amazing mapping tool in Disposable is Struct. This allows you to map hashes to objects.

Let’s assume your Album has a JSON column settings.

class Album  {admin: {read: true, write: true}, user: {destroy: false}}

This is a deeply nested hash, a terrible thing to work with. Let the twin take care of it and get back to real object-oriented programming instead of fiddling with hashes.

class AlbumTwin < Disposable::Twin
  property :settings do
    include Struct
    property :admin do
      include Struct
      property :read
      property :write
    end

    property :user
  end
end

This gives you objects.

twin = AlbumTwin.new(Album.find(1))
twin.settings.admin.read #=> true
twin.settings.user #=> {destroy: false}

You can either map keys to properties (or collections!) or retrieve the real hash.

Writing works likewise.

twin.settings.admin.read = :MAYBE

As always, this is not written to the persistent model until you say sync.

album.settings[:admin][:read] #=> true
twin.settings.admin.read = :MAYBE
twin.sync
album.settings[:admin][:read] #=> :MAYBE

Working with hash structures couldn’t be easier. Note that this also works with Reform, giving you all the form power for hash fields.

class AlbumForm < Reform::Form
  property :settings do
    include Struct
    property :admin do
      include Struct
      property :read
      validates :read, inclusion: [true, false, :MAYBE]
    end

This opens up amazing possibilities to easily work with document databases, too. Remember: Disposable doesn’t care if it’s a hash from ActiveRecord, MongoDB or plain Ruby.

Collection Semantics

One reason I wrote twins is because the way ActiveRecord handles collections is tedious. For instance, the following operation will write to the database, even though I didn’t say so.

song = Song.new
CD.songs = [song]
song.persisted? #=> true

This is a real problem. Say you want to set up an object graph, validate it and then write it to the database. Impossible with ActiveRecord unless you use weird work-arounds like CD.songs.build which is completely counter-intuitive.

song = CD.songs.build
song.persisted? #=> false

I want normal Ruby array methods to behave like normal Ruby array methods. What if I don’t have the CD.songs reference, yet, when I instantiate the Song? Twins simply give you the collection semantics you expect.

song = Song.new
AlbumTwin.songs = [song]

song.persisted? #=> false
album.songs #=> []

The changes will not be written to the database until you call sync.

Deleting works analogue to writing, moving, replacing.

song_twin = twin.songs[0]
twin.songs.delete(song_twin)

twin.sync
album.songs #=> []

You can play with any property as much as you want, the persistence layer won’t be hit until syncing happens.

Change Tracking.

Another feature extremely helpful for post-processing logic as found in callbacks is the state tracking behavior in twins. Field changes will be tracked.

twin.changed?(:title) #=> false
twin.title = "Best Of"
twin.changed?(:title) #=> true

You can also check if a twin has changed, which is the case as soon as one or more properties were modified.

twin.changed? #=> true

This works with nested twins and collections, too.

twin.songs < true
twin.songs[0].changed? #=> false
twin.songs[1].changed? #=> false 

On collections, #added, #deleted and friends help you to monitor what has changed in particular.

twin.songs < []

Several other goodies like persistence tracking help to write full-blown event dispatcher which I’m gonna discuss in a separate blog post. If you’re curious, chapter 8 of the Trailblazer book is about callbacks, change tracking and post-processing.

Twins and Representers.

Representers are Ruby declarations that render and parse documents. Have a look at the Roar gem to learn how they are used. Anyway, twins are the perfect match for representers: while the twin handles data modelling, the representer does the document work.

class Album::Representer < Roar::Decorator
  include Roar::JSON

  property :id
  property :title

  collection :songs, class: Song
    property :name
  end

  link(:self) { album_path(id) }
end

The composition twin could now be used in combination with the representer.

twin = AlbumTwin(album: Album.find(1), cd: CD.new)

Note that the CD is a brand-new, fresh and shiny instance without any songs added to it, yet.

We then use the representer to parse the incoming JSON document into Ruby objects.

json = '{"title": "Run For Cover", songs: [{"name": "Military Man"}]}'
Album::Representer.new(twin).from_json json

This will populate the twin.

twin.songs #=> []

After syncing, the CD will contain songs.

twin.sync

cd.songs #=> []

Roar, Representable and Reform come with mechanisms to optionally find existing records instead of creating new, and so on. The topic of populators is covered in chapter 5 and 7 of the Trailblazer book.

Both twins and representers internally use declarative for managing their schemas. This means you can infer representers from twins, and vice-versa.

class Album::Representer < Roar::Decorator
  include Roar::JSON
  include Schema

  from AlbumTwin

  # add properties.
  link(:self) { album_path(id) }
end

Deserialization is a task that’s poorly covered by Rails. With twins and representers, parsing documents into object graphs becomes object-oriented and predictable. Where there was complex nested hash operations, probably involving gems like Hashie, there’s now clean, encapsulated and manageable objects that parse and populate.

Onwards!

Twins are supported in all my gems and the fundamental approach for data transformations. They are an integral part of Reform 2, where every form is a twin. The form is responsible for validation and deserialization, the twin for data mapping.

Use them, make them faster, better, enjoy the simplicity of intuitive object graphs that reflect your domain, not your database schema, and never forget: Nothing is written to the persistence layer until you call sync!

3 thoughts on “Disposable – The Missing API of ActiveRecord

  1. Is there something wrong with this code?

    “`
    This will populate the twin.

    twin.songs #=> []
    After syncing, the CD will contain songs.

    twin.sync
    cd.songs #=> []
    “`

    Are those arrays supposed to be empty?

    Like

Leave a comment