Skip to content

steveklabnik/ActiveRepository

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 

Repository files navigation

# MOTIVATION:  As rails apps are growing, people are noticing the drawbacks
# of the ActiveRecord pattern.  Several apps I have seen, and several
# developers I have spoken to are looking towards other patterns for object
# persistence.  The major drawback with ActiveRecord is that the notion
# of the domain object is conflated with what it means to store/retrieve
# it in any given format (like sql, json, key/value, etc).
#
# This is an attempt to codify the Repository pattern in a way that would
# feel comfortable to beginner and seasoned Ruby developers alike.
#
# In the spirit of "make it look the way you want it to work, then make it
# work like that", here is a vision of the Repository pattern ala Ruby. I
# don't want to fault ActiveRecord for being an implementation of the
# ActiveRecord pattern; At RailsConf 2006, in his keynote, Martin Fowler
# said that ActiveRecord was "the most faithful implementation of the
# Activerecord pattern [he] had ever seen", and he was the one that
# documented that pattern in the first place.  So lets respect AR for what
# it is, and look at the repository pattern, done with Rails Flair.



# Person is a plain old Ruby object. It knows nothing about its persistence,
# but as a domain model, it knows something about its own attributes,
# relationships to other domain objects, as well as understands what it
# means to be 'valid'. Of course, it also has behavior.

class Person
  include ActiveModel::Relationships
  include ActiveModel::Validations
  
  has_many :hobbies
  belongs_to :fraternity
  
  attr_accessor :name
  attr_accessor :password
  attr_accessor :sex
  attr_accessor :born_on
  
  validates_presence_of :name, :password
  
  def favorite_drink
    if born_on < 7.years.ago
      "apple juice"
    elsif born_on < 18.years.ago
      "coke or pepsi"
    elsif < 21.years.ago
      "mountain dew"
    else
      "beer"
    end
  end
end

# conventions like _on and _at are still used.


# The person has no clue it is 'Persistent Capable'.  All of that is handled
# in the Repository.  Notice we could call the Repo anything we want.

class PersonStore < ActiveRepository::Base
  repository_for :person
  
  # other things that control the persistence can go here
  table :users
  encrypts_attribute :password
  map_attribute_to_column :sex, :gender
end

# I'm using inheritance here to enforce an opinion.  Of course this could
# be a mixin, but my current thoughts are that this Repository should only
# ever be a repository - after all, we are trying to get away from
# ActiveRecord's notion of "I'm the model and my own data store!".
# Inheritance would be an appropriate way to signal this *is a* repository.
# My fear is as a mixin, someone would think they are being clever by mixing
# the repository directly into the domain model, essentially recreating
# ActiveRecord and making this effort all for nothing.

# I would really like to have the ability to 'new' model objects as normal:

p = Person.new

# but it might be necessary to create them through the store, like:

p = PersonStore.create #, or
p = PersonStore.build

# saving is no longer done on the object, but through the repository

PersonStore.save(p)

# the save would be smart enough to call is_valid? if Validations were present.

# all the usual finder suspects are on the repository
p = PersonStore.find(5)
p = PersonStore.first
p = PersonStore.all
p = PersonStore.find_by_name("Chris")

# we could also create things like scopes, etc.


# Since Person is nothing special, you could easily swap in a different
# persistance Repository:

Class PersonStore < RedisRepository::Base
...
end

# or even:

Class PersonStore < RestfulResource::Repository
  repository_for :person
  base_uri "http://coolstuff.livingsocial.com"
end


# Swapping repositories might give you radically different methods (only an
# ActiveRepository would give you a find_by_sql method, for instance), but
# thats ok.  The "Persistant Capable" classes don't change with a change in
# the persistence mechanism. The "Persistent Aware" classes, like the
# controllers, would.  

# And it might even be possible to have multiple repositories in the same
# app...

Class PersonStore < ActiveRepository::Base
#...
end

# and

Class RemotePersonStore < RestfulResource::Repository
#...
end

# and then you could do stuff like:

p = RemotePersonStore.find(5)
PersonStore.save(p)

# and essentially use two repositories as an ETL engine.


# One nice thing we get from ActiveRecord would have to change slightly -
# the migrations.
# Actually, the migrations could work pretty much as they do now, but devs
# would have to make the corresponding attr_accessor declarations in their
# models.
#
# if an attr_accessor was declared that didn't have a corresponding column in
# the db, then it could warn on application init.  That warning for that field
# could be silenced in the repository with something like:

not_persisted :current_location

# and in reverse, if a migration added a column that couldn't map to an
# attr_accessor, it could warn unless the repo had a line like:

ignore_column :eye_color

# The generator could stick the attr_accessors in the class
# automatically if we wanted it to.  I wouldn't do anything 'magical' like
# have the persistence engine inject it with meta... that would make the
# attributes hard to discover, and could make multiple repositories in an
# app step on each other in nondeterministic ways.  By having attr_accessors,
# the model becomes the 'system of record' for what it means to be that
# kind of domain object.  I like that.

# (of course, nosql dbs may have their own requirements met with their own
# dsls).

# You could even have the store do something like:

synthetic_field :age, { Date.today - born_on }

# while you could do exactly the same thing by adding 'age' method to the
# model, having it in he Repository could be useful for an ETL task, for
# defining the transform step.  Imagine one database that has a born_on
# field, and another one that has an age field, and you are transforming
# data to go into the one with age...  in the store with the born_on,
# set the store to have a synthetic field :age.  In the other store, set
# it to ignore the born_on date. Then in the model you define attr_reader
# for :age.  Both stores see the domain model as exactly what it needs in
# order to make each database happy, and the domain model code stays clean.
#
# if you needed to map an attribute to a different column:

map_attribute_to_column :sex, :gender

# One potentially awesome tweak to the dsl is how this would
# handle polymorphic tables:

class PersonStore < ActiveRepository::Base
  repository_for :person
  repository_for :client
  repository_for :administrator
  polymorphic_model_column :type # would automatically store the class
                                 # type, just like AR polymorphism
end



# Given this pattern, I think relationship declarations go in the models,
# since there they can add the appropriate methods to return collections,
# etc, and since the repo knows what models it is supposed to manage, it can
# get access to the same knowledge to do whatever it needs to do.  If they
# were declared in the repo, it would be inappropriate for the model to
# introspect there, otherwise the model would become 'persistence aware'.
# They 'feel' a little attr_accessor-like things to me.

# Finally, while the model as code looks completely unaware of its storage,
# underneath the covers at runtime the repository could 'enhance' it with
# things like dirty flags for fields, is_dirty? & is_new? methods, etc.
# In fact, for years I used an object-oriented database in Java named
# Versant, and it had a 'bytecode enhancement' step that did exactly this
# during the compile - it modified the classes bytecode with dirty flags
# and other persistence helpers.

About

A brain dump of a new persistence strategy for Rails.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages