Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Ticket #6424 (closed enhancement: fixed)

Opened 3 years ago

Last modified 2 years ago

[PATCH] named foreign key lookups in fixtures

Reported by: court3nay Assigned to: David
Priority: normal Milestone: 2.x
Component: ActiveRecord Version: edge
Severity: normal Keywords: fixtures foreign key
Cc: esad@esse.at, bitsweat

Description

from http://blog.caboo.se/articles/2006/10/17/named-references-for-test-fixtures

Allows you to do simple lookups in fixtures. fairly opinionated. tested and documented. this fundamentally changes the way we'll all manage our fixture associations :)

user.yml

joe:
  id: 1
  name: Joe

house.yml

home:
  id: 5
  user: :joe

results

House.find(5).user_id
=> 1

Attachments

db_fixture_patch (6.3 kB) - added by court3nay on 10/21/06 03:17:31.
patch for fixtures and rake task

Change History

(follow-up: ↓ 2 ) 10/17/06 11:08:30 changed by court3nay

can't upload the patch. damnit. this is all in activerecord.

Index: test/fixtures_test.rb
===================================================================
--- test/fixtures_test.rb	(revision 5316)
+++ test/fixtures_test.rb	(working copy)
@@ -363,7 +363,6 @@
   end
 end
 
-
 class FixturesBrokenRollbackTest < Test::Unit::TestCase
   def blank_setup; end
   alias_method :ar_setup_with_fixtures, :setup_with_fixtures
@@ -388,3 +387,19 @@
       raise 'argh'
     end
 end
+
+require 'fixtures/reader'
+require 'fixtures/person'
+
+class NamedFixtures < Test::Unit::TestCase
+  fixtures :readers, :people
+  
+  # these fixtures are assigned by tablename: :fixturename rather than foreign_key and id
+  def test_named_fixtures_auto_assigned_id
+    assert_equal people(:mary).id, readers(:mary_welcome).person_id
+    assert_equal people(:mary), readers(:mary_welcome).person
+    
+    assert_equal people(:michael).id, readers(:michael_welcome).person_id
+    assert_equal people(:michael), readers(:michael_welcome).person
+  end
+end
\ No newline at end of file
Index: test/fixtures/readers.yml
===================================================================
--- test/fixtures/readers.yml	(revision 5316)
+++ test/fixtures/readers.yml	(working copy)
@@ -2,3 +2,13 @@
   id: 1
   post_id: 1
   person_id: 1
+
+mary_welcome:
+  id: 2
+  post_id: 2
+  person: :mary
+  
+michael_welcome_2:
+  id: 3
+  post_id: 1
+  person: :michael
\ No newline at end of file
Index: test/fixtures/people.yml
===================================================================
--- test/fixtures/people.yml	(revision 5316)
+++ test/fixtures/people.yml	(working copy)
@@ -1,3 +1,7 @@
 michael:
   id: 1
-  first_name: Michael
\ No newline at end of file
+  first_name: Michael
+  
+mary:
+  id: 2
+  first_name: Mary
\ No newline at end of file
Index: lib/active_record/fixtures.rb
===================================================================
--- lib/active_record/fixtures.rb	(revision 5316)
+++ lib/active_record/fixtures.rb	(working copy)
@@ -42,6 +42,40 @@
 # indented list of key/value pairs in the "key: value" format.  Records are separated by a blank line for your viewing
 # pleasure.
 #
+# == Rails extensions 
+# Rails extends the YAML model somewhat. Once you start building a complex application, managing interactions between data 
+# in separeate fixture files becomes time-consuming and confusing.  For example, maintaining the foreign-key IDs between 
+# associated models makes it difficult to add new records or understand existing data because you're always looking up the IDs.
+#
+# If your foreign keys and fixture data follow strict rails conventions, such as users.yml and (foreign key) user_id 
+# you can take advantage of named foreign keys by using the form, <table name>: :record_name
+#
+# For example, when the format of users.yml is
+# 
+#   joe: 
+#     id: 1
+#     name: Joe
+# 
+#   mary: 
+#     id: 2
+#     name: Mary-Jane
+#
+# ..and the format of an associated table's fixture file, say, houses.yml (where house belongs_to user)
+#
+#   joe_home:
+#     id: 1
+#     user: :joe
+#     name: Joe's Place
+#
+#   mary_home:
+#     id: 2
+#     user: :mary
+#     name: Mary In The Hizzle
+#
+# Upon instantiation, the joe_home record will be automatically assigned a user_id of 1, and the mary_home record will be assigned
+# user_id 2.  These IDs are automatically filled out for you.
+#
+#
 # Note that YAML fixtures are unordered. If you want ordered fixtures, use the omap YAML type.  See http://yaml.org/type/omap.html
 # for the specification.  You will need ordered fixtures when you have foreign key constraints on keys in the same table.
 # This is commonly needed for tree structures.  Example:
