Fun with Object-Relational Mapping
The way object oriented code models data and the relational model we’ve been discussing here are different. With active record we have what is a good compromise. We can use database rows just like they are Ruby objects, as long as we remember to tell them to save back to the database when we’ve finished.
What is Active Record?
Martin Fowler is a well known thinker in the enterprise software space and works for Thoughtworks. Many years ago he wrote the classic book Patterns of Enterprise Application Architecture that looks at how you build enterprise level systems. The book contains many useful patterns for this and Active Record is persented for wrapping and using databases. A good many of Fowler’s patterns ended up in Rails because its originators used his work as an inspiration. He has many other books about things like how to model financial systems and all sorts that are well worth getting hold of if you want to think about enterprise systems more broadly.
Active Record allows you to declaratively model the the relationships between the entities in a system while easily being able to query the attributes of any relation itself without having to write loads of SQL yourself.
Plain old Ruby Objects (PORO)
Let’s think about Customer/Order/OrderLine and just do it in Ruby:
class Customer
attr_accessor :name, :orders
def initialize(name, orders = [])
self.name = name
self.orders = orders
end
end
class Order
attr_accessor :date, :order_lines
def initialize(date, order_lines = [])
self.date = date
self.order_lines = order_lines
end
end
class OrderLine
attr_accessor :product_name, :quantity, :unit_price
def initialize( product_name, quantity, unit_price)
self.product_name = product_name
self.quantity = quantity
self.unit_price = unit_price
end
end
Let’s play with this a little:
irb(main):035> require 'date'
irb(main):036> customer = Customer.new("cust")
irb(main):037> order = Order.new(Date.today)
irb(main):038> customer.orders << order
irb(main):039> order_line = OrderLine.new("Prod 1", 1, 200)
irb(main):040> order.order_lines << order_line
=> [#<OrderLine:0x0000000122f7fe40 @product_name="Prod 1", @quantity=1, @unit_price=200>]
irb(main):041> customer
=>
#<Customer:0x0000000122fd0020
@name="cust",
@orders=[#<Order:0x0000000122f7ff08 @date=#<Date: 2026-08-04 ((2461257j,0s,0n),+0s,2299161j)>, @order_lines=[#<OrderLine:0x0000000122f7fe40 @product_name="Prod 1", @quantity=1, @unit_price=200>]>]>
irb(main):042>
Ignoring the complication of creating and referencing a separate product class because it complicates things.
Looking at the code above and ignoring the obvious need we might have to save and reload the data if using it in anger you can see the differences quite easily:
- The objects don’t have a database ID, so rendering routes like
/customers/1would mean we need to add something like an ID to make building an app possible - There are no queries as such, just elements in arrays or the actual object itself
- Queries would have to be hand written using the methods active record reuses, for example methods like
selectandfindfrom the Hash and Array classes. - We don’t have any of the active record helpers like
belongs_to, or any validation, and have to rely on what we can write ourselves by hand.
ORMs like Active Record allow us to use the enormous power of databases to create applications that can persist and find data while still having our nice, easy to use objects, that work in the web space. We don’t have to write SQL to do the simple stuff. The Ruby Active Record framework is designed to do the 95% simple things easily, and then let you just write some SQL for the stuff that’s hard to express using object notation.
There’s also some niceties like queries using attributes that are nil are automatically set up to use the is null operator when they get to the database.
Some folks argue that AR does too much, people drop business logic in the models where it does not belong and they become bloated and break the single responsibility principle. As usual there are no hard and fast rules. If something starts to hurt your process then think about it, otherwise leave it alone. Using patterns like services to put business processes in is a first step when this becomes too hard. The Orthogonal Architecture splits everything at the expense of being harder to set up initially, if you really want to layer things for some version of properly.