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
data/README ADDED
@@ -0,0 +1,361 @@
1
+ = Active Record -- Object-relation mapping put on rails
2
+
3
+ Active Record connects business objects and database tables to create a persistable
4
+ domain model where logic and data is presented in one wrapping. It's an implementation
5
+ of the object-relational mapping (ORM) pattern[http://www.martinfowler.com/eaaCatalog/activeRecord.html]
6
+ by the same name as described by Martin Fowler:
7
+
8
+ "An object that wraps a row in a database table or view, encapsulates
9
+ the database access, and adds domain logic on that data."
10
+
11
+ Active Records main contribution to the pattern is to relieve the original of two stunting problems:
12
+ lack of associations and inheritance. By adding a simple domain language-like set of macros to describe
13
+ the former and integrating the Single Table Inheritance pattern for the latter, Active Record narrows the
14
+ gap of functionality between the data mapper and active record approach.
15
+
16
+ A short rundown of the major features:
17
+
18
+ * Automated mapping between classes and tables, attributes and columns.
19
+
20
+ class Product < ActiveRecord::Base; end
21
+
22
+ ...is automatically mapped to the table named "products", such as:
23
+
24
+ CREATE TABLE products (
25
+ id int(11) NOT NULL auto_increment,
26
+ name varchar(255),
27
+ PRIMARY KEY (id)
28
+ );
29
+
30
+ ...which again gives Product#name and Product#name=(new_name)
31
+
32
+ Learn more in link:classes/ActiveRecord/Base.html
33
+
34
+
35
+ * Associations between objects controlled by simple meta-programming macros.
36
+
37
+ class Firm < ActiveRecord::Base
38
+ has_many :clients
39
+ has_one :account
40
+ belongs_to :conglomorate
41
+ end
42
+
43
+ Learn more in link:classes/ActiveRecord/Associations/ClassMethods.html
44
+
45
+
46
+ * Aggregations of value objects controlled by simple meta-programming macros.
47
+
48
+ class Account < ActiveRecord::Base
49
+ composed_of :balance, :class_name => "Money",
50
+ :mapping => %w(balance amount)
51
+ composed_of :address,
52
+ :mapping => [%w(address_street street), %w(address_city city)]
53
+ end
54
+
55
+ Learn more in link:classes/ActiveRecord/Aggregations/ClassMethods.html
56
+
57
+
58
+ * Validation rules that can differ for new or existing objects.
59
+
60
+ class Post < ActiveRecord::Base
61
+ def validate # validates on both creates and updates
62
+ errors.add_on_empty "title"
63
+ end
64
+
65
+ def validate_on_update
66
+ errors.add_on_empty "password"
67
+ end
68
+ end
69
+
70
+ Learn more in link:classes/ActiveRecord/Validations.html
71
+
72
+
73
+ * Callbacks as methods or queues on the entire lifecycle (instantiation, saving, destroying, validating, etc).
74
+
75
+ class Person < ActiveRecord::Base
76
+ def before_destroy # is called just before Person#destroy
77
+ CreditCard.find(credit_card_id).destroy
78
+ end
79
+ end
80
+
81
+ class Account < ActiveRecord::Base
82
+ after_find :eager_load, 'self.class.announce(#{id})'
83
+ end
84
+
85
+ Learn more in link:classes/ActiveRecord/Callbacks.html
86
+
87
+
88
+ * Observers for the entire lifecycle
89
+
90
+ class CommentObserver < ActiveRecord::Observer
91
+ def after_create(comment) # is called just after Comment#save
92
+ NotificationService.send_email("david@loudthinking.com", comment)
93
+ end
94
+ end
95
+
96
+ Learn more in link:classes/ActiveRecord/Observer.html
97
+
98
+
99
+ * Inheritance hierarchies
100
+
101
+ class Company < ActiveRecord::Base; end
102
+ class Firm < Company; end
103
+ class Client < Company; end
104
+ class PriorityClient < Client; end
105
+
106
+ Learn more in link:classes/ActiveRecord/Base.html
107
+
108
+
109
+ * Transaction support on both a database and object level. The latter is implemented
110
+ by using Transaction::Simple[http://www.halostatue.ca/ruby/Transaction__Simple.html]
111
+
112
+ # Just database transaction
113
+ Account.transaction do
114
+ david.withdrawal(100)
115
+ mary.deposit(100)
116
+ end
117
+
118
+ # Database and object transaction
119
+ Account.transaction(david, mary) do
120
+ david.withdrawal(100)
121
+ mary.deposit(100)
122
+ end
123
+
124
+ Learn more in link:classes/ActiveRecord/Transactions/ClassMethods.html
125
+
126
+
127
+ * Reflections on columns, associations, and aggregations
128
+
129
+ reflection = Firm.reflect_on_association(:clients)
130
+ reflection.klass # => Client (class)
131
+ Firm.columns # Returns an array of column descriptors for the firms table
132
+
133
+ Learn more in link:classes/ActiveRecord/Reflection/ClassMethods.html
134
+
135
+
136
+ * Direct manipulation (instead of service invocation)
137
+
138
+ So instead of (Hibernate[http://www.hibernate.org/] example):
139
+
140
+ long pkId = 1234;
141
+ DomesticCat pk = (DomesticCat) sess.load( Cat.class, new Long(pkId) );
142
+ // something interesting involving a cat...
143
+ sess.save(cat);
144
+ sess.flush(); // force the SQL INSERT
145
+
146
+ Active Record lets you:
147
+
148
+ pkId = 1234
149
+ cat = Cat.find(pkId)
150
+ # something even more interesting involving a the same cat...
151
+ cat.save
152
+
153
+ Learn more in link:classes/ActiveRecord/Base.html
154
+
155
+
156
+ * Database abstraction through simple adapters (~100 lines) with a shared connector
157
+
158
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite", :dbfile => "dbfile")
159
+
160
+ ActiveRecord::Base.establish_connection(
161
+ :adapter => "mysql",
162
+ :host => "localhost",
163
+ :username => "me",
164
+ :password => "secret",
165
+ :database => "activerecord"
166
+ )
167
+
168
+ Learn more in link:classes/ActiveRecord/Base.html#M000081
169
+
170
+
171
+ * Logging support for Log4r[http://log4r.sourceforge.net] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc]
172
+
173
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
174
+ ActiveRecord::Base.logger = Log4r::Logger.new("Application Log")
175
+
176
+
177
+ == Simple example (1/2): Defining tables and classes (using MySQL)
178
+
179
+ Data definitions are specified only in the database. Active Record queries the database for
180
+ the column names (that then serves to determine which attributes are valid) on regular
181
+ objects instantiation through the new constructor and relies on the column names in the rows
182
+ with the finders.
183
+
184
+ # CREATE TABLE companies (
185
+ # id int(11) unsigned NOT NULL auto_increment,
186
+ # client_of int(11),
187
+ # name varchar(255),
188
+ # type varchar(100),
189
+ # PRIMARY KEY (id)
190
+ # )
191
+
192
+ Active Record automatically links the "Company" object to the "companies" table
193
+
194
+ class Company < ActiveRecord::Base
195
+ has_many :people, :class_name => "Person"
196
+ end
197
+
198
+ class Firm < Company
199
+ has_many :clients
200
+
201
+ def people_with_all_clients
202
+ clients.inject([]) { |people, client| people + client.people }
203
+ end
204
+ end
205
+
206
+ The foreign_key is only necessary because we didn't use "firm_id" in the data definition
207
+
208
+ class Client < Company
209
+ belongs_to :firm, :foreign_key => "client_of"
210
+ end
211
+
212
+ # CREATE TABLE people (
213
+ # id int(11) unsigned NOT NULL auto_increment,
214
+ # name text,
215
+ # company_id text,
216
+ # PRIMARY KEY (id)
217
+ # )
218
+
219
+ Active Record will also automatically link the "Person" object to the "people" table
220
+
221
+ class Person < ActiveRecord::Base
222
+ belongs_to :company
223
+ end
224
+
225
+ == Simple example (2/2): Using the domain
226
+
227
+ Picking a database connection for all the active records
228
+
229
+ ActiveRecord::Base.establish_connection(
230
+ :adapter => "mysql",
231
+ :host => "localhost",
232
+ :username => "me",
233
+ :password => "secret",
234
+ :database => "activerecord"
235
+ )
236
+
237
+ Create some fixtures
238
+
239
+ firm = Firm.new("name" => "Next Angle")
240
+ # SQL: INSERT INTO companies (name, type) VALUES("Next Angle", "Firm")
241
+ firm.save
242
+
243
+ client = Client.new("name" => "37signals", "client_of" => firm.id)
244
+ # SQL: INSERT INTO companies (name, client_of, type) VALUES("37signals", 1, "Firm")
245
+ client.save
246
+
247
+ Lots of different finders
248
+
249
+ # SQL: SELECT * FROM companies WHERE id = 1
250
+ next_angle = Company.find(1)
251
+
252
+ # SQL: SELECT * FROM companies WHERE id = 1 AND type = 'Firm'
253
+ next_angle = Firm.find(1)
254
+
255
+ # SQL: SELECT * FROM companies WHERE id = 1 AND name = 'Next Angle'
256
+ next_angle = Company.find_first "name = 'Next Angle'"
257
+
258
+ next_angle = Firm.find_by_sql("SELECT * FROM companies WHERE id = 1").first
259
+
260
+ The supertype, Company, will return subtype instances
261
+
262
+ Firm === next_angle
263
+
264
+ All the dynamic methods added by the has_many macro
265
+
266
+ next_angle.clients.empty? # true
267
+ next_angle.clients.size # total number of clients
268
+ all_clients = next_angle.clients
269
+
270
+ Constrained finds makes access security easier when ID comes from a web-app
271
+
272
+ # SQL: SELECT * FROM companies WHERE client_of = 1 AND type = 'Client' AND id = 2
273
+ thirty_seven_signals = next_angle.clients.find(2)
274
+
275
+ Bi-directional associations thanks to the "belongs_to" macro
276
+
277
+ thirty_seven_signals.firm.nil? # true
278
+
279
+
280
+ == Examples
281
+
282
+ Active Record ships with a couple of examples that should give you a good feel for
283
+ operating usage. Be sure to edit the <tt>examples/shared_setup.rb</tt> file for your
284
+ own database before running the examples. Possibly also the table definition SQL in
285
+ the examples themselves.
286
+
287
+ It's also highly recommended to have a look at the unit tests. Read more in link:files/RUNNING_UNIT_TESTS.html
288
+
289
+
290
+ == Database support
291
+
292
+ Active Record ships with adapters for MySQL/Ruby[http://www.tmtm.org/en/mysql/ruby/]
293
+ (compatible with Ruby/MySQL[http://www.tmtm.org/ruby/mysql/README_en.html]),
294
+ PostgreSQL[http://www.postgresql.jp/interfaces/ruby/], and
295
+ SQLite[http://rubyforge.org/projects/sqlite-ruby/] (needs SQLite 2.8.13+ and SQLite-Ruby 1.1.2+).
296
+ The adapters are around 100 lines of code fulfilling the interface specified by
297
+ ActiveRecord::ConnectionAdapters::AbstractAdapter. Writing a new adapter should be a small task --
298
+ especially considering the extensive test suite that'll make sure you're fulfilling the contract.
299
+
300
+
301
+ == Philosophy
302
+
303
+ Active Record attempts to provide a coherent wrapping for the inconvenience that is
304
+ object-relational mapping. The prime directive for this mapping has been to minimize
305
+ the amount of code needed to built a real-world domain model. This is made possible
306
+ by relying on a number of conventions that make it easy for Active Record to infer
307
+ complex relations and structures from a minimal amount of explicit direction.
308
+
309
+ Convention over Configuration:
310
+ * No XML-files!
311
+ * Lots of reflection and run-time extension
312
+ * Magic is not inherently a bad word
313
+
314
+ Admit the Database:
315
+ * Lets you drop down to SQL for odd cases and performance
316
+ * Doesn't attempt to duplicate or replace data definitions
317
+
318
+
319
+ == Download
320
+
321
+ The latest version of Active Record can be found at
322
+
323
+ * http://rubyforge.org/project/showfiles.php?group_id=182
324
+
325
+ Documentation can be found at
326
+
327
+ * http://ar.rubyonrails.org
328
+
329
+
330
+ == Installation
331
+
332
+ The prefered method of installing Active Record is through its GEM file. You'll need to have
333
+ RubyGems[http://rubygems.rubyforge.org/wiki/wiki.pl] installed for that, though. If you have,
334
+ then use:
335
+
336
+ % [sudo] gem install activerecord-0.9.0.gem
337
+
338
+ You can also install Active Record the old-fashion way with the following command:
339
+
340
+ % [sudo] ruby install.rb
341
+
342
+ from its distribution directory.
343
+
344
+
345
+ == License
346
+
347
+ Active Record is released under the same license as Ruby.
348
+
349
+
350
+ == Support
351
+
352
+ The Active Record homepage is http://activerecord.rubyonrails.org. You can find the Active Record
353
+ RubyForge page at http://rubyforge.org/projects/activerecord. And as Jim from Rake says:
354
+
355
+ Feel free to submit commits or feature requests. If you send a patch,
356
+ remember to update the corresponding unit tests. If fact, I prefer
357
+ new feature to be submitted in the form of new unit tests.
358
+
359
+ For other information, feel free to ask on the ruby-talk mailing list
360
+ (which is mirrored to comp.lang.ruby) or contact mailto:david@loudthinking.com.
361
+
@@ -0,0 +1,36 @@
1
+ == Creating the test database
2
+
3
+ The default names for the test databases are "activerecord_unittest" and
4
+ "activerecord_unittest2". If you want to use another database name then be sure
5
+ to update the connection adapter setups you want to test with in
6
+ test/connections/<your database>/connection.rb.
7
+ When you have the database online, you can import the fixture tables with
8
+ the test/fixtures/db_definitions/*.sql files.
9
+
10
+ Make sure that you create database objects with the same user that you specified in i
11
+ connection.rb otherwise (on Postgres, at least) tests for default values will fail
12
+ (see http://dev.rubyonrails.org/trac.cgi/ticket/118)
13
+
14
+ == Running with Rake
15
+
16
+ The easiest way to run the unit tests is through Rake. The default task runs
17
+ the entire test suite for all the adapters. You can also run the suite on just
18
+ one adapter by using the tasks test_mysql_ruby, test_ruby_mysql, test_sqlite,
19
+ or test_postresql. For more information, checkout the full array of rake tasks with "rake -T"
20
+
21
+ Rake can be found at http://rake.rubyforge.org
22
+
23
+ == Running by hand
24
+
25
+ Unit tests are located in test directory. If you only want to run a single test suite,
26
+ or don't want to bother with Rake, you can do so with something like:
27
+
28
+ cd test; ruby -I "connections/native_mysql" base_test.rb
29
+
30
+ That'll run the base suite using the MySQL-Ruby adapter. Change the adapter
31
+ and test suite name as needed.
32
+
33
+ You can also run all the suites on a specific adapter with:
34
+
35
+ cd test; all.sh "connections/native_mysql"
36
+
@@ -0,0 +1,9 @@
1
+ # Require the eval_debugger to get an insight into the methods that aggregations and associations macross are adding.
2
+ # All the additions are reported to $stderr just by requiring this file.
3
+ class Module
4
+ alias :old_module_eval :module_eval
5
+ def module_eval(*args, &block)
6
+ puts("in #{self.name}, #{if args[1] then "file #{args[1]}" end} #{if args[2] then "on line #{args[2]}" end}:\n#{args[0]}") if args[0]
7
+ old_module_eval(*args, &block)
8
+ end
9
+ end
Binary file
@@ -0,0 +1,87 @@
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 companies", "DROP TABLE people", "DROP TABLE people_companies",
10
+ "CREATE TABLE companies (id int(11) auto_increment, client_of int(11), name varchar(255), type varchar(100), PRIMARY KEY (id))",
11
+ "CREATE TABLE people (id int(11) auto_increment, name varchar(100), PRIMARY KEY (id))",
12
+ "CREATE TABLE people_companies (person_id int(11), company_id int(11), PRIMARY KEY (person_id, company_id))",
13
+ ].each { |statement|
14
+ # Tables doesn't necessarily already exist
15
+ begin; ActiveRecord::Base.connection.execute(statement); rescue ActiveRecord::StatementInvalid; end
16
+ }
17
+
18
+
19
+ # Class setup ---------------
20
+
21
+ class Company < ActiveRecord::Base
22
+ has_and_belongs_to_many :people, :class_name => "Person", :join_table => "people_companies", :table_name => "people"
23
+ end
24
+
25
+ class Firm < Company
26
+ has_many :clients, :foreign_key => "client_of"
27
+
28
+ def people_with_all_clients
29
+ clients.inject([]) { |people, client| people + client.people }
30
+ end
31
+ end
32
+
33
+ class Client < Company
34
+ belongs_to :firm, :foreign_key => "client_of"
35
+ end
36
+
37
+ class Person < ActiveRecord::Base
38
+ has_and_belongs_to_many :companies, :join_table => "people_companies"
39
+ def self.table_name() "people" end
40
+ end
41
+
42
+
43
+ # Usage ---------------
44
+
45
+ logger.info "\nCreate fixtures"
46
+
47
+ Firm.new("name" => "Next Angle").save
48
+ Client.new("name" => "37signals", "client_of" => 1).save
49
+ Person.new("name" => "David").save
50
+
51
+
52
+ logger.info "\nUsing Finders"
53
+
54
+ next_angle = Company.find(1)
55
+ next_angle = Firm.find(1)
56
+ next_angle = Company.find_first "name = 'Next Angle'"
57
+ next_angle = Firm.find_by_sql("SELECT * FROM companies WHERE id = 1").first
58
+
59
+ Firm === next_angle
60
+
61
+
62
+ logger.info "\nUsing has_many association"
63
+
64
+ next_angle.has_clients?
65
+ next_angle.clients_count
66
+ all_clients = next_angle.clients
67
+
68
+ thirty_seven_signals = next_angle.find_in_clients(2)
69
+
70
+
71
+ logger.info "\nUsing belongs_to association"
72
+
73
+ thirty_seven_signals.has_firm?
74
+ thirty_seven_signals.firm?(next_angle)
75
+
76
+
77
+ logger.info "\nUsing has_and_belongs_to_many association"
78
+
79
+ david = Person.find(1)
80
+ david.add_companies(thirty_seven_signals, next_angle)
81
+ david.companies.include?(next_angle)
82
+ david.companies_count == 2
83
+
84
+ david.remove_companies(next_angle)
85
+ david.companies_count == 1
86
+
87
+ thirty_seven_signals.people.include?(david)