From 49eafd8c3620bf8e46d21d447fc634a12c8280ab Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Fri, 26 Oct 2007 05:56:46 +0000 Subject: Foxy fixtures. Adapter#disable_referential_integrity. Closes #9981. git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@8036 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- activerecord/lib/active_record/fixtures.rb | 310 +++++++++++++++++++++++++++-- 1 file changed, 295 insertions(+), 15 deletions(-) (limited to 'activerecord/lib/active_record/fixtures.rb') diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index b314340f0c..c9fc87756d 100755 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -215,6 +215,199 @@ end # the results of your transaction until Active Record supports nested transactions or savepoints (in progress.) # 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM. # Use InnoDB, MaxDB, or NDB instead. +# +# = Advanced YAML Fixtures +# +# YAML fixtures that don't specify an ID get some extra features: +# +# * Stable, autogenerated ID's +# * Label references for associations (belongs_to, has_one, has_many) +# * HABTM associations as inline lists +# * Autofilled timestamp columns +# * Fixture label interpolation +# * Support for YAML defaults +# +# == Stable, autogenerated ID's +# +# Here, have a monkey fixture: +# +# george: +# id: 1 +# name: George the Monkey +# +# reginald: +# id: 2 +# name: Reginald the Pirate +# +# Each of these fixtures has two unique identifiers: one for the database +# and one for the humans. Why don't we generate the primary key instead? +# Hashing each fixture's label yields a consistent ID: +# +# george: # generated id: 503576764 +# name: George the Monkey +# +# reginald: # generated id: 324201669 +# name: Reginald the Pirate +# +# ActiveRecord looks at the fixture's model class, discovers the correct +# primary key, and generates it right before inserting the fixture +# into the database. +# +# The generated ID for a given label is constant, so we can discover +# any fixture's ID without loading anything, as long as we know the label. +# +# == Label references for associations (belongs_to, has_one, has_many) +# +# Specifying foreign keys in fixtures can be very fragile, not to +# mention difficult to read. Since ActiveRecord can figure out the ID of +# and fixture from its label, you can specify FK's by label instead of ID. +# +# === belongs_to +# +# Let's break out some more monkeys and pirates. +# +# ### in pirates.yml +# +# reginald: +# id: 1 +# name: Reginald the Pirate +# monkey_id: 1 +# +# ### in monkeys.yml +# +# george: +# id: 1 +# name: George the Monkey +# pirate_id: 1 +# +# Add a few more monkeys and pirates and break this into multiple files, +# and it gets pretty hard to keep track of what's going on. Let's +# use labels instead of ID's: +# +# ### in pirates.yml +# +# reginald: +# name: Reginald the Pirate +# monkey: george +# +# ### in monkeys.yml +# +# george: +# name: George the Monkey +# pirate: reginald +# +# Pow! All is made clear. ActiveRecord reflects on the fixture's model class, +# finds all the +belongs_to+ associations, and allows you to specify +# a target *label* for the *association* (monkey: george) rather than +# a target *id* for the *FK* (monkey_id: 1). +# +# === has_and_belongs_to_many +# +# Time to give our monkey some fruit. +# +# ### in monkeys.yml +# +# george: +# id: 1 +# name: George the Monkey +# pirate_id: 1 +# +# ### in fruits.yml +# +# apple: +# id: 1 +# name: apple +# +# orange: +# id: 2 +# name: orange +# +# grape: +# id: 3 +# name: grape +# +# ### in fruits_monkeys.yml +# +# apple_george: +# fruit_id: 1 +# monkey_id: 1 +# +# orange_george: +# fruit_id: 2 +# monkey_id: 1 +# +# grape_george: +# fruit_id: 3 +# monkey_id: 1 +# +# Let's make the HABTM fixture go away. +# +# ### in monkeys.yml +# +# george: +# name: George the Monkey +# pirate: reginald +# fruits: apple, orange, grape +# +# ### in fruits.yml +# +# apple: +# name: apple +# +# orange: +# name: orange +# +# grape: +# name: grape +# +# Zap! No more fruits_monkeys.yml file. We've specified the list of fruits +# on George's fixture, but we could've just as easily specified a list +# of monkeys on each fruit. As with +belongs_to+, ActiveRecord reflects on +# the fixture's model class and discovers the +has_and_belongs_to_many+ +# associations. +# +# == Autofilled timestamp columns +# +# If your table/model specifies any of ActiveRecord's +# standard timestamp columns (created_at, created_on, updated_at, updated_on), +# they will automatically be set to Time.now. +# +# If you've set specific values, they'll be left alone. +# +# == Fixture label interpolation +# +# The label of the current fixture is always available as a column value: +# +# geeksomnia: +# name: Geeksomnia's Account +# subdomain: $LABEL +# +# Also, sometimes (like when porting older join table fixtures) you'll need +# to be able to get ahold of the identifier for a given label. ERB +# to the rescue: +# +# george_reginald: +# monkey_id: <%= Fixtures.identify(:reginald) %> +# pirate_id: <%= Fixtures.identify(:george) %> +# +# == Support for YAML defaults +# +# You probably already know how to use YAML to set and reuse defaults in +# your +database.yml+ file,. You can use the same technique in your fixtures: +# +# DEFAULTS: &DEFAULTS +# created_on: <%= 3.weeks.ago.to_s(:db) %> +# +# first: +# name: Smurf +# <<: *DEFAULTS +# +# second: +# name: Fraggle +# <<: *DEFAULTS +# +# Any fixture labeled "DEFAULTS" is safely ignored. + class Fixtures < YAML::Omap DEFAULT_FILTER_RE = /\.ya?ml$/ @@ -279,32 +472,41 @@ class Fixtures < YAML::Omap unless table_names_to_fetch.empty? ActiveRecord::Base.silence do - fixtures_map = {} + connection.disable_referential_integrity do + fixtures_map = {} - fixtures = table_names_to_fetch.map do |table_name| - fixtures_map[table_name] = Fixtures.new(connection, File.split(table_name.to_s).last, class_names[table_name.to_sym], File.join(fixtures_directory, table_name.to_s)) - end + fixtures = table_names_to_fetch.map do |table_name| + fixtures_map[table_name] = Fixtures.new(connection, File.split(table_name.to_s).last, class_names[table_name.to_sym], File.join(fixtures_directory, table_name.to_s)) + end - all_loaded_fixtures.update(fixtures_map) + all_loaded_fixtures.update(fixtures_map) - connection.transaction(Thread.current['open_transactions'].to_i == 0) do - fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures } - fixtures.each { |fixture| fixture.insert_fixtures } + connection.transaction(Thread.current['open_transactions'].to_i == 0) do + fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures } + fixtures.each { |fixture| fixture.insert_fixtures } - # Cap primary key sequences to max(pk). - if connection.respond_to?(:reset_pk_sequence!) - table_names.each do |table_name| - connection.reset_pk_sequence!(table_name) + # Cap primary key sequences to max(pk). + if connection.respond_to?(:reset_pk_sequence!) + table_names.each do |table_name| + connection.reset_pk_sequence!(table_name) + end end end - end - cache_fixtures(connection, fixtures) + cache_fixtures(connection, fixtures) + end end end cached_fixtures(connection, table_names) end + # Returns a consistent identifier for +label+. This will always + # be a positive integer, and will always be the same for a given + # label, assuming the same OS, platform, and version of Ruby. + def self.identify(label) + label.to_s.hash.abs + end + attr_reader :table_name def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE) @@ -322,12 +524,90 @@ class Fixtures < YAML::Omap end def insert_fixtures - values.each do |fixture| + now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now + now = now.to_s(:db) + + # allow a standard key to be used for doing defaults in YAML + delete(assoc("DEFAULTS")) + + # track any join tables we need to insert later + habtm_fixtures = Hash.new do |h, habtm| + h[habtm] = HabtmFixtures.new(@connection, habtm.options[:join_table], nil, nil) + end + + each do |label, fixture| + row = fixture.to_hash + + if model_class && model_class < ActiveRecord::Base && !row[primary_key_name] + # fill in timestamp columns if they aren't specified + timestamp_column_names.each do |name| + row[name] = now unless row.key?(name) + end + + # interpolate the fixture label + row.each do |key, value| + row[key] = label if value == "$LABEL" + end + + # generate a primary key + row[primary_key_name] = Fixtures.identify(label) + + model_class.reflect_on_all_associations.each do |association| + case association.macro + when :belongs_to + if value = row.delete(association.name.to_s) + fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s + row[fk_name] = Fixtures.identify(value) + end + when :has_and_belongs_to_many + if (targets = row.delete(association.name.to_s)) + targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) + join_fixtures = habtm_fixtures[association] + + targets.each do |target| + join_fixtures["#{label}_#{target}"] = Fixture.new( + { association.primary_key_name => Fixtures.identify(label), + association.association_foreign_key => Fixtures.identify(target) }, nil) + end + end + end + end + end + @connection.insert_fixture(fixture, @table_name) end + + # insert any HABTM join tables we discovered + habtm_fixtures.values.each do |fixture| + fixture.delete_existing_fixtures + fixture.insert_fixtures + end end private + class HabtmFixtures < ::Fixtures #:nodoc: + def read_fixture_files; end + end + + def model_class + @model_class ||= @class_name.is_a?(Class) ? + @class_name : @class_name.constantize rescue nil + end + + def primary_key_name + @primary_key_name ||= model_class && model_class.primary_key + end + + def timestamp_column_names + @timestamp_column_names ||= %w(created_at created_on updated_at updated_on).select do |name| + column_names.include?(name) + end + end + + def column_names + @column_names ||= @connection.columns(@table_name).collect(&:name) + end + def read_fixture_files if File.file?(yaml_file_path) read_yaml_fixture_files -- cgit v1.2.3