21 Nov

Active Poro: :has_one and :has_many associations for your POROs

Last update on 2014-11-21

A magical feature of database-backed models is the way objects are linked to one another through associations, setting bidirectional assignments between them.

Let's have a quick recap on how ActiveRecord associations work. These associations let you specify something like.

class Driver < ActiveRecord::Base
  has_one :car
end

class Car < ActiveRecord::Base
  belongs_to :driver
end

If you assign the driver to the car and save, you will magically have the objects linked to one another, like this:

driver = Driver.first
car = Car.first

# Assign the driver to the car and save
car.driver = driver
car.save

# Check the bidirectional assignment
car.driver #=> the driver
driver.car #=> the car

This is pretty much taken for granted when dealing with models in Rails for example, but let's look at what we have to do when working with simple ruby classes and objects - POROs (Plain Old Ruby Objects).

# Create some simple classes with accessors for each other
class Driver
  attr_accessor :car
end

class Car
  attr_accessor :driver
end

# Create two simple objects
driver = Driver.new
car = Car.new

# Assign a driver to the car
car.driver = driver

# Check the accessors
car.driver #=> the driver
driver.car #=> nil ... nope, not magically set

If we want for both objects to be 'associated', we must set each to one another explicitly, like this:

# Assign a driver to the car AND viceversa
car.driver = driver
driver.car = car

Typing that extra line of code seems somewhat annoying when we are actually trying to associate the two objects, and it would be even worse if we were talking about collections of objects.

Introducing ActivePoro

How about writing the following piece of ruby code?

class Driver
  include SomeMagicalModule
  has_one :car
end

class Car
  include SomeMagicalModule
  belongs_to :driver
end

With the active_poro gem, you can have exactly this. There are already many gems that do a great job helping with model-like functionality, however, it wasn't clear how would associations could work for POROs.

If you want to quickly try this out, check out the following:

Install the gem

gem install active_poro

and now you can do something like

require 'active_poro'

class Driver
  include ActivePoro::Model
  has_one :car
end

class Car
  include ActivePoro::Model
  belongs_to :driver
end

Not only that, but you can also set a has_many association too!

require 'active_poro'

class Driver
  include ActivePoro::Model
  has_many :cars
end

class Car
  include ActivePoro::Model
  belongs_to :driver
end

When this is set, any objects that you include ActivePoro::Model on will find the reflected association and complete the bidirectional assignment for you. In fact, you only need to include the ActivePoro::Associations module, since its the only thing active_poro provides by now.

From the example above, active_poro creates for you some convenience methods for adding and removing associated objects when using a has_many association.

driver = Driver.new
car_A = Car.new
car_B = Car.new

driver.cars = [car_A]

driver.add_car car_B
driver.remove_car car_A

At the time of this writing, we are not proxying the collection so the following would not update the association on both sides

driver.cars << car_C

However we could implement an invisible proxy to allow these kind of behaviour.

If you find this useful, be sure to check out the README and source on github. We are more than happy to receive your comments below!

Cheers!

comments powered by Disqus