Saturday, May 24, 2008

Fixtures

Rails fixtures suck. Everyone knows it, they don't know what foreign keys are, they are inflexible. In merb there are no fixtures since merb is ORM-agnostic and there are also lots of attempts to create a flexible fixture system (every attempt has drawbacks though). For my merb app I created simple rake task to load fixtures which are kept in a specific folder in YAML files.

$map = Hash.new

path = Merb.root / "spec" / "fixtures"
files = ["users", "news_items", "privs"]
files.reverse.each { |f| f.classify.constantize.create_table! }
files.map! { |f| (path / f) + ".yml" }

files.each do |path|
puts "Processing #{path}"
fixtures = YAML::load_file(path) || {}
klass = File.basename(path, ".yml")
klass = klass.classify.constantize
fixtures.each do |name, attributes|
attributes.each_pair do |key, value|
if value =~ /^@/
methods = value[1 .. -1].split(".")
m = methods.shift
value = $map[m]
raise "Value is nil for key '#{m}'" if value.nil?
value = value.send(methods.shift) while !methods.empty?
attributes[key] = value
end
end
object = klass.new(attributes)
raise "Object invalid: #{object.inspect}\n#{object.errors.inspect}" unless object.valid?
object.save
raise "Key '#{name}' already exists!" if $map[name]
$map[name] = object
end
end


What happens here is we recreate the table for which we are about to load fixtures. Notice the order of files they appear in. You have to take it into consideration when you take advantage of foreign keys in your database (that's why the #reverse method). Then we map the names to have YAML paths.

For each of the file, we take all the fixtures written in it. Each fixture is assigned to specific *unique* name. For example fixture
fix1:
name: Fixture 1

has name "fix1".

The interesting part is the processing of attributes hash. We look for all the attribute values which begin with "@". This is my own choice, you could choose something else. When a value beggining with "@" is found, we treat it as a reference to an object.

methods = value[1 .. -1].split(".")
m = methods.shift
value = $map[m]

We create a "methods" table by splitting with a dot, and then we take the unique name of the fixture (m = methods.shift). We look for the reference in a global $map Hash to which we add objects when we create it (key is the fixture name and it references the object). When we have the object (value) we send it all the methods we need (usually this is #id since we mainly need the id of an DB object. Then we create an object in the db and save a reference to it in global $map.

Notice all the "raise" lines. This is just for security and debugging reasons, we want to check if we have nil object which we don't like (and maybe we "didn't expect it" ;P) and raise also when a key exists already in hash (we wouldn't like to overwrite a key by mistake).

That's it, simple but works for me, all the fixtures load nicely in the db, and it's very easy to write a fixture which depends on other objects (since we need only the unique name). This is also quite efficient, because we keep all the references, and don't have to ask the DB for objects (we just save them). I didn't have the opportunity to check how does this code behave with *loads* of fixtures, but for now it's going ok and I keep adding my fixtures painless.

0 comment(s):