activerecord 1.9.1 → 1.10.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (68) hide show
  1. data/CHANGELOG +78 -0
  2. data/README +1 -1
  3. data/install.rb +7 -42
  4. data/lib/active_record.rb +2 -0
  5. data/lib/active_record/acts/list.rb +28 -4
  6. data/lib/active_record/acts/nested_set.rb +212 -0
  7. data/lib/active_record/associations.rb +203 -21
  8. data/lib/active_record/associations/association_proxy.rb +10 -2
  9. data/lib/active_record/associations/belongs_to_association.rb +0 -1
  10. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +15 -9
  11. data/lib/active_record/associations/has_many_association.rb +25 -25
  12. data/lib/active_record/associations/has_one_association.rb +2 -2
  13. data/lib/active_record/base.rb +134 -110
  14. data/lib/active_record/connection_adapters/abstract_adapter.rb +9 -9
  15. data/lib/active_record/connection_adapters/mysql_adapter.rb +4 -0
  16. data/lib/active_record/connection_adapters/oci_adapter.rb +2 -2
  17. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +1 -2
  18. data/lib/active_record/deprecated_associations.rb +1 -19
  19. data/lib/active_record/deprecated_finders.rb +41 -0
  20. data/lib/active_record/fixtures.rb +24 -11
  21. data/lib/active_record/observer.rb +17 -11
  22. data/lib/active_record/reflection.rb +5 -1
  23. data/lib/active_record/transactions.rb +7 -0
  24. data/lib/active_record/validations.rb +32 -33
  25. data/rakefile +30 -6
  26. data/test/associations_go_eager_test.rb +55 -0
  27. data/test/associations_test.rb +72 -15
  28. data/test/base_test.rb +15 -21
  29. data/test/deprecated_associations_test.rb +0 -24
  30. data/test/deprecated_finder_test.rb +147 -0
  31. data/test/finder_test.rb +37 -37
  32. data/test/fixtures/author.rb +3 -0
  33. data/test/fixtures/authors.yml +7 -0
  34. data/test/fixtures/categories.yml +7 -0
  35. data/test/fixtures/categories_posts.yml +11 -0
  36. data/test/fixtures/category.rb +3 -0
  37. data/test/fixtures/comment.rb +5 -0
  38. data/test/fixtures/comments.yml +17 -0
  39. data/test/fixtures/company.rb +3 -0
  40. data/test/fixtures/courses.yml +4 -4
  41. data/test/fixtures/db_definitions/db2.drop.sql +6 -0
  42. data/test/fixtures/db_definitions/db2.sql +46 -0
  43. data/test/fixtures/db_definitions/mysql.drop.sql +6 -1
  44. data/test/fixtures/db_definitions/mysql.sql +60 -12
  45. data/test/fixtures/db_definitions/mysql2.sql +1 -1
  46. data/test/fixtures/db_definitions/oci.drop.sql +5 -0
  47. data/test/fixtures/db_definitions/oci.sql +45 -0
  48. data/test/fixtures/db_definitions/postgresql.drop.sql +6 -0
  49. data/test/fixtures/db_definitions/postgresql.sql +45 -0
  50. data/test/fixtures/db_definitions/sqlite.drop.sql +6 -1
  51. data/test/fixtures/db_definitions/sqlite.sql +46 -0
  52. data/test/fixtures/db_definitions/sqlserver.drop.sql +7 -1
  53. data/test/fixtures/db_definitions/sqlserver.sql +46 -0
  54. data/test/fixtures/fk_test_has_fk.yml +3 -0
  55. data/test/fixtures/fk_test_has_pk.yml +2 -0
  56. data/test/fixtures/mixin.rb +18 -0
  57. data/test/fixtures/mixins.yml +30 -0
  58. data/test/fixtures/post.rb +8 -0
  59. data/test/fixtures/posts.yml +20 -0
  60. data/test/fixtures/task.rb +3 -0
  61. data/test/fixtures/tasks.yml +7 -0
  62. data/test/fixtures_test.rb +34 -2
  63. data/test/mixin_nested_set_test.rb +184 -0
  64. data/test/mixin_test.rb +28 -3
  65. data/test/validations_test.rb +16 -0
  66. metadata +21 -5
  67. data/test/fixtures/db_definitions/drop_oracle_tables.sql +0 -35
  68. data/test/fixtures/db_definitions/drop_oracle_tables2.sql +0 -3