@@ -242,6 +276,29 @@
   cattr_accessor :all_loaded_fixtures
   self.all_loaded_fixtures = {}
 
+  # Parses the loaded fixture hash to find any obvious association names
+  # where the name of the field matches the name of another table.
+  # For example, if the table set contains users and this fixture has
+  # a column user containing a symbol, we attempt to find the id from 
+  # the foreign table and set the column user_id with this value.
+  # Note this is highly opinionated and does not take into account
+  # actual associations from your application.
+  def self.parse_foreign_keys(fixtures, table_names)
+    fixtures.each do |table,fixture_data| # ['table_name' => [fixture_data]]
+      fixture_data.each do |fixture| # ['fixture_name', <fixture object>]
+        fixture[1].to_hash.each do |key,value|
+          # search through the fields in your fixture for matching, singular table names
+          if value.is_a?Symbol and table_names.include?(plur = key.pluralize)
+            if fixtures[plur] and result = fixtures[plur][value.to_s] and id = result.to_hash['id']
+              fixture[1].to_hash[key+"_id"] = id # luckily it's all pass-by-reference here
+              fixture[1].to_hash.delete(key)     # don't need the old key any more
+            end
+          end
+        end
+      end
+    end
+  end
+
   def self.create_fixtures(fixtures_directory, table_names, class_names = {})
     table_names = [table_names].flatten.map { |n| n.to_s }
     connection = block_given? ? yield : ActiveRecord::Base.connection
@@ -253,6 +310,7 @@
       all_loaded_fixtures.merge! fixtures_map  
 
       connection.transaction(Thread.current['open_transactions'] == 0) do
+      self.parse_foreign_keys all_loaded_fixtures, table_names
         fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
         fixtures.each { |fixture| fixture.insert_fixtures }
 

(in reply to: ↑ 1 ) 10/21/06 03:15:37 changed by court3nay

also, patch to db:fixtures:load; load all fixtures in one transaction rather than one per-file. allows post-processing of fixture data and speeds up fixture loading times by about 20 %

{{{ for n=30

c1, original c2, patched

c1 c2 c1/c2

2.324139 2.814425 0.825 }}}

10/21/06 03:17:31 changed by court3nay

  • attachment db_fixture_patch added.

patch for fixtures and rake task

(follow-up: ↓ 4 ) 11/27/06 22:13:00 changed by esad

  • cc set to esad@esse.at.

Any chance this gets commited to edge anytime soon?

(in reply to: ↑ 3 ) 11/29/06 01:18:07 changed by tohagan

Replying to esad:

Any chance this gets commited to edge anytime soon?

I can definitely see a need for a good solution to this but I'm not sure that this approach is the right direction.

A concern I have is that you then can't then have a field value that starts with ":". This could be a problem when fixtures are themselves generated from another arbitrary input source. Basically, it would introduce a syntactic weakness in Ruby's implementation of YAML language.

One (partial) solution might be to modify the field delimiter to indicate that the field value is to be treated differently. This would at least avoid the previous problem:

house.yml

  home:
  id: 5
  user'''::''' :joe

While this previous solution solves today's problem, In the long term you want a more general purpose solution that can use general expressions (not be limited to solving one specific problem)

But wait ... Ruby's YAML already supports this via templates: So I suggest a better approach would be to leverage the existing template features so that the following would work:

house.yml

  home:
  id: 5
  user:  <%= @joe.id %>

So this worked then we could also replace @joe.id with any arbitrary ruby expression.

11/29/06 01:21:56 changed by tohagan

Oops ..

   user'''::''' :joe

was supposed to be

   user:: :joe

03/09/07 01:09:13 changed by nfbuckley

I implemented your suggestion and put the patch in patch #7772

03/09/07 01:18:42 changed by bitsweat

  • cc changed from esad@esse.at to esad@esse.at, bitsweat.
  • keywords set to fixtures foreign key.

(Pointed #7772 back here.)

03/17/07 17:48:19 changed by josh

  • keywords changed from fixtures foreign key to fixtures foreign key verified.

04/07/07 22:02:25 changed by obrie

Thought I'd mention a plugin I just released that is similar to this: http://wiki.pluginaweek.org/Fixture_references. The syntax is not as short, but lets you access fixtures that same way you would in a unit test.

07/23/07 00:36:37 changed by mpalmer

  • keywords changed from fixtures foreign key verified to fixtures foreign key.

I dream about this functionality, but I think the best solution is the syntax provided by the Fixture references plugin. I'm -1 on both the patch here (icky syntax) and the patch in #7772 (again, icky syntax); I'd vote either for the inclusion of Fixture references in core, or just close this ticket and tell people to go use the plugin.

12/17/07 22:32:08 changed by jbarnette

  • status changed from new to closed.
  • resolution set to fixed.

This is pretty obsolete now. I'm resolving as "fixed, more or less".