aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/fixtures.rb
diff options
context:
space:
mode:
authorJeremy Kemper <jeremy@bitsweat.net>2007-10-26 05:56:46 +0000
committerJeremy Kemper <jeremy@bitsweat.net>2007-10-26 05:56:46 +0000
commit49eafd8c3620bf8e46d21d447fc634a12c8280ab (patch)
tree5e76c1587d3e3db619944091923659846ecede51 /activerecord/lib/active_record/fixtures.rb
parent742694e0eb1abd049aa2a4eadbc7a93591c65db4 (diff)
downloadrails-49eafd8c3620bf8e46d21d447fc634a12c8280ab.tar.gz
rails-49eafd8c3620bf8e46d21d447fc634a12c8280ab.tar.bz2
rails-49eafd8c3620bf8e46d21d447fc634a12c8280ab.zip
Foxy fixtures. Adapter#disable_referential_integrity. Closes #9981.
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@8036 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
Diffstat (limited to 'activerecord/lib/active_record/fixtures.rb')
-rwxr-xr-xactiverecord/lib/active_record/fixtures.rb310
1 files changed, 295 insertions, 15 deletions
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