data/CHANGELOG CHANGED
@@ -1,3 +1,81 @@
1
+ *1.10.0* (19th April, 2005)
2
+
3
+ * Added eager loading of associations as a way to solve the N+1 problem more gracefully without piggy-back queries. Example:
4
+
5
+ for post in Post.find(:all, :limit => 100)
6
+ puts "Post: " + post.title
7
+ puts "Written by: " + post.author.name
8
+ puts "Last comment on: " + post.comments.first.created_on
9
+ end
10
+
11
+ This used to generate 301 database queries if all 100 posts had both author and comments. It can now be written as:
12
+
13
+ for post in Post.find(:all, :limit => 100, :include => [ :author, :comments ])
14
+
15
+ ...and the number of database queries needed is now 1.
16
+
17
+ * Added new unified Base.find API and deprecated the use of find_first and find_all. See the documentation for Base.find. Examples:
18
+
19
+ Person.find(1, :conditions => "administrator = 1", :order => "created_on DESC")
20
+ Person.find(1, 5, 6, :conditions => "administrator = 1", :order => "created_on DESC")
21
+ Person.find(:first, :order => "created_on DESC", :offset => 5)
22
+ Person.find(:all, :conditions => [ "category IN (?)", categories], :limit => 50)
23
+ Person.find(:all, :offset => 10, :limit => 10)
24
+
25
+ * Added acts_as_nested_set #1000 [wschenk]. Introduction:
26
+
27
+ This acts provides Nested Set functionality. Nested Set is similiar to Tree, but with
28
+ the added feature that you can select the children and all of it's descendants with
29
+ a single query. A good use case for this is a threaded post system, where you want
30
+ to display every reply to a comment without multiple selects.
31
+
32
+ * Added Base.save! that attempts to save the record just like Base.save but will raise a RecordInvalid exception instead of returning false if the record is not valid [After much pestering from Dave Thomas]
33
+
34
+ * Fixed PostgreSQL usage of fixtures with regards to public schemas and table names with dots #962 [gnuman1@gmail.com]
35
+
36
+ * Fixed that fixtures were being deleted in the same order as inserts causing FK errors #890 [andrew.john.peters@gmail.com]
37
+
38
+ * Fixed loading of fixtures in to be in the right order (or PostgreSQL would bark) #1047 [stephenh@chase3000.com]
39
+
40
+ * Fixed page caching for non-vhost applications living underneath the root #1004 [Ben Schumacher]
41
+
42
+ * Fixes a problem with the SQL Adapter which was resulting in IDENTITY_INSERT not being set to ON when it should be #1104 [adelle]
43
+
44
+ * Added the option to specify the acceptance string in validates_acceptance_of #1106 [caleb@aei-tech.com]
45
+
46
+ * Added insert_at(position) to acts_as_list #1083 [DeLynnB]
47
+
48
+ * Removed the default order by id on has_and_belongs_to_many queries as it could kill performance on large sets (you can still specify by hand with :order)
49
+
50
+ * Fixed that Base.silence should restore the old logger level when done, not just set it to DEBUG #1084 [yon@milliped.com]
51
+
52
+ * Fixed boolean saving on Oracle #1093 [mparrish@pearware.org]
53
+
54
+ * Moved build_association and create_association for has_one and belongs_to out of deprecation as they work when the association is nil unlike association.build and association.create, which require the association to be already in place #864
55
+
56
+ * Added rollbacks of transactions if they're active as the dispatcher is killed gracefully (TERM signal) #1054 [Leon Bredt]
57
+
58
+ * Added quoting of column names for fixtures #997 [jcfischer@gmail.com]
59
+
60
+ * Fixed counter_sql when no records exist in database for PostgreSQL (would give error, not 0) #1039 [Caleb Tennis]
61
+
62
+ * Fixed that benchmarking times for rendering included db runtimes #987 [skaes@web.de]
63
+
64
+ * Fixed boolean queries for t/f fields in PostgreSQL #995 [dave@cherryville.org]
65
+
66
+ * Added that model.items.delete(child) will delete the child, not just set the foreign key to nil, if the child is dependent on the model #978 [bitsweat]
67
+
68
+ * Fixed auto-stamping of dates (created_on/updated_on) for PostgreSQL #985 [dave@cherryville.org]
69
+
70
+ * Fixed Base.silence/benchmark to only log if a logger has been configured #986 [skaes@web.de]
71
+
72
+ * Added a join parameter as the third argument to Base.find_first and as the second to Base.count #426, #988 [skaes@web.de]
73
+
74
+ * Fixed bug in Base#hash method that would treat records with the same string-based id as different [Dave Thomas]
75
+
76
+ * Renamed DateHelper#distance_of_time_in_words_to_now to DateHelper#time_ago_in_words (old method name is still available as a deprecated alias)
77
+
78
+
1
79
  *1.9.1* (27th March, 2005)
2
80
 
3
81
  * Fixed that Active Record objects with float attribute could not be cloned #808
