Your proposal fits with all of my multi-repository use cases, I can't think of one for the existing behaviour. As you mention, you can achieve it with inheritance (I'd probably use mixins but whatever).
On Feb 2, 5:45 am, Anthony Williams <h...@antw.me> wrote: > Hi all, > > I've been thinking a little about defining properties in DataMapper, > particularly relating to how they work within repository blocks. I've > posted some ideas to my > site:http://antw.me/thoughts/datamapper-property-api.html > > If you prefer to read them here instead (sans links and syntax > highlighting), I've included the full message below. > > I'd love to get some feedback; let me know what you think... > > Anthony. > > TRANSCRIPT: > > In my own dm-core fork on Github I've recently been experimenting with > ways to trim down both Resource and Model, extracting specific > functionality out to separate classes and modules. I've started by > relieving Resource of the need to take care of attributes, creating an > AttributeSet class which holds all of a Resource's attributes, tracks > when they've been updated (marking them as dirty), and lazy-loading > attributes when needed. > > Although not yet pushed to Github, my latest commits pass the full dm- > core spec suite against SQLite3 and PostgreSQL, but have 3-4 failures > with the InMemory and Yaml adapters. I eventually tracked this down to > an example where a Property is defined on a Model within the context > of a specific repository. For example: > > require 'dm-core' > > DataMapper.setup(:default, 'in_memory://localhost/one') > DataMapper.setup(:second, 'in_memory://localhost/two') > > class Person > include DataMapper::Resource > > property :id, Serial > property :name, String > > repository(:second) do > property :external_id, Integer > end > end > > This creates a Person model with two attributes: `id` and `name`, and > a third `external_id` attribute which applies only when using the > model within the `:second` repository context: > > DataMapper.repository(:second) do > Person.create(:name => 'Michael Scarn', :external_id => 1) > end > > In then realised my AttributeSet implementation didn't account for the > properties of a Model changing depending on the current repository > context. It then led to me thinking a little more about the purpose-- > and usefulness--of being able to define models in this way. > > I'd like to--perhaps a little presumptuously--suggest that this > functionality isn't as nice as it first seems, provide an alternative > means for achieving the same result, and elaborate on how I think such > repository blocks should work. > > ### Inconsistent instance API > > Allowing a user to wrap properties in a repository block results in a > model changing it's behaviour depending on external state (the current > repository context). At one moment the resource has an `external_id` > attribute, and in the next the attribute seems to disappear. > > DataMapper.repository(:second) do > Person.new(:external_id => 1) > end > # => #<Person @id=nil @name=nil> > > Wait... where did the `external_id` attribute go? In fact the > attribute was set, it just doesn't appear since `Person#inspect` was > called outside of the repository block... > > DataMapper.repository(:second) do > puts Person.new(:external_id => 1).inspect > end > # => #<Person @id=nil @name=nil @external_id=1> > > Aha! There it is. Trying to set the `external_id` attribute outside of > the `:second` repository context will also (rightly) fail. > > ### Ambiguity as to where a resource is saved > > DataMapper's repository context allows you to save any Resource to any > defined repository (providing they support the same features). > > person = Person.new(:name => 'Samuel L. Chang') > > # Now that I have my resource, I can save it to wherever I > # want... By default, the resource will be saved in the > # :default repository > person.save > > # Alternatively, I can specify a different repository... > DataMapper.repository(:second) do > person.save > end > > While this is an interesting feature, I'm struggling to come up with a > reason why you'd _want_ to do this. To me it just introduces > ambiguity: > > > Erm, where did I save that person instance? I'm sure it's around here > > somewhere... Where are you little person instance? Peekaboo! > > > <cite>Me, a year later.</cite> > > In reality, so long as you're explicit about wrapping parts of your > application in the correct repository blocks, this is not a problem. > But wherever you have to be explicit there is the possibility that > someone will forget; forgetting _just once_ might be enough to cause > obscure bugs. > > If the `external_id` attribute was set to disallow nil, the second > call to `person.save` in the above example would fail, since no value > was set. (In fact, the above example would fail anyway, since the > first call to `person.save` would mark the resource as clean, thus the > second call would do nothing.) > > ## A better way? > > I'm of the belief that each model should be associated with one--and > only one--repository. This would be the `:default` repository, except > where a user explicitly declares otherwise when setting up their > model. A `Person` would be associated with the default repository > _always_, regardless of the current repository context. In the example > below, the person would be persisted to the default repository even > though it's wrapped in another repo. > > person = Person.new(:name => 'Michael Scarn') > > DataMapper.repository(:second) do > person.save > end > > DataMapper could provide a method for changing the default repository: > > class Person > include DataMapper::Resource > > # Tells DM that the Person model should be persisted > # to the :second repository. > set_repository :second > > property :id, Serial > property :name, String > end > > By doing this, users would never need to worry about repository > context outside of their models, making their domain objects much more > straight-forward. > > As far as I'm concerned, `Person` and `repo(:second) { Person }` are > two different models, with different interfaces, different properties, > and are stored in different repositories. The second Person should > probably be represented as another model, distinct from the first. > > Since DataMapper doesn't congflate class inheritance with Single Table > Inheritance, we could use inheritance to achieve the same effect as > the current API: > > class Person > include DataMapper::Resource > > property :id, Serial > property :name, String > end > > # Inherits properties from Person, but adds it's own > # custom properties, and persists to another repo. > class HRPerson < Person > set_repository :second > property :external_id, Integer > end > > ### Problems with this approach... > > `Model#copy` would break. Well... it wouldn't just break. The entire > concept of copying resources across repositories would become > redundant. > > ## An alternative meaning for repository blocks > > By doing away with the current meaning of repository blocks within > model instances, we free up the API to do something I think is much > more interesting: models which persist _across_ multiple repositories. > > Let's take a (slightly contrived) example... > > DataMapper.setup(:default, 'yaml://localhost/main') > DataMapper.setup(:human_resources, 'yaml://localhost/hr') > > class Employee > include DataMapper::Resource > > property :id, Serial > property :name, String > property :username, String > property :password, String > > repository(:human_resources) do > property :salary, Integer > property :pay_on, Date > end > end > > Our employee model has six properties: `name`, `username`, and > `password` will be persisted to the default repository, while `salary` > and `pay_on` will be persisted to the human resources repository. > `id`, since it is a key, is used in _both_. > > Let's create a employee... > > Employee.create( > :name => 'Michael Scarn', > :username => 'mscarn', > :password => '12345', > :salary => 2000, > :pay_on => Date.today > ) > > Here's what would happen "under the hood": > > 1. We assume that the key is generated by the model's default > repository. In the absence of a `set_repository` statement, DataMapper > assumes `:default`. > > 2. DataMapper then saves the resource to the default repository. In > this example it persists the name, username, and password, and returns > the ID which was generated. > > 3. It then proceeds to persist the salary and pay_on attributes to the > human resources repository with the ID returned by the default repo. > > Our storage ends up looking a little like this: > > # default/employees.yaml > - id: 95143 > name: "Michael Scarn" > username: "mscarn" > password: "12345" > > # hr/employees.yaml > - id: 95143 > salary: 2000 > pay_on: 2010-02-01 > > ### Lazy loading from multiple repositories > > Loading a resource without specifying which fields you want to load > would work in a way similar to lazy loading. > > user = User.get(95143) > > This loads the User with `id`, `name`, `username`, and `password` from > the default repository. Calling `user.salary` would load all of the > attributes which belong to the human resources repository. > > user = DataMapper.repository(:human_resources) do > User.get(95143) > end > > # ... or ... > user = User.get(95143, :repository => :human_resources) > > This loads the User with `id`, `salary`, and `pay_on` from the human > resources repository. Calling `user.name` would load all of the > attributes which belong to the default repository. > > ### Finishing up > > I think this behaviour has a lot of potential: In many web > applications developers have made the compromise of denormalising data > in order to improve performance. DataMapper could instead provide an > API to store these denormalised "cache" attributes in a fast key/value > store. > > class Journey > include DataMapper::Resource > > property :id, Serial > property :start_at, String > property :end_at, String > > repository(:redis) do > property :really_expensive_computation, String > end > end -- You received this message because you are subscribed to the Google Groups "DataMapper" group. To post to this group, send email to datamap...@googlegroups.com. To unsubscribe from this group, send email to datamapper+unsubscr...@googlegroups.com. For more options, visit this group at http://groups.google.com/group/datamapper?hl=en.