Wed 15 Nov 2006
We’re using FK constraints at Attendio. As you all know, this means we must be careful about the order we load fixtures in our tests when calling fixtures. But what if you can’t control the order of the fixture load as when using the FixtureSets or FixtureScenarios plugins? Or maybe you simply don’t want to have to worry about the order?
One solution is to disable FK constraints before loading the fixtures, then enable them afterward. This is liable to be a vendor-specific solution, however, and it just feels bad. Another solution is to determine the correct order and ensure that the fixtures you want to load are loaded according to that order (which would require a modification to the plugins, but at least they could then work without the user needing to specify an order). Let’s say you have a few models:
- class User < ActiveRecord::Base
- has_many :memberships
- has_many :groups, :through => :memberships
- has_many :taggings, :as => :taggable
- end
- class Group < ActiveRecord::Base
- has_many :memberships
- has_many :users, :through => :memberships
- has_many :taggings, :as => :taggable
- end
- class Membership < ActiveRecord::Base
- belongs_to :user
- belongs_to :group
- end
- class Tag < ActiveRecord::Base
- has_many :taggings
- end
- class Tagging < ActiveRecord::Base
- belongs_to :tag
- belongs_to :taggable, :polymorphic => true
- end
One correct order for loading the fixtures is users, groups, memberships, tags, taggings (there are four correct ways to load them). All we really have to do is look at the non-polymorphic belongs_to associations, and we’re set. Here’s some code that does just that.
Update: the code is cleaner now, and probably faster since it doesn’t build up an entire dependency tree.
- def load_order
- models = Dir[RAILS_ROOT + ‘/app/models/**/*.rb‘]
- models.each { |file| require(file) }
- klasses = models.map { |file| File.basename(file, ‘.rb‘).camelize.constantize }
- klasses.reject! { |klass| !klass.ancestors.include?(ActiveRecord::Base) }
- deps = klasses.inject({}) do |h,klass|
- h[klass] = klass.reflect_on_all_associations.select { |a| a.macro == :belongs_to && !a.options[:polymorphic] }
- h[klass] = h[klass].map { |a| a.class_name.constantize }
- h
- end
- order = []
- until deps.empty?
- free = deps.keys.select { |k| deps[k] == [] }
- deps = deps.without(*free)
- deps.each { |k,v| deps[k] = v - free }
- order += free
- end
- order.map(&:table_name)
- end
The code is rough and not yet integrated in a useful fashion, but it gives you an idea and will let you integrate it into your fixture solution as I have. It won’t work with HABTM relationships since there’s no model for the join table. Other than that it seems decent, if a bit on the ugly side. If anyone wants to clean it up I’d be obliged. Oh and by the way it’s under the Creative Commons Share-Alike license.