data/README CHANGED
@@ -333,7 +333,7 @@ The prefered method of installing Active Record is through its GEM file. You'll
333
333
  RubyGems[http://rubygems.rubyforge.org/wiki/wiki.pl] installed for that, though. If you have,
334
334
  then use:
335
335
 
336
- % [sudo] gem install activerecord-1.7.0.gem
336
+ % [sudo] gem install activerecord-1.10.0.gem
337
337
 
338
338
  You can also install Active Record the old-fashion way with the following command:
339
339
 
data/install.rb CHANGED
@@ -18,48 +18,13 @@ unless $sitedir
18
18
  end
19
19
  end
20
20
 
21
- makedirs = %w{ active_record/associations active_record/connection_adapters active_record/support active_record/vendor active_record/acts }
22
- makedirs.each {|f| File::makedirs(File.join($sitedir, *f.split(/\//)))}
23
-
24
- # deprecated files that should be removed
25
- # deprecated = %w{ }
26
-
27
- # files to install in library path
28
- files = %w-
29
- active_record.rb
30
- active_record/aggregations.rb
31
- active_record/associations.rb
32
- active_record/associations/association_collection.rb
33
- active_record/associations/has_and_belongs_to_many_association.rb
34
- active_record/associations/has_many_association.rb
35
- active_record/base.rb
36
- active_record/callbacks.rb
37
- active_record/connection_adapters/abstract_adapter.rb
38
- active_record/connection_adapters/db2_adapter.rb
39
- active_record/connection_adapters/mysql_adapter.rb
40
- active_record/connection_adapters/oracle_adapter.rb
41
- active_record/connection_adapters/postgresql_adapter.rb
42
- active_record/connection_adapters/sqlite_adapter.rb
43
- active_record/connection_adapters/sqlserver_adapter.rb
44
- active_record/deprecated_associations.rb
45
- active_record/fixtures.rb
46
- active_record/locking.rb
47
- active_record/observer.rb
48
- active_record/reflection.rb
49
- active_record/acts/list.rb
50
- active_record/acts/tree.rb
51
- active_record/timestamp.rb
52
- active_record/transactions.rb
53
- active_record/validations.rb
54
- active_record/vendor/db2.rb
55
- active_record/vendor/mysql.rb
56
- active_record/vendor/mysql411.rb
57
- active_record/vendor/simple.rb
58
- -
59
-
60
21
  # the acual gruntwork
61
22
  Dir.chdir("lib")
62
- # File::safe_unlink *deprecated.collect{|f| File.join($sitedir, f.split(/\//))}
63
- files.each {|f|
64
- File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
23
+
24
+ Find.find("active_record", "active_record.rb") { |f|
25
+ if f[-3..-1] == ".rb"
26
+ File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
27
+ else
28
+ File::makedirs(File.join($sitedir, *f.split(/\//)))
29
+ end
65
30
  }
@@ -43,6 +43,7 @@ require 'active_record/reflection'
43
43
  require 'active_record/timestamp'
44
44
  require 'active_record/acts/list'
45
45
  require 'active_record/acts/tree'
46
+ require 'active_record/acts/nested_set'
46
47
  require 'active_record/locking'
47
48
  require 'active_record/migration'
48
49
 
@@ -57,6 +58,7 @@ ActiveRecord::Base.class_eval do
57
58
  include ActiveRecord::Reflection
58
59
  include ActiveRecord::Acts::Tree
59
60
  include ActiveRecord::Acts::List
61
+ include ActiveRecord::Acts::NestedSet
60
62
  end
61
63
 
62
64
  require 'active_record/connection_adapters/mysql_adapter'
@@ -71,6 +71,10 @@ module ActiveRecord
71
71
  # lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return true if that chapter is
72
72
  # the first in the list of all chapters.
73
73
  module InstanceMethods
74
+ def insert_at(position = 1)
75
+ position == 1 ? add_to_list_top : insert_at_position(position)
76
+ end
77
+
74
78
  def move_lower
75
79
  return unless lower_item
76
80
 
@@ -107,7 +111,6 @@ module ActiveRecord
107
111
  decrement_positions_on_lower_items
108
112
  end
109
113
 
110
-
111
114
  def increment_position
112
115
  update_attribute position_column, self.send(position_column).to_i + 1
113
116
  end
@@ -168,24 +171,45 @@ module ActiveRecord
168
171
  update_attribute(position_column, 1)
169
172
  end
170
173
 
174
+ # This has the effect of moving all the higher items up one.
175
+ def decrement_positions_on_higher_items(position)
176
+ self.class.update_all(
177
+ "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
178
+ )
179
+ end
180
+
171
181
  # This has the effect of moving all the lower items up one.
172
182
  def decrement_positions_on_lower_items
173
183
  self.class.update_all(
174
- "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
184
+ "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
175
185
  )
176
186
  end
177
-
187
+
188
+ # This has the effect of moving all the higher items down one.
178
189
  def increment_positions_on_higher_items
179
190
  self.class.update_all(
180
- "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
191
+ "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
181
192
  )
182
193
  end
183
194
 
195
+ # This has the effect of moving all the lower items down one.
196
+ def increment_positions_on_lower_items(position)
197
+ self.class.update_all(
198
+ "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
199
+ )
200
+ end
201
+
184
202
  def increment_positions_on_all_items
185
203
  self.class.update_all(
186
204
  "#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
187
205
  )
188
206
  end
207
+
208
+ def insert_at_position(position)
209
+ remove_from_list
210
+ increment_positions_on_lower_items(position)
211
+ self.update_attribute(position_column, position)
212
+ end
189
213
  end
190
214
  end
191
215
  end
@@ -0,0 +1,212 @@
1
+ module ActiveRecord
2
+ module Acts #:nodoc:
3
+ module NestedSet #:nodoc:
4
+ def self.append_features(base)
5
+ super
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ # This acts provides Nested Set functionality. Nested Set is similiar to Tree, but with
10
+ # the added feature that you can select the children and all of it's descendants with
11
+ # a single query. A good use case for this is a threaded post system, where you want
12
+ # to display every reply to a comment without multiple selects.
13
+ #
14
+ # A google search for "Nested Set" should point you in the direction to explain the
15
+ # data base theory. I figured a bunch of this from
16
+ # http://threebit.net/tutorials/nestedset/tutorial1.html
17
+ #
18
+ # Instead of picturing a leaf node structure with child pointing back to their parent,
19
+ # the best way to imagine how this works is to think of the parent entity surrounding all
20
+ # of it's children, and it's parent surrounding it, etc. Assuming that they are lined up
21
+ # horizontally, we store the left and right boundries in the database.
22
+ #
23
+ # Imagine:
24
+ # root
25
+ # |_ Child 1
26
+ # |_ Child 1.1
27
+ # |_ Child 1.2
28
+ # |_ Child 2
29
+ # |_ Child 2.1
30
+ # |_ Child 2.2
31
+ #
32
+ # If my cirlces in circles description didn't make sense, check out this sweet
33
+ # ASCII art:
34
+ #
35
+ # ___________________________________________________________________
36
+ # | Root |
37
+ # | ____________________________ ____________________________ |
38
+ # | | Child 1 | | Child 2 | |
39
+ # | | __________ _________ | | __________ _________ | |
40
+ # | | | C 1.1 | | C 1.2 | | | | C 2.1 | | C 2.2 | | |
41
+ # 1 2 3_________4 5________6 7 8 9_________10 11_______12 13 14
42
+ # | |___________________________| |___________________________| |
43
+ # |___________________________________________________________________|
44
+ #
45
+ # The numbers represent the left and right boundries. The table them might
46
+ # look like this:
47
+ # ID | PARENT | LEFT | RIGHT | DATA
48
+ # 1 | 0 | 1 | 14 | root
49
+ # 2 | 1 | 2 | 7 | Child 1
50
+ # 3 | 2 | 3 | 4 | Child 1.1
51
+ # 4 | 2 | 5 | 6 | Child 1.2
52
+ # 5 | 1 | 8 | 13 | Child 2
53
+ # 6 | 5 | 9 | 10 | Child 2.1
54
+ # 7 | 5 | 11 | 12 | Child 2.2
55
+ #
56
+ # So, to get all children of an entry, you
57
+ # SELECT * WHERE CHILD.LEFT IS BETWEEN PARENT.LEFT AND PARENT.RIGHT
58
+ #
59
+ # To get the count, it's (LEFT - RIGHT + 1)/2, etc.
60
+ #
61
+ # To get the direct parent, it falls back to using the PARENT_ID field.
62
+ #
63
+ # There are instance methods for all of these.
64
+ #
65
+ # The structure is good if you need to group things together; the downside is that
66
+ # keeping data integrity is a pain, and both adding and removing and entry
67
+ # require a full table write.
68
+ #
69
+ # This sets up a before_destroy trigger to prune the tree correctly if one of it's
70
+ # elements gets deleted.
71
+ #
72
+ module ClassMethods
73
+ # Configuration options are:
74
+ #
75
+ # * +parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
76
+ # * +left_column+ - column name for left boundry data, default "lft"
77
+ # * +right_column+ - column name for right boundry data, default "rgt"
78
+ # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
79
+ # (if that hasn't been already) and use that as the foreign key restriction. It's also possible
80
+ # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
81
+ # Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
82
+ def acts_as_nested_set(options = {})
83
+ configuration = { :parent_column => "parent_id", :left_column => "lft", :right_column => "rgt", :scope => "1 = 1" }
84
+
85
+ configuration.update(options) if options.is_a?(Hash)
86
+
87
+ configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
88
+
89
+ if configuration[:scope].is_a?(Symbol)
90
+ scope_condition_method = %(
91
+ def scope_condition
92
+ if #{configuration[:scope].to_s}.nil?
93
+ "#{configuration[:scope].to_s} IS NULL"
94
+ else
95
+ "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
96
+ end
97
+ end
98
+ )
99
+ else
100
+ scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
101
+ end
102
+
103
+ class_eval <<-EOV
104
+ include ActiveRecord::Acts::NestedSet::InstanceMethods
105
+
106
+ #{scope_condition_method}
107
+
108
+ def left_col_name() "#{configuration[:left_column]}" end
109
+
110
+ def right_col_name() "#{configuration[:right_column]}" end
111
+
112
+ def parent_column() "#{configuration[:parent_column]}" end
113
+
114
+ EOV
115
+ end
116
+ end
117
+
118
+ module InstanceMethods
119
+ # Returns true is this is a root node.
120
+ def root?
121
+ parent_id = self[parent_column]
122
+ (parent_id == 0 || parent_id.nil?) && (self[left_col_name] == 1) && (self[right_col_name] > self[left_col_name])
123
+ end
124
+
125
+ # Returns true is this is a child node
126
+ def child?
127
+ parent_id = self[parent_column]
128
+ !(parent_id == 0 || parent_id.nil?) && (self[left_col_name] > 1) && (self[right_col_name] > self[left_col_name])
129
+ end
130
+
131
+ # Returns true if we have no idea what this is
132
+ def unknown?
133
+ !root? && !child?
134
+ end
135
+
136
+
137
+ # Added a child to this object in the tree. If this object hasn't been initialized,
138
+ # it gets set up as a root node. Otherwise, this method will update all of the
139
+ # other elements in the tree and shift them to the right. Keeping everything
140
+ # balanaced.
141
+ def add_child( child )
142
+ self.reload
143
+ child.reload
144
+
145
+ if child.root?
146
+ raise "Adding sub-tree isn\'t currently supported"
147
+ else
148
+ if ( (self[left_col_name] == nil) || (self[right_col_name] == nil) )
149
+ # Looks like we're now the root node! Woo
150
+ self[left_col_name] = 1
151
+ self[right_col_name] = 4
152
+
153
+ # What do to do about validation?
154
+ return nil unless self.save
155
+
156
+ child[parent_column] = self.id
157
+ child[left_col_name] = 2
158
+ child[right_col_name]= 3
159
+ return child.save
160
+ else
161
+ # OK, we need to add and shift everything else to the right
162
+ child[parent_column] = self.id
163
+ right_bound = self[right_col_name]
164
+ child[left_col_name] = right_bound
165
+ child[right_col_name] = right_bound + 1
166
+ self[right_col_name] += 2
167
+ self.class.transaction {
168
+ self.class.update_all( "#{left_col_name} = (#{left_col_name} + 2)", "#{scope_condition} AND #{left_col_name} >= #{right_bound}" )
169
+ self.class.update_all( "#{right_col_name} = (#{right_col_name} + 2)", "#{scope_condition} AND #{right_col_name} >= #{right_bound}" )
170
+ self.save
171
+ child.save
172
+ }
173
+ end
174
+ end
175
+ end
176
+
177
+ # Returns the number of nested children of this object.
178
+ def children_count
179
+ return (self[right_col_name] - self[left_col_name] - 1)/2
180
+ end
181
+
182
+ # Returns a set of itself and all of it's nested children
183
+ def full_set
184
+ self.class.find_all( "#{scope_condition} AND (#{left_col_name} BETWEEN #{self[left_col_name]} and #{self[right_col_name]})" )
185
+ end
186
+
187
+ # Returns a set of all of it's children and nested children
188
+ def all_children
189
+ self.class.find_all( "#{scope_condition} AND (#{left_col_name} > #{self[left_col_name]}) and (#{right_col_name} < #{self[right_col_name]})" )
190
+ end
191
+
192
+ # Returns a set of only this entries immediate children
193
+ def direct_children
194
+ self.class.find_all( "#{scope_condition} and #{parent_column} = #{self.id}")
195
+ end
196
+
197
+ # Prunes a branch off of the tree, shifting all of the elements on the right
198
+ # back to the left so the counts still work.
199
+ def before_destroy
200
+ return if self[right_col_name].nil? || self[left_col_name].nil?
201
+ dif = self[right_col_name] - self[left_col_name] + 1
202
+
203
+ self.class.transaction {
204
+ self.class.delete_all( "#{scope_condition} and #{left_col_name} > #{self[left_col_name]} and #{right_col_name} < #{self[right_col_name]}" )
205
+ self.class.update_all( "#{left_col_name} = (#{left_col_name} - #{dif})", "#{scope_condition} AND #{left_col_name} >= #{self[right_col_name]}" )
206
+ self.class.update_all( "#{right_col_name} = (#{right_col_name} - #{dif} )", "#{scope_condition} AND #{right_col_name} >= #{self[right_col_name]}" )
207
+ }
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
@@ -19,7 +19,7 @@ module ActiveRecord
19
19
  instance_variable_set "@#{assoc.name}", nil
20
20
  end
21
21
  end
22
-
22
+
23
23
  # Associations are a set of macro-like class methods for tying objects together through foreign keys. They express relationships like
24
24
  # "Project has one Project Manager" or "Project belongs to a Portfolio". Each macro adds a number of methods to the class which are
25
25
  # specialized according to the collection or association symbol and the options hash. It works much the same was as Ruby's own attr*
@@ -108,6 +108,41 @@ module ActiveRecord
108
108
  # project.milestones(true).size # fetches milestones from the database
109
109
  # project.milestones # uses the milestone cache
110
110
  #
111
+ # == Eager loading of associations
112
+ #
113
+ # Eager loading is a way to find objects of a certain class and a number of named associations along with it in a single SQL call. This is
114
+ # one of the easiest ways of to prevent the dreaded 1+N problem in which fetching 100 posts that each needs to display their author
115
+ # triggers 101 database queries. Through the use of eager loading, the 101 queries can be reduced to 1. Example:
116
+ #
117
+ # class Post < ActiveRecord::Base
118
+ # belongs_to :author
119
+ # has_many :comments
120
+ # end
121
+ #
122
+ # Consider the following loop using the class above:
123
+ #
124
+ # for post in Post.find(:all, :limit => 100)
125
+ # puts "Post: " + post.title
126
+ # puts "Written by: " + post.author.name
127
+ # puts "Last comment on: " + post.comments.first.created_on
128
+ # end
129
+ #
130
+ # To iterate over these one hundred posts, we'll generate 201 database queries. Let's first just optimize it for retrieving the author:
131
+ #
132
+ # for post in Post.find(:all, :limit => 100, :include => :author)
133
+ #
134
+ # This references the name of the belongs_to association that also used the :author symbol, so the find will now weave in a join something
135
+ # like this: LEFT OUTER JOIN authors ON authors.id = posts.author_id. Doing so will cut down the number of queries from 201 to 101.
136
+ #
137
+ # We can improve upon the situation further by referencing both associations in the finder with:
138
+ #
139
+ # for post in Post.find(:all, :limit => 100, :include => [ :author, :comments ])
140
+ #
141
+ # That'll add another join along the lines of: LEFT OUTER JOIN comments ON comments.post_id = posts.id. And we'll be down to 1 query.
142
+ # But that shouldn't fool you to think that you can pull out huge amounts of data with no performance penalty just because you've reduced
143
+ # the number of queries. The database still needs to send all the data to Active Record and it still needs to be processed. So its no
144
+ # catch-all for performance problems, but its a great way to cut down on the number of queries in a situation as the one described above.
145
+ #
111
146
  # == Modules
112
147
  #
113
148
  # By default, associations will look for objects within the current module scope. Consider:
@@ -153,28 +188,27 @@ module ActiveRecord
153
188
  # * <tt>collection(force_reload = false)</tt> - returns an array of all the associated objects.
154
189
  # An empty array is returned if none are found.
155
190
  # * <tt>collection<<(object, ...)</tt> - adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
156
- # * <tt>collection.delete(object, ...)</tt> - removes one or more objects from the collection by setting their foreign keys to NULL. This does not destroy the objects.
191
+ # * <tt>collection.delete(object, ...)</tt> - removes one or more objects from the collection by setting their foreign keys to NULL.
192
+ # This will also destroy the objects if they're declared as belongs_to and dependent on this model.
157
193
  # * <tt>collection.clear</tt> - removes every object from the collection. This does not destroy the objects.
158
194
  # * <tt>collection.empty?</tt> - returns true if there are no associated objects.
159
195
  # * <tt>collection.size</tt> - returns the number of associated objects.
160
- # * <tt>collection.find(id)</tt> - finds an associated object responding to the +id+ and that
161
- # meets the condition that it has to be associated with this object.
162
- # * <tt>collection.find_all(conditions = nil, orderings = nil, limit = nil, joins = nil)</tt> - finds all associated objects responding
163
- # criteria mentioned (like in the standard find_all) and that meets the condition that it has to be associated with this object.
196
+ # * <tt>collection.find</tt> - finds an associated object according to the same rules as Base.find.
164
197
  # * <tt>collection.build(attributes = {})</tt> - returns a new object of the collection type that has been instantiated
165
- # with +attributes+ and linked to this object through a foreign key but has not yet been saved.
198
+ # with +attributes+ and linked to this object through a foreign key but has not yet been saved. *Note:* This only works if an
199
+ # associated object already exists, not if its nil!
166
200
  # * <tt>collection.create(attributes = {})</tt> - returns a new object of the collection type that has been instantiated
167
201
  # with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
202
+ # *Note:* This only works if an associated object already exists, not if its nil!
168
203
  #
169
204
  # Example: A Firm class declares <tt>has_many :clients</tt>, which will add:
170
- # * <tt>Firm#clients</tt> (similar to <tt>Clients.find_all "firm_id = #{id}"</tt>)
205
+ # * <tt>Firm#clients</tt> (similar to <tt>Clients.find :all, :conditions => "firm_id = #{id}"</tt>)
171
206
  # * <tt>Firm#clients<<</tt>
172
207
  # * <tt>Firm#clients.delete</tt>
173
208
  # * <tt>Firm#clients.clear</tt>
174
209
  # * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
175
210
  # * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
176
- # * <tt>Firm#clients.find</tt> (similar to <tt>Client.find_on_conditions(id, "firm_id = #{id}")</tt>)
177
- # * <tt>Firm#clients.find_all</tt> (similar to <tt>Client.find_all "firm_id = #{id}"</tt>)
211
+ # * <tt>Firm#clients.find</tt> (similar to <tt>Client.find(id, :conditions => "firm_id = #{id}")</tt>)
178
212
  # * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
179
213
  # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("client_id" => id); c.save; c</tt>)
180
214
  # The declaration can also include an options hash to specialize the behavior of the association.
@@ -219,6 +253,8 @@ module ActiveRecord
219
253
 
220
254
  if options[:dependent] and options[:exclusively_dependent]
221
255
  raise ArgumentError, ':dependent and :exclusively_dependent are mutually exclusive options. You may specify one or the other.' # ' ruby-mode
256
+ # See HasManyAssociation#delete_records. Dependent associations
257
+ # delete children, otherwise foreign key is set to NULL.
222
258
  elsif options[:dependent]
223
259
  module_eval "before_destroy '#{association_name}.each { |o| o.destroy }'"
224
260
  elsif options[:exclusively_dependent]
@@ -247,19 +283,19 @@ module ActiveRecord
247
283
  # * <tt>association=(associate)</tt> - assigns the associate object, extracts the primary key, sets it as the foreign key,
248
284
  # and saves the associate object.
249
285
  # * <tt>association.nil?</tt> - returns true if there is no associated object.
250
- # * <tt>association.build(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
286
+ # * <tt>build_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
251
287
  # with +attributes+ and linked to this object through a foreign key but has not yet been saved. Note: This ONLY works if
252
288
  # an association already exists. It will NOT work if the association is nil.
253
- # * <tt>association.create(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
289
+ # * <tt>create_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
254
290
  # with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
255
- # Note: This ONLY works if an association already exists. It will NOT work if the association is nil.
256
291
  #
257
292
  # Example: An Account class declares <tt>has_one :beneficiary</tt>, which will add:
258
293
  # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.find_first "account_id = #{id}"</tt>)
259
294
  # * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
260
295
  # * <tt>Account#beneficiary.nil?</tt>
261
- # * <tt>Account#beneficiary.build</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
262
- # * <tt>Account#beneficiary.create</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
296
+ # * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
297
+ # * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
298
+ #
263
299
  # The declaration can also include an options hash to specialize the behavior of the association.
264
300
  #
265
301
  # Options are:
@@ -300,13 +336,13 @@ module ActiveRecord
300
336
  end
301
337
 
302
338
  association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasOneAssociation)
339
+ association_constructor_method(:build, association_name, association_class_name, association_class_primary_key_name, options, HasOneAssociation)
340
+ association_constructor_method(:create, association_name, association_class_name, association_class_primary_key_name, options, HasOneAssociation)
303
341
 
304
342
  module_eval "before_destroy '#{association_name}.destroy unless #{association_name}.nil?'" if options[:dependent]
305
343
 
306
344
  # deprecated api
307
345
  deprecated_has_association_method(association_name)
308
- deprecated_build_method("build_", association_name, association_class_name, association_class_primary_key_name)
309
- deprecated_create_method("create_", association_name, association_class_name, association_class_primary_key_name)
310
346
  deprecated_association_comparison_method(association_name, association_class_name)
311
347
  end
312
348
 
@@ -316,9 +352,9 @@ module ActiveRecord
316
352
  # * <tt>association(force_reload = false)</tt> - returns the associated object. Nil is returned if none is found.
317
353
  # * <tt>association=(associate)</tt> - assigns the associate object, extracts the primary key, and sets it as the foreign key.
318
354
  # * <tt>association.nil?</tt> - returns true if there is no associated object.
319
- # * <tt>association.build(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
355
+ # * <tt>build_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
320
356
  # with +attributes+ and linked to this object through a foreign key but has not yet been saved.
321
- # * <tt>association.create(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
357
+ # * <tt>create_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
322
358
  # with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
323
359
  #
324
360
  # Example: A Post class declares <tt>belongs_to :author</tt>, which will add:
@@ -326,8 +362,8 @@ module ActiveRecord
326
362
  # * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
327
363
  # * <tt>Post#author?</tt> (similar to <tt>post.author == some_author</tt>)
328
364
  # * <tt>Post#author.nil?</tt>
329
- # * <tt>Post#author.build</tt> (similar to <tt>Author.new("post_id" => id)</tt>)
330
- # * <tt>Post#author.create</tt> (similar to <tt>b = Author.new("post_id" => id); b.save; b</tt>)
365
+ # * <tt>Post#build_author</tt> (similar to <tt>Author.new("post_id" => id)</tt>)
366
+ # * <tt>Post#create_author</tt> (similar to <tt>b = Author.new("post_id" => id); b.save; b</tt>)
331
367
  # The declaration can also include an options hash to specialize the behavior of the association.
332
368
  #
333
369
  # Options are:
@@ -362,6 +398,8 @@ module ActiveRecord
362
398
  association_class_primary_key_name = options[:foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name)) + "_id"
363
399
 
364
400
  association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation)
401
+ association_constructor_method(:build, association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation)
402
+ association_constructor_method(:create, association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation)
365
403
 
366
404
  module_eval do
367
405
  before_save <<-EOF
@@ -548,6 +586,15 @@ module ActiveRecord
548
586
  end
549
587
  association
550
588
  end
589
+
590
+ define_method("set_#{association_name}_target") do |target|
591
+ return if target.nil?
592
+ association = association_proxy_class.new(self,
593
+ association_name, association_class_name,
594
+ association_class_primary_key_name, options)
595
+ association.target = target
596
+ instance_variable_set("@#{association_name}", association)
597
+ end
551
598
  end
552
599
 
553
600
  def collection_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, association_proxy_class)
@@ -614,6 +661,141 @@ module ActiveRecord
614
661
  end_eval
615
662
  end
616
663
  end
664
+
665
+ def association_constructor_method(constructor, association_name, association_class_name, association_class_primary_key_name, options, association_proxy_class)
666
+ define_method("#{constructor}_#{association_name}") do |*params|
667
+ attributees = params.first unless params.empty?
668
+ association = instance_variable_get("@#{association_name}")
669
+
670
+ if association.nil?
671
+ association = association_proxy_class.new(self,
672
+ association_name, association_class_name,
673
+ association_class_primary_key_name, options)
674
+ instance_variable_set("@#{association_name}", association)
675
+ end
676
+
677
+ association.send(constructor, attributees)
678
+ end
679
+ end
680
+
681
+ def find_with_associations(options = {})
682
+ reflections = reflect_on_included_associations(options[:include])
683
+ schema_abbreviations = generate_schema_abbreviations(reflections)
684
+ primary_key_table = generate_primary_key_table(reflections, schema_abbreviations)
685
+
686
+ rows = select_all_rows(options, schema_abbreviations, reflections)
687
+ records = { }
688
+ primary_key = primary_key_table[table_name]
689
+
690
+ for row in rows
691
+ id = row[primary_key]
692
+ records[id] ||= instantiate(extract_record(schema_abbreviations, table_name, row))
693
+
694
+ reflections.each do |reflection|
695
+ next unless row[primary_key_table[reflection.table_name]]
696
+
697
+ case reflection.macro
698
+ when :has_many, :has_and_belongs_to_many
699
+ records[id].send(reflection.name)
700
+ records[id].instance_variable_get("@#{reflection.name}").target.push(
701
+ reflection.klass.send(:instantiate, extract_record(schema_abbreviations, reflection.table_name, row))
702
+ )
703
+ when :has_one, :belongs_to
704
+ records[id].send(
705
+ "#{reflection.name}=",
706
+ reflection.klass.send(:instantiate, extract_record(schema_abbreviations, reflection.table_name, row))
707
+ )
708
+ end
709
+ end
710
+ end
711
+
712
+ return records.values
713
+ end
714
+
715
+ def reflect_on_included_associations(associations)
716
+ [ associations ].flatten.collect { |association| reflect_on_association(association) }
717
+ end
718
+
719
+ def generate_schema_abbreviations(reflections)
720
+ schema = [ [ table_name, column_names ] ]
721
+ schema += reflections.collect { |r| [ r.table_name, r.klass.column_names ] }
722
+
723
+ schema_abbreviations = {}
724
+ schema.each_with_index do |table_and_columns, i|
725
+ table, columns = table_and_columns
726
+ columns.each_with_index { |column, j| schema_abbreviations["t#{i}_r#{j}"] = [ table, column ] }
727
+ end
728
+
729
+ return schema_abbreviations
730
+ end
731
+
732
+ def generate_primary_key_table(reflections, schema_abbreviations)
733
+ primary_key_lookup_table = {}
734
+ primary_key_lookup_table[table_name] =
735
+ schema_abbreviations.find { |cn, tc| tc == [ table_name, primary_key ] }.first
736
+
737
+ reflections.collect do |reflection|
738
+ primary_key_lookup_table[reflection.klass.table_name] = schema_abbreviations.find { |cn, tc|
739
+ tc == [ reflection.klass.table_name, reflection.klass.primary_key ]
740
+ }.first
741
+ end
742
+
743
+ return primary_key_lookup_table
744
+ end
745
+
746
+
747
+ def select_all_rows(options, schema_abbreviations, reflections)
748
+ connection.select_all(
749
+ construct_finder_sql_with_included_associations(options, schema_abbreviations, reflections),
750
+ "#{name} Load Including Associations"
751
+ )
752
+ end
753
+
754
+ def construct_finder_sql_with_included_associations(options, schema_abbreviations, reflections)
755
+ sql = "SELECT #{column_aliases(schema_abbreviations)} FROM #{table_name} "
756
+ sql << reflections.collect { |reflection| association_join(reflection) }.to_s
757
+ sql << "#{options[:joins]} " if options[:joins]
758
+ add_conditions!(sql, options[:conditions])
759
+ sql << "ORDER BY #{options[:order]} " if options[:order]
760
+
761
+ return sanitize_sql(sql)
762
+ end
763
+
764
+ def column_aliases(schema_abbreviations)
765
+ schema_abbreviations.collect { |cn, tc| "#{tc.join(".")} AS #{cn}" }.join(", ")
766
+ end
767
+
768
+ def association_join(reflection)
769
+ case reflection.macro
770
+ when :has_and_belongs_to_many
771
+ " LEFT OUTER JOIN #{reflection.options[:join_table]} ON " +
772
+ "#{reflection.options[:join_table]}.#{reflection.options[:foreign_key] || table_name.classify.foreign_key} = " +
773
+ "#{table_name}.#{primary_key} " +
774
+ " LEFT OUTER JOIN #{reflection.klass.table_name} ON " +
775
+ "#{reflection.options[:join_table]}.#{reflection.options[:associated_foreign_key] || reflection.klass.table_name.classify.foreign_key} = " +
776
+ "#{reflection.klass.table_name}.#{reflection.klass.primary_key} "
777
+ when :has_many, :has_one
778
+ " LEFT OUTER JOIN #{reflection.klass.table_name} ON " +
779
+ "#{reflection.klass.table_name}.#{reflection.options[:foreign_key] || table_name.classify.foreign_key} = " +
780
+ "#{table_name}.#{primary_key} "
781
+ when :belongs_to
782
+ " LEFT OUTER JOIN #{reflection.klass.table_name} ON " +
783
+ "#{reflection.klass.table_name}.#{reflection.klass.primary_key} = " +
784
+ "#{table_name}.#{reflection.options[:foreign_key] || reflection.klass.table_name.classify.foreign_key} "
785
+ else
786
+ ""
787
+ end
788
+ end
789
+
790
+
791
+ def extract_record(schema_abbreviations, table_name, row)
792
+ record = {}
793
+ row.each do |column, value|
794
+ prefix, column_name = schema_abbreviations[column]
795
+ record[column_name] = value if prefix == table_name
796
+ end
797
+ return record
798
+ end
617
799
  end
618
800
  end
619
801
  end