h2. dm-accepts_nested_attributes A DataMapper plugin that allows nested model attribute assignment like activerecord does. At the end of this file, you can see a list of all current integration specs. For more information on the progress, have a look at this README and also at "this article":http://sick.snusnu.info/2009/04/08/dm-accepts_nested_attributes/ on my blog, where I will try to comment on the development (problems). h2. Current limitations Interaction with @dm-validations@ is actually possible but not very well specced atm. I added @not null@ constraints to all spec fixtures for now, but no other custom validations. All specs still pass. However, as long as I'm not decided on where to put the specs for interaction with @dm-validations@ (most probably inside @dm-validations@ would be the right place for these), I won't write many more specs for these scenarios, since it's very hard to decide where to stop, once I start writing some. Currently, the creation of the record together with all its join models, is not handled inside a transaction. This must definitely change! As soon as I find out why my initial experiments with transactions consistently yielded _no such table errors_ while migrating the specsuite (see "this pastie":http://pastie.org/446060), I will do my best to add this feature. h2. TODO * use transactions * update README to include more complete usecases * specs for custom validations (dm-validations in general) * specs for adding errors to the parent objects * reorganize specs and fix bugs * Adapt to datamapper/next h2. Implementation details This section mainly serves as a place for me to take notes during development. h3. Why isn't this implemented as options on association declarations? * I somehow like the declarative style of @accepts_nested_attributes_for@ better. it jumps out immediately. * The API for datamapper and activerecord is the same. * association definitions can already get quite long and "unreadable". chances are you overlook it! h3. Why doesn't accepts_nested_attributes_for take more than one association_name? While writing the unit specs for this method, I realised that there are way too many ways to call this method, which makes it "hard" to spec all possible calls. That's why I started to list Pros and Cons, and decided to support only one @association_name@ per call, at least for now. h4. Pros * less complex code * fewer ways to call the method (simpler to understand, easier to spec) * easier to read (nr of calls == nr of accessible associations, this could be seen as a con also) * easier (and more extensible) option handling ** options don't implicitly apply to _all_ associations (could be seen as a con also?) ** options can explicitly be applied to _only the desired_ associations ** reject_if option maybe often makes more sense on exactly _one_ associaton (maybe not?) * no question what happens if the association_name is invalid (the whole call is invalid) ** with at least one _invalid_ association_name, what happens for the other _valid_ ones? h4. Cons * needs more method calls (overhead should be minimal) * options that apply to more than one attribute need to be duplicated (maybe a Pro because of readability) h3. Examples The following example illustrates the use of this plugin.
require "rubygems"
gem 'dm-core', '0.9.11'
gem 'dm-validations', '0.9.11'
gem 'dm-accepts_nested_attributes', '0.0.1'
require "dm-core"
require "dm-validations"
require "dm-accepts_nested_attributes"
DataMapper::Logger.new(STDOUT, :debug)
DataMapper.setup(:default, 'sqlite3::memory:')
class Person
include DataMapper::Resource
property :id, Serial
property :name, String
has 1, :profile
has n, :project_memberships
has n, :projects, :through => :project_memberships
accepts_nested_attributes_for :profile
accepts_nested_attributes_for :projects
# adds the following instance methods
# #profile_attributes
# #projects_attributes
end
class Profile
include DataMapper::Resource
property :id, Serial
property :person_id, Integer
belongs_to :person
accepts_nested_attributes_for :person
# adds the following instance methods
# #person_attributes
end
class Project
include DataMapper::Resource
property :id, Serial
has n, :tasks
has n, :project_memberships
has n, :people, :through => :project_memberships
accepts_nested_attributes_for :tasks
accepts_nested_attributes_for :people
# adds the following instance methods
# #tasks_attributes
# #people_attributes
end
class ProjectMembership
include DataMapper::Resource
property :id, Serial
property :person_id, Integer
property :project_id, Integer
belongs_to :person
belongs_to :project
# nothing added here
# code only listed to provide complete example env
end
class Task
include DataMapper::Resource
property :id, Serial
property :project_id, Integer
belongs_to :project
# nothing added here
# code only listed to provide complete example env
end
DataMapper.auto_migrate!
h2. Current Integration Specs
DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for(:person)
- should allow to create a new person via Profile#person_attributes
- should allow to update an existing person via Profile#person_attributes
- should not allow to delete an existing person via Profile#person_attributes
DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for(:person, :allow_destroy => false)
- should allow to create a new person via Profile#person_attributes
- should allow to update an existing person via Profile#person_attributes
- should not allow to delete an existing person via Profile#person_attributes
DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for(:person, :allow_destroy = true)
- should allow to create a new person via Profile#person_attributes
- should allow to update an existing person via Profile#person_attributes
- should allow to delete an existing person via Profile#person_attributes
DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for :person, :reject_if => :foo
- should allow to create a new person via Profile#person_attributes
- should allow to update an existing person via Profile#person_attributes
- should not allow to delete an existing person via Profile#person_attributes
DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for :person, :reject_if => lambda { |attrs| true }
- should not allow to create a new person via Profile#person_attributes
- should not allow to delete an existing person via Profile#person_attributes
DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for :person, :reject_if => lambda { |attrs| false }
- should allow to create a new person via Profile#person_attributes
- should allow to update an existing person via Profile#person_attributes
- should not allow to delete an existing person via Profile#person_attributes
DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for(:profile)
- should allow to create a new profile via Person#profile_attributes
- should allow to update an existing profile via Person#profile_attributes
- should not allow to delete an existing profile via Person#profile_attributes
DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for(:profile, :allow_destroy => false)
- should allow to create a new profile via Person#profile_attributes
- should allow to update an existing profile via Person#profile_attributes
- should not allow to delete an existing profile via Person#profile_attributes
DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for(:profile, :allow_destroy => true)
- should allow to create a new profile via Person#profile_attributes
- should allow to update an existing profile via Person#profile_attributes
- should allow to delete an existing profile via Person#profile_attributes
DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for :profile, :reject_if => :foo
- should allow to create a new profile via Person#profile_attributes
- should allow to update an existing profile via Person#profile_attributes
- should not allow to delete an existing profile via Person#profile_attributes
DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for :profile, :reject_if => lambda { |attrs| true }
- should not allow to create a new profile via Person#profile_attributes
- should not allow to delete an existing profile via Person#profile_attributes
DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for :profile, :reject_if => lambda { |attrs| false }
- should allow to create a new profile via Person#profile_attributes
- should allow to update an existing profile via Person#profile_attributes
- should not allow to delete an existing profile via Person#profile_attributes
DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for(:tasks)
- should allow to create a new task via Project#tasks_attributes
- should allow to update an existing task via Project#tasks_attributes
- should not allow to delete an existing task via Profile#tasks_attributes
DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for(:tasks, :allow_destroy => false)
- should allow to create a new task via Project#tasks_attributes
- should allow to update an existing task via Project#tasks_attributes
- should not allow to delete an existing task via Profile#tasks_attributes
DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for(:tasks, :allow_destroy => true)
- should allow to create a new task via Project#tasks_attributes
- should allow to update an existing task via Project#tasks_attributes
- should allow to delete an existing task via Profile#tasks_attributes
DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for :tasks, :reject_if => :foo
- should allow to create a new task via Project#tasks_attributes
- should allow to update an existing task via Project#tasks_attributes
- should not allow to delete an existing task via Profile#tasks_attributes
DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for :tasks, :reject_if => lambda { |attrs| true }
- should not allow to create a new task via Project#tasks_attributes
- should not allow to delete an existing task via Profile#tasks_attributes
DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for :tasks, :reject_if => lambda { |attrs| false }
- should allow to create a new task via Project#tasks_attributes
- should allow to update an existing task via Project#tasks_attributes
- should not allow to delete an existing task via Profile#tasks_attributes
DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for(:projects)
- should allow to create a new project via Person#projects_attributes
- should allow to update an existing project via Person#projects_attributes
- should not allow to delete an existing project via Person#projects_attributes
DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for(:projects, :allow_destroy => false)
- should allow to create a new project via Person#projects_attributes
- should allow to update an existing project via Person#projects_attributes
- should not allow to delete an existing project via Person#projects_attributes
DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for(:projects, :allow_destroy = true)
- should allow to create a new project via Person#projects_attributes
- should allow to update an existing project via Person#projects_attributes
- should allow to delete an existing project via Person#projects_attributes
DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for :projects, :reject_if => :foo
- should allow to create a new project via Person#projects_attributes
- should allow to update an existing project via Person#projects_attributes
- should not allow to delete an existing project via Person#projects_attributes
DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for :projects, :reject_if => lambda { |attrs| true }
- should not allow to create a new project via Person#projects_attributes
- should not allow to delete an existing project via Person#projects_attributes
DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for :projects, :reject_if => lambda { |attrs| false }
- should allow to create a new project via Person#projects_attributes
- should allow to update an existing project via Person#projects_attributes
- should not allow to delete an existing project via Person#projects_attributes