-
-
Save bokmann/2217602 to your computer and use it in GitHub Desktop.
| # 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. |
That is to say DataMapper 2 https://github.com/datamapper
Hey guys,
I have started experimenting with a similar idea (with a slightly different approach) here: https://github.com/fredwu/datamappify And I'd love to get some feedback from you all! :)
Basically, we want to separate the behaviour and persistence - without losing the convenience ActiveRecord (which we heavily rely on) provides to us.
Any thoughts?
Hi guys,
I have made some more progress on Datamappify. It no longer tries to do clever things with association, and it now maps data from any ActiveRecord classes!
Any feedback is welcomed! :)
Thanks for sharing this @bokmann. I really like the design that you've sketched out.
I stumbled across this today while researching repository implementations in Ruby land. I went looking for DataMapper 2 as suggested by @elight, but I later lids covered that it's been renamed to Ruby Object Mapper or ROM https://github.com/rom-rb/rom.
I also came across a gem named active_repostory, which looks close to this design, but not quite the same.
I was hoping that I wouldn't need to build my own repository framework to address problems on my current project. The design in this gist is going to be used for inspiration.
There's http://engineering.nulogy.com/posts/tag/edr that describes https://github.com/nulogy/edr.
But I'm not convinced that this is anything more than a stop-gap.
Why not Data Mapper? http://martinfowler.com/eaaCatalog/dataMapper.html