activerecord 1.0.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 (106) hide show
  1. data/CHANGELOG +581 -0
  2. data/README +361 -0
  3. data/RUNNING_UNIT_TESTS +36 -0
  4. data/dev-utils/eval_debugger.rb +9 -0
  5. data/examples/associations.png +0 -0
  6. data/examples/associations.rb +87 -0
  7. data/examples/shared_setup.rb +15 -0
  8. data/examples/validation.rb +88 -0
  9. data/install.rb +60 -0
  10. data/lib/active_record.rb +48 -0
  11. data/lib/active_record/aggregations.rb +165 -0
  12. data/lib/active_record/associations.rb +536 -0
  13. data/lib/active_record/associations/association_collection.rb +70 -0
  14. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +46 -0
  15. data/lib/active_record/associations/has_many_association.rb +104 -0
  16. data/lib/active_record/base.rb +985 -0
  17. data/lib/active_record/callbacks.rb +337 -0
  18. data/lib/active_record/connection_adapters/abstract_adapter.rb +326 -0
  19. data/lib/active_record/connection_adapters/mysql_adapter.rb +131 -0
  20. data/lib/active_record/connection_adapters/postgresql_adapter.rb +177 -0
  21. data/lib/active_record/connection_adapters/sqlite_adapter.rb +107 -0
  22. data/lib/active_record/deprecated_associations.rb +70 -0
  23. data/lib/active_record/fixtures.rb +172 -0
  24. data/lib/active_record/observer.rb +71 -0
  25. data/lib/active_record/reflection.rb +126 -0
  26. data/lib/active_record/support/class_attribute_accessors.rb +43 -0
  27. data/lib/active_record/support/class_inheritable_attributes.rb +37 -0
  28. data/lib/active_record/support/clean_logger.rb +10 -0
  29. data/lib/active_record/support/inflector.rb +70 -0
  30. data/lib/active_record/transactions.rb +102 -0
  31. data/lib/active_record/validations.rb +205 -0
  32. data/lib/active_record/vendor/mysql.rb +1117 -0
  33. data/lib/active_record/vendor/simple.rb +702 -0
  34. data/lib/active_record/wrappers/yaml_wrapper.rb +15 -0
  35. data/lib/active_record/wrappings.rb +59 -0
  36. data/rakefile +122 -0
  37. data/test/abstract_unit.rb +16 -0
  38. data/test/aggregations_test.rb +34 -0
  39. data/test/all.sh +8 -0
  40. data/test/associations_test.rb +477 -0
  41. data/test/base_test.rb +513 -0
  42. data/test/class_inheritable_attributes_test.rb +33 -0
  43. data/test/connections/native_mysql/connection.rb +24 -0
  44. data/test/connections/native_postgresql/connection.rb +24 -0
  45. data/test/connections/native_sqlite/connection.rb +24 -0
  46. data/test/deprecated_associations_test.rb +336 -0
  47. data/test/finder_test.rb +67 -0
  48. data/test/fixtures/accounts/signals37 +3 -0
  49. data/test/fixtures/accounts/unknown +2 -0
  50. data/test/fixtures/auto_id.rb +4 -0
  51. data/test/fixtures/column_name.rb +3 -0
  52. data/test/fixtures/companies/first_client +6 -0
  53. data/test/fixtures/companies/first_firm +4 -0
  54. data/test/fixtures/companies/second_client +6 -0
  55. data/test/fixtures/company.rb +37 -0
  56. data/test/fixtures/company_in_module.rb +33 -0
  57. data/test/fixtures/course.rb +3 -0
  58. data/test/fixtures/courses/java +2 -0
  59. data/test/fixtures/courses/ruby +2 -0
  60. data/test/fixtures/customer.rb +30 -0
  61. data/test/fixtures/customers/david +6 -0
  62. data/test/fixtures/db_definitions/mysql.sql +96 -0
  63. data/test/fixtures/db_definitions/mysql2.sql +4 -0
  64. data/test/fixtures/db_definitions/postgresql.sql +113 -0
  65. data/test/fixtures/db_definitions/postgresql2.sql +4 -0
  66. data/test/fixtures/db_definitions/sqlite.sql +85 -0
  67. data/test/fixtures/db_definitions/sqlite2.sql +4 -0
  68. data/test/fixtures/default.rb +2 -0
  69. data/test/fixtures/developer.rb +8 -0
  70. data/test/fixtures/developers/david +2 -0
  71. data/test/fixtures/developers/jamis +2 -0
  72. data/test/fixtures/developers_projects/david_action_controller +2 -0
  73. data/test/fixtures/developers_projects/david_active_record +2 -0
  74. data/test/fixtures/developers_projects/jamis_active_record +2 -0
  75. data/test/fixtures/entrant.rb +3 -0
  76. data/test/fixtures/entrants/first +3 -0
  77. data/test/fixtures/entrants/second +3 -0
  78. data/test/fixtures/entrants/third +3 -0
  79. data/test/fixtures/fixture_database.sqlite +0 -0
  80. data/test/fixtures/fixture_database_2.sqlite +0 -0
  81. data/test/fixtures/movie.rb +5 -0
  82. data/test/fixtures/movies/first +2 -0
  83. data/test/fixtures/movies/second +2 -0
  84. data/test/fixtures/project.rb +3 -0
  85. data/test/fixtures/projects/action_controller +2 -0
  86. data/test/fixtures/projects/active_record +2 -0
  87. data/test/fixtures/reply.rb +21 -0
  88. data/test/fixtures/subscriber.rb +5 -0
  89. data/test/fixtures/subscribers/first +2 -0
  90. data/test/fixtures/subscribers/second +2 -0
  91. data/test/fixtures/topic.rb +20 -0
  92. data/test/fixtures/topics/first +9 -0
  93. data/test/fixtures/topics/second +8 -0
  94. data/test/fixtures_test.rb +20 -0
  95. data/test/inflector_test.rb +104 -0
  96. data/test/inheritance_test.rb +125 -0
  97. data/test/lifecycle_test.rb +110 -0
  98. data/test/modules_test.rb +21 -0
  99. data/test/multiple_db_test.rb +46 -0
  100. data/test/pk_test.rb +57 -0
  101. data/test/reflection_test.rb +78 -0
  102. data/test/thread_safety_test.rb +33 -0
  103. data/test/transactions_test.rb +83 -0
  104. data/test/unconnected_test.rb +24 -0
  105. data/test/validations_test.rb +126 -0
  106. metadata +166 -0
