Blog post describing EDR: "Building Rich Domain Models in Rails. Separating Persistence" (though this post uses an older and more verbose version of the API).
Originally named for "Entity, Data, Repository", based somewhat on the Repository pattern. The edr gem separates lookup, persistence, and domain model responsibilities into distinct classes.
Domain Model classes are defined as "plain old Ruby objects" whose lifecycle is managed through a Repository class. The Repository creates Domain Model classes on DB reads and persisting them to the DB on saves through an ActiveRecord::Base. The Repository class can be viewed as a sort of simple Data Mapper, mapping a single database table onto a single Domain Model object. In this context, the ActiveRecord::Base subclass operates primarily as a Row Data Gateway.
Each Domain Model class has a single Repository that uses a single ActiveRecord::Base subclass.
Lifted from spec/test_data.rb
:
ActiveRecord::Schema.define(:version => 1) do
create_table "orders", :force => true do |t|
t.decimal "amount"
t.date "deliver_at"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
create_table "items", :force => true do |t|
t.string "name"
t.decimal "amount"
t.integer "order_id"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
end
class OrderData < ActiveRecord::Base
self.table_name = "orders"
attr_accessible :amount, :deliver_at
validates :amount, numericality: true
has_many :items, class_name: 'ItemData', foreign_key: 'order_id'
end
class ItemData < ActiveRecord::Base
self.table_name = "items"
attr_accessible :amount, :name
validates :amount, numericality: true
validates :name, presence: true
end
Fields and associations on the Domain Model are determined via ActiveRecord reflection. The Domain Model is coupled to its ActiveRecord class by naming convention.
Be aware that your Domain Model test/specs will need to stub/mock out dependencies upon their Repository and other Domain Model objects. That is, you Domain Model instances will lack fields or associations in their tests. This is because Domain Model objects are POROs until they are registered with edr at runtime. As you should not want to test the framework, this should facilitate testing your Domain Model and your persistence in isolation from one another and from the edr framework.
class Order
def add_item attrs
repository.create_item self, attrs
end
end
class Item
end
Domain Model classes should share the same name as the AR classes except they should not end in "Data". So OrderData < ActiveRecord::Base maps to the Order domain model class.
You probably want the below in a config/initializers/edr.rb
Edr::Registry.map_models_to_mappers
Your Repository class maps ActiveRecord CRUD results onto Domain Model instances.
module OrderRepository
extend Edr::Repository
set_model_class Order
def self.find_by_amount amount
where(amount: amount)
end
def self.find_by_id id
where(id: id).first
end
def self.create_item order, attrs
item = ItemRepository.create_item(order, attrs)
data(order).reload
return item
end
end
module ItemRepository
extend Edr::Repository
set_model_class Item
def self.create_item order, attrs
item = Item.build(attrs)
item.order_id = order.id
persist item
end
end
In an ideal world, the Domain Model classes would remain blissfully unaware of their Repositories. However, associations between Domain Model objects necessitate either awareness of the associated Domain Model class' Repository or arbitration via a Unit of Work. While a UoW could be defined/used by edr, this likely will remain beyond the scope of this gem.
It is my hope that, in the near future, we will all instead switch over to using Datamapper 2 when it is ready. However, as of the time of the writing of this README, DM2 is still pre-alpha.
Add this line to your application's Gemfile:
gem 'edr'
And then execute:
$ bundle
Or install it yourself as:
$ gem install edr