@@ -0,0 +1,15 @@
1
+ # Be sure to change the mysql_connection details and create a database for the example
2
+
3
+ $: << File.dirname(__FILE__) + '/../lib'
4
+
5
+ require 'active_record'
6
+ require 'logger'; class Logger; def format_message(severity, timestamp, msg, progname) "#{msg}\n" end; end
7
+
8
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
9
+ ActiveRecord::Base.establish_connection(
10
+ :adapter => "mysql",
11
+ :host => "localhost",
12
+ :username => "root",
13
+ :password => "",
14
+ :database => "activerecord_examples"
15
+ )
@@ -0,0 +1,88 @@
1
+ require File.dirname(__FILE__) + '/shared_setup'
2
+
3
+ logger = Logger.new(STDOUT)
4
+
5
+ # Database setup ---------------
6
+
7
+ logger.info "\nCreate tables"
8
+
9
+ [ "DROP TABLE people",
10
+ "CREATE TABLE people (id int(11) auto_increment, name varchar(100), pass varchar(100), email varchar(100), PRIMARY KEY (id))"
11
+ ].each { |statement|
12
+ begin; ActiveRecord::Base.connection.execute(statement); rescue ActiveRecord::StatementInvalid; end # Tables doesn't necessarily already exist
13
+ }
14
+
15
+
16
+ # Class setup ---------------
17
+
18
+ class Person < ActiveRecord::Base
19
+ # Active Record can only guess simple table names like Card/cards, Company/companies
20
+ def self.table_name() "people" end
21
+
22
+ # Using
23
+ def self.authenticate(name, pass)
24
+ # find_first "name = '#{name}' AND pass = '#{pass}'" would be open to sql-injection (in a web-app scenario)
25
+ find_first [ "name = '%s' AND pass = '%s'", name, pass ]
26
+ end
27
+
28
+ def self.name_exists?(name, id = nil)
29
+ if id.nil?
30
+ condition = [ "name = '%s'", name ]
31
+ else
32
+ # Check if anyone else than the person identified by person_id has that user_name
33
+ condition = [ "name = '%s' AND id <> %d", name, id ]
34
+ end
35
+
36
+ !find_first(condition).nil?
37
+ end
38
+
39
+ def email_address_with_name
40
+ "\"#{name}\" <#{email}>"
41
+ end
42
+
43
+ protected
44
+ def validate
45
+ errors.add_on_empty(%w(name pass email))
46
+ errors.add("email", "must be valid") unless email_address_valid?
47
+ end
48
+
49
+ def validate_on_create
50
+ if attribute_present?("name") && Person.name_exists?(name)
51
+ errors.add("name", "is already taken by another person")
52
+ end
53
+ end
54
+
55
+ def validate_on_update
56
+ if attribute_present?("name") && Person.name_exists?(name, id)
57
+ errors.add("name", "is already taken by another person")
58
+ end
59
+ end
60
+
61
+ private
62
+ def email_address_valid?() email =~ /\w[-.\w]*\@[-\w]+[-.\w]*\.\w+/ end
63
+ end
64
+
65
+ # Usage ---------------
66
+
67
+ logger.info "\nCreate fixtures"
68
+ david = Person.new("name" => "David Heinemeier Hansson", "pass" => "", "email" => "")
69
+ unless david.save
70
+ puts "There was #{david.errors.count} error(s)"
71
+ david.errors.each_full { |error| puts error }
72
+ end
73
+
74
+ david.pass = "something"
75
+ david.email = "invalid_address"
76
+ unless david.save
77
+ puts "There was #{david.errors.count} error(s)"
78
+ puts "It was email with: " + david.errors.on("email")
79
+ end
80
+
81
+ david.email = "david@loudthinking.com"
82
+ if david.save then puts "David finally made it!" end
83
+
84
+
85
+ another_david = Person.new("name" => "David Heinemeier Hansson", "pass" => "xc", "email" => "david@loudthinking")
86
+ unless another_david.save
87
+ puts "Error on name: " + another_david.errors.on("name")
88
+ end
@@ -0,0 +1,60 @@
1
+ require 'rbconfig'
2
+ require 'find'
3
+ require 'ftools'
4
+
5
+ include Config
6
+
7
+ # this was adapted from rdoc's install.rb by ways of Log4r
8
+
9
+ $sitedir = CONFIG["sitelibdir"]
10
+ unless $sitedir
11
+ version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]
12
+ $libdir = File.join(CONFIG["libdir"], "ruby", version)
13
+ $sitedir = $:.find {|x| x =~ /site_ruby/ }
14
+ if !$sitedir
15
+ $sitedir = File.join($libdir, "site_ruby")
16
+ elsif $sitedir !~ Regexp.quote(version)
17
+ $sitedir = File.join($sitedir, version)
18
+ end
19
+ end
20
+
21
+ makedirs = %w{ active_record/associations active_record/connection_adapters active_record/support active_record/vendor }
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/mysql_adapter.rb
39
+ active_record/connection_adapters/postgresql_adapter.rb
40
+ active_record/connection_adapters/sqlite_adapter.rb
41
+ active_record/deprecated_associations.rb
42
+ active_record/fixtures.rb
43
+ active_record/observer.rb
44
+ active_record/reflection.rb
45
+ active_record/support/class_attribute_accessors.rb
46
+ active_record/support/class_inheritable_attributes.rb
47
+ active_record/support/clean_logger.rb
48
+ active_record/support/inflector.rb
49
+ active_record/transactions.rb
50
+ active_record/validations.rb
51
+ active_record/vendor/mysql.rb
52
+ active_record/vendor/simple.rb
53
+ -
54
+
55
+ # the acual gruntwork
56
+ Dir.chdir("lib")
57
+ # File::safe_unlink *deprecated.collect{|f| File.join($sitedir, f.split(/\//))}
58
+ files.each {|f|
59
+ File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
60
+ }
@@ -0,0 +1,48 @@
1
+ #--
2
+ # Copyright (c) 2004 David Heinemeier Hansson
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ $:.unshift(File.dirname(__FILE__))
25
+
26
+ require 'active_record/support/clean_logger'
27
+
28
+ require 'active_record/base'
29
+ require 'active_record/observer'
30
+ require 'active_record/validations'
31
+ require 'active_record/callbacks'
32
+ require 'active_record/associations'
33
+ require 'active_record/aggregations'
34
+ require 'active_record/transactions'
35
+ require 'active_record/reflection'
36
+
37
+ ActiveRecord::Base.class_eval do
38
+ include ActiveRecord::Validations
39
+ include ActiveRecord::Callbacks
40
+ include ActiveRecord::Associations
41
+ include ActiveRecord::Aggregations
42
+ include ActiveRecord::Transactions
43
+ include ActiveRecord::Reflection
44
+ end
45
+
46
+ require 'active_record/connection_adapters/mysql_adapter'
47
+ require 'active_record/connection_adapters/postgresql_adapter'
48
+ require 'active_record/connection_adapters/sqlite_adapter'
@@ -0,0 +1,165 @@
1
+ module ActiveRecord
2
+ module Aggregations # :nodoc:
3
+ def self.append_features(base)
4
+ super
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ # Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes
9
+ # as value objects. It expresses relationships like "Account [is] composed of Money [among other things]" or "Person [is]
10
+ # composed of [an] address". Each call to the macro adds a description on how the value objects are created from the
11
+ # attributes of the entity object (when the entity is initialized either as a new object or from finding an existing)
12
+ # and how it can be turned back into attributes (when the entity is saved to the database). Example:
13
+ #
14
+ # class Customer < ActiveRecord::Base
15
+ # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
16
+ # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
17
+ # end
18
+ #
19
+ # The customer class now has the following methods to manipulate the value objects:
20
+ # * <tt>Customer#balance, Customer#balance=(money)</tt>
21
+ # * <tt>Customer#address, Customer#address=(address)</tt>
22
+ #
23
+ # These methods will operate with value objects like the ones described below:
24
+ #
25
+ # class Money
26
+ # include Comparable
27
+ # attr_reader :amount, :currency
28
+ # EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
29
+ #
30
+ # def initialize(amount, currency = "USD")
31
+ # @amount, @currency = amount, currency
32
+ # end
33
+ #
34
+ # def exchange_to(other_currency)
35
+ # exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor
36
+ # Money.new(exchanged_amount, other_currency)
37
+ # end
38
+ #
39
+ # def ==(other_money)
40
+ # amount == other_money.amount && currency == other_money.currency
41
+ # end
42
+ #
43
+ # def <=>(other_money)
44
+ # if currency == other_money.currency
45
+ # among <=> amount
46
+ # else
47
+ # amount <=> other_money.exchange_to(currency).amount
48
+ # end
49
+ # end
50
+ # end
51
+ #
52
+ # class Address
53
+ # attr_reader :street, :city
54
+ # def initialize(street, city)
55
+ # @street, @city = street, city
56
+ # end
57
+ #
58
+ # def close_to?(other_address)
59
+ # city == other_address.city
60
+ # end
61
+ #
62
+ # def ==(other_address)
63
+ # city == other_address.city && street == other_address.street
64
+ # end
65
+ # end
66
+ #
67
+ # Now it's possible to access attributes from the database through the value objects instead. If you choose to name the
68
+ # composition the same as the attributes name, it will be the only way to access that attribute. That's the case with our
69
+ # +balance+ attribute. You interact with the value objects just like you would any other attribute, though:
70
+ #
71
+ # customer.balance = Money.new(20) # sets the Money value object and the attribute
72
+ # customer.balance # => Money value object
73
+ # customer.balance.exchanged_to("DKK") # => Money.new(120, "DKK")
74
+ # customer.balance > Money.new(10) # => true
75
+ # customer.balance == Money.new(20) # => true
76
+ # customer.balance < Money.new(5) # => false
77
+ #
78
+ # Value objects can also be composed of multiple attributes, such as the case of Address. The order of the mappings will
79
+ # determine the order of the parameters. Example:
80
+ #
81
+ # customer.address_street = "Hyancintvej"
82
+ # customer.address_city = "Copenhagen"
83
+ # customer.address # => Address.new("Hyancintvej", "Copenhagen")
84
+ # customer.address = Address.new("May Street", "Chicago")
85
+ # customer.address_street # => "May Street"
86
+ # customer.address_city # => "Chicago"
87
+ #
88
+ # == Writing value objects
89
+ #
90
+ # Value objects are immutable and interchangeable objects that represent a given value, such as a Money object representing
91
+ # $5. Two Money objects both representing $5 should be equal (through methods such == and <=> from Comparable if ranking makes
92
+ # sense). This is unlike a entity objects where equality is determined by identity. An entity class such as Customer can
93
+ # easily have two different objects that both have an address on Hyancintvej. Entity identity is determined by object or
94
+ # relational unique identifiers (such as primary keys). Normal ActiveRecord::Base classes are entity objects.
95
+ #
96
+ # It's also important to treat the value objects as immutable. Don't allow the Money object to have its amount changed after
97
+ # creation. Create a new money object with the new value instead. This is examplified by the Money#exchanged_to method that
98
+ # returns a new value object instead of changing its own values. Active Record won't persist value objects that have been
99
+ # changed through other means than the writer method.
100
+ #
101
+ # The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to
102
+ # change it afterwards will result in a TypeError.
103
+ #
104
+ # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not keeping value objects
105
+ # immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
106
+ module ClassMethods
107
+ # Adds the a reader and writer method for manipulating a value object, so
108
+ # <tt>composed_of :address</tt> would add <tt>address</tt> and <tt>address=(new_address)</tt>.
109
+ #
110
+ # Options are:
111
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
112
+ # from the part id. So <tt>composed_of :address</tt> will by default be linked to the +Address+ class, but
113
+ # if the real class name is +CompanyAddress+, you'll have to specify it with this option.
114
+ # * <tt>:mapping</tt> - specifies a number of mapping arrays (attribute, parameter) that bind an attribute name
115
+ # to a constructor parameter on the value class.
116
+ #
117
+ # Option examples:
118
+ # composed_of :temperature, :mapping => %w(reading celsius)
119
+ # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
120
+ # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
121
+ def composed_of(part_id, options = {})
122
+ validate_options([ :class_name, :mapping ], options.keys)
123
+
124
+ name = part_id.id2name
125
+ class_name = options[:class_name] || name_to_class_name(name)
126
+ mapping = options[:mapping]
127
+
128
+ reader_method(name, class_name, mapping)
129
+ writer_method(name, class_name, mapping)
130
+ end
131
+
132
+ private
133
+ # Raises an exception if an invalid option has been specified to prevent misspellings from slipping through
134
+ def validate_options(valid_option_keys, supplied_option_keys)
135
+ unknown_option_keys = supplied_option_keys - valid_option_keys
136
+ raise(ActiveRecordError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty?
137
+ end
138
+
139
+ def name_to_class_name(name)
140
+ name.capitalize.gsub(/_(.)/) { |s| $1.capitalize }
141
+ end
142
+
143
+ def reader_method(name, class_name, mapping)
144
+ module_eval <<-end_eval
145
+ def #{name}(force_reload = false)
146
+ if @#{name}.nil? || force_reload
147
+ @#{name} = #{class_name}.new(#{(Array === mapping.first ? mapping : [ mapping ]).collect{ |pair| "read_attribute(\"#{pair.first}\")"}.join(", ")})
148
+ end
149
+
150
+ return @#{name}
151
+ end
152
+ end_eval
153
+ end
154
+
155
+ def writer_method(name, class_name, mapping)
156
+ module_eval <<-end_eval
157
+ def #{name}=(part)
158
+ @#{name} = part.freeze
159
+ #{(Array === mapping.first ? mapping : [ mapping ]).collect{ |pair| "@attributes[\"#{pair.first}\"] = part.#{pair.last}" }.join("\n")}
160
+ end
161
+ end_eval
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,536 @@
1
+ require 'active_record/associations/association_collection'
2
+ require 'active_record/associations/has_many_association'
3
+ require 'active_record/associations/has_and_belongs_to_many_association'
4
+ require 'active_record/deprecated_associations'
5
+
6
+ module ActiveRecord
7
+ module Associations # :nodoc:
8
+ def self.append_features(base)
9
+ super
10
+ base.extend(ClassMethods)
11
+ end
12
+
13
+ # Associations are a set of macro-like class methods for tying objects together through foreign keys. They express relationships like
14
+ # "Project has one Project Manager" or "Project belongs to a Portfolio". Each macro adds a number of methods to the class which are
15
+ # specialized according to the collection or association symbol and the options hash. It works much the same was as Ruby's own attr*
16
+ # methods. Example:
17
+ #
18
+ # class Project < ActiveRecord::Base
19
+ # belongs_to :portfolio
20
+ # has_one :project_manager
21
+ # has_many :milestones
22
+ # has_and_belongs_to_many :categories
23
+ # end
24
+ #
25
+ # The project class now has the following methods to ease the traversel and manipulation of its relationships:
26
+ # * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?, Project#portfolio?(portfolio)</tt>
27
+ # * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manger.nil?,</tt>
28
+ # <tt>Project#project_manager?(project_manager), Project#build_project_manager, Project#create_project_manager</tt>
29
+ # * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt>
30
+ # <tt>Project#milestones.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.find_all(conditions),</tt>
31
+ # <tt>Project#milestones.build, Project#milestones.create</tt>
32
+ # * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt>
33
+ # <tt>Project#categories.delete(category1)</tt>
34
+ #
35
+ # == Example
36
+ #
37
+ # link:../examples/associations.png
38
+ #
39
+ # == Is it belongs_to or has_one?
40
+ #
41
+ # Both express a 1-1 relationship, the difference is mostly where to place the foreign key, which goes on the table for the class
42
+ # saying belongs_to. Example:
43
+ #
44
+ # class Post < ActiveRecord::Base
45
+ # has_one :author
46
+ # end
47
+ #
48
+ # class Author < ActiveRecord::Base
49
+ # belongs_to :post
50
+ # end
51
+ #
52
+ # The tables for these classes could look something like:
53
+ #
54
+ # CREATE TABLE posts (
55
+ # id int(11) NOT NULL auto_increment,
56
+ # title varchar default NULL,
57
+ # PRIMARY KEY (id)
58
+ # )
59
+ #
60
+ # CREATE TABLE authors (
61
+ # id int(11) NOT NULL auto_increment,
62
+ # post_id int(11) default NULL,
63
+ # name varchar default NULL,
64
+ # PRIMARY KEY (id)
65
+ # )
66
+ #
67
+ # == Caching
68
+ #
69
+ # All of the methods are built on a simple caching principle that will keep the result of the last query around unless specifically
70
+ # instructed not to. The cache is even shared across methods to make it even cheaper to use the macro-added methods without
71
+ # worrying too much about performance at the first go. Example:
72
+ #
73
+ # project.milestones # fetches milestones from the database
74
+ # project.milestones.size # uses the milestone cache
75
+ # project.milestones.empty? # uses the milestone cache
76
+ # project.milestones(true).size # fetches milestones from the database
77
+ # project.milestones # uses the milestone cache
78
+ #
79
+ # == Modules
80
+ #
81
+ # By default, associations will look for objects within the current module scope. Consider:
82
+ #
83
+ # module MyApplication
84
+ # module Business
85
+ # class Firm < ActiveRecord::Base
86
+ # has_many :clients
87
+ # end
88
+ #
89
+ # class Company < ActiveRecord::Base; end
90
+ # end
91
+ # end
92
+ #
93
+ # When Firm#clients is called, it'll in turn call <tt>MyApplication::Business::Company.find(firm.id)</tt>. If you want to associate
94
+ # with a class in another module scope this can be done by specifying the complete class name, such as:
95
+ #
96
+ # module MyApplication
97
+ # module Business
98
+ # class Firm < ActiveRecord::Base; end
99
+ # end
100
+ #
101
+ # module Billing
102
+ # class Account < ActiveRecord::Base
103
+ # belongs_to :firm, :class_name => "MyApplication::Business::Firm"
104
+ # end
105
+ # end
106
+ # end
107
+ #
108
+ # == Type safety with ActiveRecord::AssociationTypeMismatch
109
+ #
110
+ # If you attempt to assign an object to an association that doesn't match the inferred or specified <tt>:class_name</tt>, you'll
111
+ # get a ActiveRecord::AssociationTypeMismatch.
112
+ #
113
+ # == Options
114
+ #
115
+ # All of the association macros can be specialized through options which makes more complex cases than the simple and guessable ones
116
+ # possible.
117
+ module ClassMethods
118
+ # Adds the following methods for retrival and query of collections of associated objects.
119
+ # +collection+ is replaced with the symbol passed as the first argument, so
120
+ # <tt>has_many :clients</tt> would add among others <tt>has_clients?</tt>.
121
+ # * <tt>collection(force_reload = false)</tt> - returns an array of all the associated objects.
122
+ # An empty array is returned if none is found.
123
+ # * <tt>collection<<(object)</tt> - adds the object to the collection (by setting the foreign key on it) and saves it.
124
+ # * <tt>collection.delete(object)</tt> - removes the association by setting the foreign key to null on the associated object.
125
+ # * <tt>!collection.empty?</tt> - returns true if there's any associated objects.
126
+ # * <tt>collection.size</tt> - returns the number of associated objects.
127
+ # * <tt>collection.find(id)</tt> - finds an associated object responding to the +id+ and that
128
+ # meets the condition that it has to be associated with this object.
129
+ # * <tt>collection.find_all(conditions = nil, orderings = nil, limit = nil, joins = nil)</tt> - finds all associated objects responding
130
+ # criterias mentioned (like in the standard find_all) and that meets the condition that it has to be associated with this object.
131
+ # * <tt>collection.build(attributes = {})</tt> - returns a new object of the collection type that has been instantiated
132
+ # with +attributes+ and linked to this object through a foreign key but has not yet been saved.
133
+ # * <tt>collection.create(attributes = {})</tt> - returns a new object of the collection type that has been instantiated
134
+ # with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
135
+ #
136
+ # Example: A Firm class declares <tt>has_many :clients</tt>, which will add:
137
+ # * <tt>Firm#clients</tt> (similar to <tt>Clients.find_all "firm_id = #{id}"</tt>)
138
+ # * <tt>Firm#clients<<</tt>
139
+ # * <tt>Firm#clients.delete</tt>
140
+ # * <tt>!Firm#clients.empty?</tt> (similar to <tt>firm.clients.length > 0</tt>)
141
+ # * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
142
+ # * <tt>Firm#clients.find</tt> (similar to <tt>Client.find_on_conditions(id, "firm_id = #{id}")</tt>)
143
+ # * <tt>Firm#clients.find_all</tt> (similar to <tt>Client.find_all "firm_id = #{id}"</tt>)
144
+ # * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
145
+ # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("client_id" => id); c.save; c</tt>)
146
+ # The declaration can also include an options hash to specialize the generated methods.
147
+ #
148
+ # Options are:
149
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
150
+ # from the association name. So <tt>has_many :products</tt> will by default be linked to the +Product+ class, but
151
+ # if the real class name is +SpecialProduct+, you'll have to specify it with this option.
152
+ # * <tt>:conditions</tt> - specify the conditions that the associated objects must meet in order to be included as a "WHERE"
153
+ # sql fragment, such as "price > 5 AND name LIKE 'B%'".
154
+ # * <tt>:order</tt> - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment,
155
+ # such as "last_name, first_name DESC"
156
+ # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
157
+ # of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_many association will use "person_id"
158
+ # as the default foreign_key.
159
+ # * <tt>:dependent</tt> - if set to true all the associated object are destroyed alongside this object
160
+ # * <tt>:exclusively_dependent</tt> - if set to true all the associated object are deleted in one SQL statement without having their
161
+ # before_destroy callback run. This should only be used on associations that depend solely on this class and don't need to do any
162
+ # clean-up in before_destroy. The upside is that it's much faster, especially if there's a counter_cache involved.
163
+ # * <tt>:finder_sql</tt> - specify a complete SQL statement to fetch the association. This is a good way to go for complex
164
+ # associations that depends on multiple tables. Note: When this option is used, +find_in_collection+ is _not_ added.
165
+ #
166
+ # Option examples:
167
+ # has_many :comments, :order => "posted_on"
168
+ # has_many :people, :class_name => "Person", :conditions => "deleted = 0", :order => "name"
169
+ # has_many :tracks, :order => "position", :dependent => true
170
+ # has_many :subscribers, :class_name => "Person", :finder_sql =>
171
+ # 'SELECT DISTINCT people.* ' +
172
+ # 'FROM people p, post_subscriptions ps ' +
173
+ # 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
174
+ # 'ORDER BY p.first_name'
175
+ def has_many(association_id, options = {})
176
+ validate_options([ :foreign_key, :class_name, :exclusively_dependent, :dependent, :conditions, :order, :finder_sql ], options.keys)
177
+ association_name, association_class_name, association_class_primary_key_name =
178
+ associate_identification(association_id, options[:class_name], options[:foreign_key])
179
+
180
+ if options[:dependent]
181
+ module_eval "before_destroy '#{association_name}.each { |o| o.destroy }'"
182
+ end
183
+
184
+ if options[:exclusively_dependent]
185
+ module_eval "before_destroy Proc.new{ |record| #{association_class_name}.delete_all(%(#{association_class_primary_key_name} = '\#{record.id}')) }"
186
+ end
187
+
188
+ module_eval <<-"end_eval", __FILE__, __LINE__
189
+ def #{association_name}(force_reload = false)
190
+ if @#{association_name}.nil?
191
+ @#{association_name} = HasManyAssociation.new(self, "#{association_name}", "#{association_class_name}",
192
+ "#{association_class_primary_key_name}", #{options.inspect})
193
+ end
194
+ @#{association_name}.reload if force_reload
195
+
196
+ return @#{association_name}
197
+ end
198
+ end_eval
199
+
200
+ # deprecated api
201
+ deprecated_collection_count_method(association_name)
202
+ deprecated_add_association_relation(association_name)
203
+ deprecated_remove_association_relation(association_name)
204
+ deprecated_has_collection_method(association_name)
205
+ deprecated_find_in_collection_method(association_name)
206
+ deprecated_find_all_in_collection_method(association_name)
207
+ deprecated_create_method(association_name)
208
+ deprecated_build_method(association_name)
209
+ end
210
+
211
+ # Adds the following methods for retrival and query of a single associated object.
212
+ # +association+ is replaced with the symbol passed as the first argument, so
213
+ # <tt>has_one :manager</tt> would add among others <tt>has_manager?</tt>.
214
+ # * <tt>association(force_reload = false)</tt> - returns the associated object. Nil is returned if none is found.
215
+ # * <tt>association=(associate)</tt> - assigns the associate object, extracts the primary key, sets it as the foreign key,
216
+ # and saves the associate object.
217
+ # * <tt>association?(object, force_reload = false)</tt> - returns true if the +object+ is of the same type and has the
218
+ # same id as the associated object.
219
+ # * <tt>!association.nil?</tt> - returns true if there's an associated object.
220
+ # * <tt>build_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
221
+ # with +attributes+ and linked to this object through a foreign key but has not yet been saved.
222
+ # * <tt>create_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
223
+ # with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
224
+ #
225
+ # Example: An Account class declares <tt>has_one :beneficiary</tt>, which will add:
226
+ # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.find_first "account_id = #{id}"</tt>)
227
+ # * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
228
+ # * <tt>Account#beneficiary?</tt> (similar to <tt>account.beneficiary == some_beneficiary</tt>)
229
+ # * <tt>!Account#beneficiary.nil?</tt>
230
+ # * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
231
+ # * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
232
+ # The declaration can also include an options hash to specialize the generated methods.
233
+ #
234
+ # Options are:
235
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
236
+ # from the association name. So <tt>has_one :manager</tt> will by default be linked to the +Manager+ class, but
237
+ # if the real class name is +Person+, you'll have to specify it with this option.
238
+ # * <tt>:conditions</tt> - specify the conditions that the associated object must meet in order to be included as a "WHERE"
239
+ # sql fragment, such as "rank = 5".
240
+ # * <tt>:order</tt> - specify the order from which the associated object will be picked at the top. Specified as
241
+ # an "ORDER BY" sql fragment, such as "last_name, first_name DESC"
242
+ # * <tt>:dependent</tt> - if set to true the associated object is destroyed alongside this object
243
+ # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
244
+ # of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_one association will use "person_id"
245
+ # as the default foreign_key.
246
+ #
247
+ # Option examples:
248
+ # has_one :credit_card, :dependent => true
249
+ # has_one :last_comment, :class_name => "Comment", :order => "posted_on"
250
+ # has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'"
251
+ def has_one(association_id, options = {})
252
+ options.merge!({ :remote => true })
253
+ belongs_to(association_id, options)
254
+
255
+ association_name, association_class_name, class_primary_key_name =
256
+ associate_identification(association_id, options[:class_name], options[:foreign_key], false)
257
+
258
+ has_one_writer_method(association_name, association_class_name, class_primary_key_name)
259
+ build_method("build_", association_name, association_class_name, class_primary_key_name)
260
+ create_method("create_", association_name, association_class_name, class_primary_key_name)
261
+
262
+ module_eval "before_destroy '#{association_name}.destroy if has_#{association_name}?'" if options[:dependent]
263
+ end
264
+
265
+ # Adds the following methods for retrival and query for a single associated object that this object holds an id to.
266
+ # +association+ is replaced with the symbol passed as the first argument, so
267
+ # <tt>belongs_to :author</tt> would add among others <tt>has_author?</tt>.
268
+ # * <tt>association(force_reload = false)</tt> - returns the associated object. Nil is returned if none is found.
269
+ # * <tt>association=(associate)</tt> - assigns the associate object, extracts the primary key, and sets it as the foreign key.
270
+ # * <tt>association?(object, force_reload = false)</tt> - returns true if the +object+ is of the same type and has the
271
+ # same id as the associated object.
272
+ # * <tt>association.nil?</tt> - returns true if there's an associated object.
273
+ #
274
+ # Example: An Post class declares <tt>has_one :author</tt>, which will add:
275
+ # * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>)
276
+ # * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
277
+ # * <tt>Post#author?</tt> (similar to <tt>post.author == some_author</tt>)
278
+ # * <tt>!Post#author.nil?</tt>
279
+ # The declaration can also include an options hash to specialize the generated methods.
280
+ #
281
+ # Options are:
282
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
283
+ # from the association name. So <tt>has_one :author</tt> will by default be linked to the +Author+ class, but
284
+ # if the real class name is +Person+, you'll have to specify it with this option.
285
+ # * <tt>:conditions</tt> - specify the conditions that the associated object must meet in order to be included as a "WHERE"
286
+ # sql fragment, such as "authorized = 1".
287
+ # * <tt>:order</tt> - specify the order from which the associated object will be picked at the top. Specified as
288
+ # an "ORDER BY" sql fragment, such as "last_name, first_name DESC"
289
+ # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
290
+ # of the associated class in lower-case and "_id" suffixed. So a +Person+ class that makes a belongs_to association to a
291
+ # +Boss+ class will use "boss_id" as the default foreign_key.
292
+ # * <tt>:counter_cache</tt> - caches the number of belonging objects on the associate class through use of increment_counter
293
+ # and decrement_counter. The counter cache is incremented when an object of this class is created and decremented when it's
294
+ # destroyed. This requires that a column named "#{table_name}_count" (such as comments_count for a belonging Comment class)
295
+ # is used on the associate class (such as a Post class).
296
+ #
297
+ # Option examples:
298
+ # belongs_to :firm, :foreign_key => "client_of"
299
+ # belongs_to :author, :class_name => "Person", :foreign_key => "author_id"
300
+ # belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id",
301
+ # :conditions => 'discounts > #{payments_count}'
302
+ def belongs_to(association_id, options = {})
303
+ validate_options([ :class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache ], options.keys)
304
+
305
+ association_name, association_class_name, class_primary_key_name =
306
+ associate_identification(association_id, options[:class_name], options[:foreign_key], false)
307
+
308
+ association_class_primary_key_name = options[:foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name)) + "_id"
309
+
310
+ if options[:remote]
311
+ association_finder = <<-"end_eval"
312
+ #{association_class_name}.find_first(
313
+ "#{class_primary_key_name} = '\#{id}'#{options[:conditions] ? " AND " + options[:conditions] : ""}",
314
+ #{options[:order] ? "\"" + options[:order] + "\"" : "nil" }
315
+ )
316
+ end_eval
317
+ else
318
+ association_finder = options[:conditions] ?
319
+ "#{association_class_name}.find_on_conditions(#{association_class_primary_key_name}, \"#{options[:conditions]}\")" :
320
+ "#{association_class_name}.find(#{association_class_primary_key_name})"
321
+ end
322
+
323
+ has_association_method(association_name)
324
+ association_reader_method(association_name, association_finder)
325
+ belongs_to_writer_method(association_name, association_class_name, association_class_primary_key_name)
326
+ association_comparison_method(association_name, association_class_name)
327
+
328
+ if options[:counter_cache]
329
+ module_eval(
330
+ "after_create '#{association_class_name}.increment_counter(\"#{Inflector.pluralize(self.to_s.downcase). + "_count"}\", #{association_class_primary_key_name})" +
331
+ " if has_#{association_name}?'"
332
+ )
333
+
334
+ module_eval(
335
+ "before_destroy '#{association_class_name}.decrement_counter(\"#{Inflector.pluralize(self.to_s.downcase) + "_count"}\", #{association_class_primary_key_name})" +
336
+ " if has_#{association_name}?'"
337
+ )
338
+ end
339
+ end
340
+
341
+ # Associates two classes via an intermediate join table. Unless the join table is explicitly specified as
342
+ # an option, it is guessed using the lexical order of the class names. So a join between Developer and Project
343
+ # will give the default join table name of "developers_projects" because "D" outranks "P".
344
+ # Adds the following methods for retrival and query.
345
+ # +collection+ is replaced with the symbol passed as the first argument, so
346
+ # <tt>has_and_belongs_to_many :categories</tt> would add among others +add_categories+.
347
+ # * <tt>collection(force_reload = false)</tt> - returns an array of all the associated objects.
348
+ # An empty array is returned if none is found.
349
+ # * <tt>!collection.empty?</tt> - returns true if there's any associated objects.
350
+ # * <tt>collection.size</tt> - returns the number of associated objects.
351
+ # * <tt>collection<<(object)</tt> - adds an association between this object and the object given as argument. Multiple associations
352
+ # can be created by passing an array of objects instead.
353
+ # * <tt>collection.delete(object)</tt> - removes the association between this object and the object given as
354
+ # argument. Multiple associations can be removed by passing an array of objects instead.
355
+ #
356
+ # Example: An Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
357
+ # * <tt>Developer#projects</tt>
358
+ # * <tt>!Developer#projects.empty?</tt>
359
+ # * <tt>Developer#projects.size</tt>
360
+ # * <tt>Developer#projects<<</tt>
361
+ # * <tt>Developer#projects.delete</tt>
362
+ # The declaration can also include an options hash to specialize the generated methods.
363
+ #
364
+ # Options are:
365
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
366
+ # from the association name. So <tt>has_and_belongs_to_many :projects</tt> will by default be linked to the
367
+ # +Project+ class, but if the real class name is +SuperProject+, you'll have to specify it with this option.
368
+ # * <tt>:join_table</tt> - specify the name of the join table if the default based on lexical order isn't what you want.
369
+ # WARNING: If you're overwriting the table name of either class, the table_name method MUST be declared underneath any
370
+ # has_and_belongs_to_many declaration in order to work.
371
+ # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
372
+ # of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_and_belongs_to_many association
373
+ # will use "person_id" as the default foreign_key.
374
+ # * <tt>:association_foreign_key</tt> - specify the association foreign key used for the association. By default this is
375
+ # guessed to be the name of the associated class in lower-case and "_id" suffixed. So the associated class is +Project+
376
+ # that makes a has_and_belongs_to_many association will use "project_id" as the default association foreign_key.
377
+ # * <tt>:order</tt> - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment, such as "last_name, first_name DESC".
378
+ # * <tt>:finder_sql</tt> - overwrite the default generated SQL used to fetch the association with a manual one
379
+ # * <tt>:delete_sql</tt> - overwrite the default generated SQL used to remove links between the associated
380
+ # classes with a manual one
381
+ # * <tt>:insert_sql</tt> - overwrite the default generated SQL used to add links between the associated classes
382
+ # with a manual one
383
+ #
384
+ # Option examples:
385
+ # has_and_belongs_to_many :projects
386
+ # has_and_belongs_to_many :nations, :class_name => "Country"
387
+ # has_and_belongs_to_many :categories, :join_table => "prods_cats"
388
+ def has_and_belongs_to_many(association_id, options = {})
389
+ validate_options([ :class_name, :table_name, :foreign_key, :association_foreign_key,
390
+ :join_table, :finder_sql, :delete_sql, :insert_sql, :order ], options.keys)
391
+ association_name, association_class_name, association_class_primary_key_name =
392
+ associate_identification(association_id, options[:class_name], options[:foreign_key])
393
+
394
+ join_table = options[:join_table] ||
395
+ join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(association_class_name))
396
+
397
+
398
+ module_eval <<-"end_eval", __FILE__, __LINE__
399
+ def #{association_name}(force_reload = false)
400
+ if @#{association_name}.nil?
401
+ @#{association_name} = HasAndBelongsToManyCollection.new(self, "#{association_name}", "#{association_class_name}",
402
+ "#{association_class_primary_key_name}", '#{join_table}', #{options.inspect})
403
+ end
404
+ @#{association_name}.reload if force_reload
405
+
406
+ return @#{association_name}
407
+ end
408
+ end_eval
409
+
410
+ before_destroy_sql = "DELETE FROM #{join_table} WHERE #{Inflector.foreign_key(self.class_name)} = '\\\#{self.id}'"
411
+ module_eval(%{before_destroy "self.connection.delete(%{#{before_destroy_sql}})"}) # "
412
+
413
+ # deprecated api
414
+ deprecated_collection_count_method(association_name)
415
+ deprecated_add_association_relation(association_name)
416
+ deprecated_remove_association_relation(association_name)
417
+ deprecated_has_collection_method(association_name)
418
+ end
419
+
420
+ private
421
+ # Raises an exception if an invalid option has been specified to prevent misspellings from slipping through
422
+ def validate_options(valid_option_keys, supplied_option_keys)
423
+ unknown_option_keys = supplied_option_keys - valid_option_keys
424
+ raise(ActiveRecord::ActiveRecordError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty?
425
+ end
426
+
427
+ def join_table_name(first_table_name, second_table_name)
428
+ if first_table_name < second_table_name
429
+ join_table = "#{first_table_name}_#{second_table_name}"
430
+ else
431
+ join_table = "#{second_table_name}_#{first_table_name}"
432
+ end
433
+
434
+ table_name_prefix + join_table + table_name_suffix
435
+ end
436
+
437
+ def associate_identification(association_id, association_class_name, foreign_key, plural = true)
438
+ if association_class_name !~ /::/
439
+ association_class_name = type_name_with_module(
440
+ association_class_name ||
441
+ Inflector.camelize(plural ? Inflector.singularize(association_id.id2name) : association_id.id2name)
442
+ )
443
+ end
444
+
445
+ primary_key_name = foreign_key || Inflector.underscore(Inflector.demodulize(name)) + "_id"
446
+
447
+ return association_id.id2name, association_class_name, primary_key_name
448
+ end
449
+
450
+ def association_comparison_method(association_name, association_class_name)
451
+ module_eval <<-"end_eval", __FILE__, __LINE__
452
+ def #{association_name}?(comparison_object, force_reload = false)
453
+ if comparison_object.kind_of?(#{association_class_name})
454
+ #{association_name}(force_reload) == comparison_object
455
+ else
456
+ raise "Comparison object is a #{association_class_name}, should have been \#{comparison_object.class.name}"
457
+ end
458
+ end
459
+ end_eval
460
+ end
461
+
462
+ def association_reader_method(association_name, association_finder)
463
+ module_eval <<-"end_eval", __FILE__, __LINE__
464
+ def #{association_name}(force_reload = false)
465
+ if @#{association_name}.nil? || force_reload
466
+ begin
467
+ @#{association_name} = #{association_finder}
468
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotFound
469
+ nil
470
+ end
471
+ end
472
+
473
+ return @#{association_name}
474
+ end
475
+ end_eval
476
+ end
477
+
478
+ def has_one_writer_method(association_name, association_class_name, class_primary_key_name)
479
+ module_eval <<-"end_eval", __FILE__, __LINE__
480
+ def #{association_name}=(association)
481
+ if association.nil?
482
+ @#{association_name}.#{class_primary_key_name} = nil
483
+ @#{association_name}.save(false)
484
+ @#{association_name} = nil
485
+ else
486
+ raise ActiveRecord::AssociationTypeMismatch unless #{association_class_name} === association
487
+ association.#{class_primary_key_name} = id
488
+ association.save(false)
489
+ @#{association_name} = association
490
+ end
491
+ end
492
+ end_eval
493
+ end
494
+
495
+ def belongs_to_writer_method(association_name, association_class_name, association_class_primary_key_name)
496
+ module_eval <<-"end_eval", __FILE__, __LINE__
497
+ def #{association_name}=(association)
498
+ if association.nil?
499
+ @#{association_name} = self.#{association_class_primary_key_name} = nil
500
+ else
501
+ raise ActiveRecord::AssociationTypeMismatch unless #{association_class_name} === association
502
+ @#{association_name} = association
503
+ self.#{association_class_primary_key_name} = association.id
504
+ end
505
+ end
506
+ end_eval
507
+ end
508
+
509
+ def has_association_method(association_name)
510
+ module_eval <<-"end_eval", __FILE__, __LINE__
511
+ def has_#{association_name}?(force_reload = false)
512
+ !#{association_name}(force_reload).nil?
513
+ end
514
+ end_eval
515
+ end
516
+
517
+ def build_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
518
+ module_eval <<-"end_eval", __FILE__, __LINE__
519
+ def #{method_prefix + collection_name}(attributes = {})
520
+ association = #{collection_class_name}.new
521
+ association.attributes = attributes.merge({ "#{class_primary_key_name}" => id})
522
+ association
523
+ end
524
+ end_eval
525
+ end
526
+
527
+ def create_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
528
+ module_eval <<-"end_eval", __FILE__, __LINE__
529
+ def #{method_prefix + collection_name}(attributes = nil)
530
+ #{collection_class_name}.create((attributes || {}).merge({ "#{class_primary_key_name}" => id}))
531
+ end
532
+ end_eval
533
+ end
534
+ end
535
+ end
536
+ end