activerecord 1.0.0 → 2.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.
- data/CHANGELOG +4928 -3
- data/README +45 -46
- data/RUNNING_UNIT_TESTS +8 -11
- data/Rakefile +247 -0
- data/install.rb +8 -38
- data/lib/active_record/aggregations.rb +64 -49
- data/lib/active_record/associations/association_collection.rb +217 -47
- data/lib/active_record/associations/association_proxy.rb +159 -0
- data/lib/active_record/associations/belongs_to_association.rb +56 -0
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +50 -0
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +155 -37
- data/lib/active_record/associations/has_many_association.rb +145 -75
- data/lib/active_record/associations/has_many_through_association.rb +283 -0
- data/lib/active_record/associations/has_one_association.rb +96 -0
- data/lib/active_record/associations.rb +1537 -304
- data/lib/active_record/attribute_methods.rb +328 -0
- data/lib/active_record/base.rb +2001 -588
- data/lib/active_record/calculations.rb +269 -0
- data/lib/active_record/callbacks.rb +169 -165
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +308 -0
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +171 -0
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +87 -0
- data/lib/active_record/connection_adapters/abstract/quoting.rb +69 -0
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +472 -0
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +306 -0
- data/lib/active_record/connection_adapters/abstract_adapter.rb +125 -279
- data/lib/active_record/connection_adapters/mysql_adapter.rb +442 -77
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +805 -135
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +34 -0
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +353 -69
- data/lib/active_record/fixtures.rb +946 -100
- data/lib/active_record/locking/optimistic.rb +144 -0
- data/lib/active_record/locking/pessimistic.rb +77 -0
- data/lib/active_record/migration.rb +417 -0
- data/lib/active_record/observer.rb +142 -32
- data/lib/active_record/query_cache.rb +23 -0
- data/lib/active_record/reflection.rb +163 -70
- data/lib/active_record/schema.rb +58 -0
- data/lib/active_record/schema_dumper.rb +171 -0
- data/lib/active_record/serialization.rb +98 -0
- data/lib/active_record/serializers/json_serializer.rb +71 -0
- data/lib/active_record/serializers/xml_serializer.rb +315 -0
- data/lib/active_record/timestamp.rb +41 -0
- data/lib/active_record/transactions.rb +87 -57
- data/lib/active_record/validations.rb +909 -122
- data/lib/active_record/vendor/db2.rb +362 -0
- data/lib/active_record/vendor/mysql.rb +126 -29
- data/lib/active_record/version.rb +9 -0
- data/lib/active_record.rb +35 -7
- data/lib/activerecord.rb +1 -0
- data/test/aaa_create_tables_test.rb +72 -0
- data/test/abstract_unit.rb +73 -5
- data/test/active_schema_test_mysql.rb +43 -0
- data/test/adapter_test.rb +105 -0
- data/test/adapter_test_sqlserver.rb +95 -0
- data/test/aggregations_test.rb +110 -16
- data/test/all.sh +2 -2
- data/test/ar_schema_test.rb +33 -0
- data/test/association_inheritance_reload.rb +14 -0
- data/test/associations/ar_joins_test.rb +0 -0
- data/test/associations/callbacks_test.rb +147 -0
- data/test/associations/cascaded_eager_loading_test.rb +110 -0
- data/test/associations/eager_singularization_test.rb +145 -0
- data/test/associations/eager_test.rb +442 -0
- data/test/associations/extension_test.rb +47 -0
- data/test/associations/inner_join_association_test.rb +88 -0
- data/test/associations/join_model_test.rb +553 -0
- data/test/associations_test.rb +1930 -267
- data/test/attribute_methods_test.rb +146 -0
- data/test/base_test.rb +1316 -84
- data/test/binary_test.rb +32 -0
- data/test/calculations_test.rb +251 -0
- data/test/callbacks_test.rb +400 -0
- data/test/class_inheritable_attributes_test.rb +3 -4
- data/test/column_alias_test.rb +17 -0
- data/test/connection_test_firebird.rb +8 -0
- data/test/connection_test_mysql.rb +30 -0
- data/test/connections/native_db2/connection.rb +25 -0
- data/test/connections/native_firebird/connection.rb +26 -0
- data/test/connections/native_frontbase/connection.rb +27 -0
- data/test/connections/native_mysql/connection.rb +21 -18
- data/test/connections/native_openbase/connection.rb +21 -0
- data/test/connections/native_oracle/connection.rb +27 -0
- data/test/connections/native_postgresql/connection.rb +17 -18
- data/test/connections/native_sqlite/connection.rb +17 -16
- data/test/connections/native_sqlite3/connection.rb +25 -0
- data/test/connections/native_sqlite3/in_memory_connection.rb +18 -0
- data/test/connections/native_sybase/connection.rb +23 -0
- data/test/copy_table_test_sqlite.rb +69 -0
- data/test/datatype_test_postgresql.rb +203 -0
- data/test/date_time_test.rb +37 -0
- data/test/default_test_firebird.rb +16 -0
- data/test/defaults_test.rb +67 -0
- data/test/deprecated_finder_test.rb +30 -0
- data/test/finder_test.rb +607 -32
- data/test/fixtures/accounts.yml +28 -0
- data/test/fixtures/all/developers.yml +0 -0
- data/test/fixtures/all/people.csv +0 -0
- data/test/fixtures/all/tasks.yml +0 -0
- data/test/fixtures/author.rb +107 -0
- data/test/fixtures/author_favorites.yml +4 -0
- data/test/fixtures/authors.yml +7 -0
- data/test/fixtures/bad_fixtures/attr_with_numeric_first_char +1 -0
- data/test/fixtures/bad_fixtures/attr_with_spaces +1 -0
- data/test/fixtures/bad_fixtures/blank_line +3 -0
- data/test/fixtures/bad_fixtures/duplicate_attributes +3 -0
- data/test/fixtures/bad_fixtures/missing_value +1 -0
- data/test/fixtures/binaries.yml +132 -0
- data/test/fixtures/binary.rb +2 -0
- data/test/fixtures/book.rb +4 -0
- data/test/fixtures/books.yml +7 -0
- data/test/fixtures/categories/special_categories.yml +9 -0
- data/test/fixtures/categories/subsubdir/arbitrary_filename.yml +4 -0
- data/test/fixtures/categories.yml +14 -0
- data/test/fixtures/categories_ordered.yml +7 -0
- data/test/fixtures/categories_posts.yml +23 -0
- data/test/fixtures/categorization.rb +5 -0
- data/test/fixtures/categorizations.yml +17 -0
- data/test/fixtures/category.rb +26 -0
- data/test/fixtures/citation.rb +6 -0
- data/test/fixtures/comment.rb +23 -0
- data/test/fixtures/comments.yml +59 -0
- data/test/fixtures/companies.yml +55 -0
- data/test/fixtures/company.rb +81 -4
- data/test/fixtures/company_in_module.rb +32 -6
- data/test/fixtures/computer.rb +4 -0
- data/test/fixtures/computers.yml +4 -0
- data/test/fixtures/contact.rb +16 -0
- data/test/fixtures/courses.yml +7 -0
- data/test/fixtures/customer.rb +28 -3
- data/test/fixtures/customers.yml +17 -0
- data/test/fixtures/db_definitions/db2.drop.sql +33 -0
- data/test/fixtures/db_definitions/db2.sql +235 -0
- data/test/fixtures/db_definitions/db22.drop.sql +2 -0
- data/test/fixtures/db_definitions/db22.sql +5 -0
- data/test/fixtures/db_definitions/firebird.drop.sql +65 -0
- data/test/fixtures/db_definitions/firebird.sql +310 -0
- data/test/fixtures/db_definitions/firebird2.drop.sql +2 -0
- data/test/fixtures/db_definitions/firebird2.sql +6 -0
- data/test/fixtures/db_definitions/frontbase.drop.sql +33 -0
- data/test/fixtures/db_definitions/frontbase.sql +273 -0
- data/test/fixtures/db_definitions/frontbase2.drop.sql +1 -0
- data/test/fixtures/db_definitions/frontbase2.sql +4 -0
- data/test/fixtures/db_definitions/openbase.drop.sql +2 -0
- data/test/fixtures/db_definitions/openbase.sql +318 -0
- data/test/fixtures/db_definitions/openbase2.drop.sql +2 -0
- data/test/fixtures/db_definitions/openbase2.sql +7 -0
- data/test/fixtures/db_definitions/oracle.drop.sql +67 -0
- data/test/fixtures/db_definitions/oracle.sql +330 -0
- data/test/fixtures/db_definitions/oracle2.drop.sql +2 -0
- data/test/fixtures/db_definitions/oracle2.sql +6 -0
- data/test/fixtures/db_definitions/postgresql.drop.sql +44 -0
- data/test/fixtures/db_definitions/postgresql.sql +217 -38
- data/test/fixtures/db_definitions/postgresql2.drop.sql +2 -0
- data/test/fixtures/db_definitions/postgresql2.sql +2 -2
- data/test/fixtures/db_definitions/schema.rb +354 -0
- data/test/fixtures/db_definitions/schema2.rb +11 -0
- data/test/fixtures/db_definitions/sqlite.drop.sql +33 -0
- data/test/fixtures/db_definitions/sqlite.sql +139 -5
- data/test/fixtures/db_definitions/sqlite2.drop.sql +2 -0
- data/test/fixtures/db_definitions/sqlite2.sql +1 -0
- data/test/fixtures/db_definitions/sybase.drop.sql +35 -0
- data/test/fixtures/db_definitions/sybase.sql +222 -0
- data/test/fixtures/db_definitions/sybase2.drop.sql +4 -0
- data/test/fixtures/db_definitions/sybase2.sql +5 -0
- data/test/fixtures/developer.rb +70 -6
- data/test/fixtures/developers.yml +21 -0
- data/test/fixtures/developers_projects/david_action_controller +2 -1
- data/test/fixtures/developers_projects/david_active_record +2 -1
- data/test/fixtures/developers_projects.yml +17 -0
- data/test/fixtures/edge.rb +5 -0
- data/test/fixtures/edges.yml +6 -0
- data/test/fixtures/entrants.yml +14 -0
- data/test/fixtures/example.log +1 -0
- data/test/fixtures/fk_test_has_fk.yml +3 -0
- data/test/fixtures/fk_test_has_pk.yml +2 -0
- data/test/fixtures/flowers.jpg +0 -0
- data/test/fixtures/funny_jokes.yml +10 -0
- data/test/fixtures/item.rb +7 -0
- data/test/fixtures/items.yml +4 -0
- data/test/fixtures/joke.rb +3 -0
- data/test/fixtures/keyboard.rb +3 -0
- data/test/fixtures/legacy_thing.rb +3 -0
- data/test/fixtures/legacy_things.yml +3 -0
- data/test/fixtures/matey.rb +4 -0
- data/test/fixtures/mateys.yml +4 -0
- data/test/fixtures/migrations/1_people_have_last_names.rb +9 -0
- data/test/fixtures/migrations/2_we_need_reminders.rb +12 -0
- data/test/fixtures/migrations/3_innocent_jointable.rb +12 -0
- data/test/fixtures/migrations_with_decimal/1_give_me_big_numbers.rb +15 -0
- data/test/fixtures/migrations_with_duplicate/1_people_have_last_names.rb +9 -0
- data/test/fixtures/migrations_with_duplicate/2_we_need_reminders.rb +12 -0
- data/test/fixtures/migrations_with_duplicate/3_foo.rb +7 -0
- data/test/fixtures/migrations_with_duplicate/3_innocent_jointable.rb +12 -0
- data/test/fixtures/migrations_with_missing_versions/1000_people_have_middle_names.rb +9 -0
- data/test/fixtures/migrations_with_missing_versions/1_people_have_last_names.rb +9 -0
- data/test/fixtures/migrations_with_missing_versions/3_we_need_reminders.rb +12 -0
- data/test/fixtures/migrations_with_missing_versions/4_innocent_jointable.rb +12 -0
- data/test/fixtures/minimalistic.rb +2 -0
- data/test/fixtures/minimalistics.yml +2 -0
- data/test/fixtures/mixed_case_monkey.rb +3 -0
- data/test/fixtures/mixed_case_monkeys.yml +6 -0
- data/test/fixtures/mixins.yml +29 -0
- data/test/fixtures/movies.yml +7 -0
- data/test/fixtures/naked/csv/accounts.csv +1 -0
- data/test/fixtures/naked/yml/accounts.yml +1 -0
- data/test/fixtures/naked/yml/companies.yml +1 -0
- data/test/fixtures/naked/yml/courses.yml +1 -0
- data/test/fixtures/order.rb +4 -0
- data/test/fixtures/parrot.rb +13 -0
- data/test/fixtures/parrots.yml +27 -0
- data/test/fixtures/parrots_pirates.yml +7 -0
- data/test/fixtures/people.yml +3 -0
- data/test/fixtures/person.rb +4 -0
- data/test/fixtures/pirate.rb +5 -0
- data/test/fixtures/pirates.yml +9 -0
- data/test/fixtures/post.rb +59 -0
- data/test/fixtures/posts.yml +48 -0
- data/test/fixtures/project.rb +27 -2
- data/test/fixtures/projects.yml +7 -0
- data/test/fixtures/reader.rb +4 -0
- data/test/fixtures/readers.yml +4 -0
- data/test/fixtures/reply.rb +18 -2
- data/test/fixtures/reserved_words/distinct.yml +5 -0
- data/test/fixtures/reserved_words/distincts_selects.yml +11 -0
- data/test/fixtures/reserved_words/group.yml +14 -0
- data/test/fixtures/reserved_words/select.yml +8 -0
- data/test/fixtures/reserved_words/values.yml +7 -0
- data/test/fixtures/ship.rb +3 -0
- data/test/fixtures/ships.yml +5 -0
- data/test/fixtures/subject.rb +4 -0
- data/test/fixtures/subscriber.rb +4 -3
- data/test/fixtures/tag.rb +7 -0
- data/test/fixtures/tagging.rb +10 -0
- data/test/fixtures/taggings.yml +25 -0
- data/test/fixtures/tags.yml +7 -0
- data/test/fixtures/task.rb +3 -0
- data/test/fixtures/tasks.yml +7 -0
- data/test/fixtures/topic.rb +20 -3
- data/test/fixtures/topics.yml +22 -0
- data/test/fixtures/treasure.rb +4 -0
- data/test/fixtures/treasures.yml +10 -0
- data/test/fixtures/vertex.rb +9 -0
- data/test/fixtures/vertices.yml +4 -0
- data/test/fixtures_test.rb +574 -8
- data/test/inheritance_test.rb +113 -27
- data/test/json_serialization_test.rb +180 -0
- data/test/lifecycle_test.rb +56 -29
- data/test/locking_test.rb +273 -0
- data/test/method_scoping_test.rb +416 -0
- data/test/migration_test.rb +933 -0
- data/test/migration_test_firebird.rb +124 -0
- data/test/mixin_test.rb +95 -0
- data/test/modules_test.rb +23 -10
- data/test/multiple_db_test.rb +17 -3
- data/test/pk_test.rb +59 -15
- data/test/query_cache_test.rb +104 -0
- data/test/readonly_test.rb +107 -0
- data/test/reflection_test.rb +124 -27
- data/test/reserved_word_test_mysql.rb +177 -0
- data/test/schema_authorization_test_postgresql.rb +75 -0
- data/test/schema_dumper_test.rb +131 -0
- data/test/schema_test_postgresql.rb +64 -0
- data/test/serialization_test.rb +47 -0
- data/test/synonym_test_oracle.rb +17 -0
- data/test/table_name_test_sqlserver.rb +23 -0
- data/test/threaded_connections_test.rb +48 -0
- data/test/transactions_test.rb +227 -29
- data/test/unconnected_test.rb +14 -6
- data/test/validations_test.rb +1293 -32
- data/test/xml_serialization_test.rb +202 -0
- metadata +347 -143
- data/dev-utils/eval_debugger.rb +0 -9
- data/examples/associations.rb +0 -87
- data/examples/shared_setup.rb +0 -15
- data/examples/validation.rb +0 -88
- data/lib/active_record/deprecated_associations.rb +0 -70
- data/lib/active_record/support/class_attribute_accessors.rb +0 -43
- data/lib/active_record/support/class_inheritable_attributes.rb +0 -37
- data/lib/active_record/support/clean_logger.rb +0 -10
- data/lib/active_record/support/inflector.rb +0 -70
- data/lib/active_record/vendor/simple.rb +0 -702
- data/lib/active_record/wrappers/yaml_wrapper.rb +0 -15
- data/lib/active_record/wrappings.rb +0 -59
- data/rakefile +0 -122
- data/test/deprecated_associations_test.rb +0 -336
- data/test/fixtures/accounts/signals37 +0 -3
- data/test/fixtures/accounts/unknown +0 -2
- data/test/fixtures/companies/first_client +0 -6
- data/test/fixtures/companies/first_firm +0 -4
- data/test/fixtures/companies/second_client +0 -6
- data/test/fixtures/courses/java +0 -2
- data/test/fixtures/courses/ruby +0 -2
- data/test/fixtures/customers/david +0 -6
- data/test/fixtures/db_definitions/mysql.sql +0 -96
- data/test/fixtures/db_definitions/mysql2.sql +0 -4
- data/test/fixtures/developers/david +0 -2
- data/test/fixtures/developers/jamis +0 -2
- data/test/fixtures/entrants/first +0 -3
- data/test/fixtures/entrants/second +0 -3
- data/test/fixtures/entrants/third +0 -3
- data/test/fixtures/fixture_database.sqlite +0 -0
- data/test/fixtures/fixture_database_2.sqlite +0 -0
- data/test/fixtures/movies/first +0 -2
- data/test/fixtures/movies/second +0 -2
- data/test/fixtures/projects/action_controller +0 -2
- data/test/fixtures/projects/active_record +0 -2
- data/test/fixtures/topics/first +0 -9
- data/test/fixtures/topics/second +0 -8
- data/test/inflector_test.rb +0 -104
- data/test/thread_safety_test.rb +0 -33
@@ -1,14 +1,19 @@
|
|
1
1
|
module ActiveRecord
|
2
2
|
module Aggregations # :nodoc:
|
3
|
-
def self.
|
4
|
-
super
|
3
|
+
def self.included(base)
|
5
4
|
base.extend(ClassMethods)
|
6
5
|
end
|
7
6
|
|
7
|
+
def clear_aggregation_cache #:nodoc:
|
8
|
+
self.class.reflect_on_all_aggregations.to_a.each do |assoc|
|
9
|
+
instance_variable_set "@#{assoc.name}", nil
|
10
|
+
end unless self.new_record?
|
11
|
+
end
|
12
|
+
|
8
13
|
# Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes
|
9
14
|
# 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
|
11
|
-
# attributes of the entity object (when the entity is initialized either as a new object or from finding an existing)
|
15
|
+
# composed of [an] address". Each call to the macro adds a description of how the value objects are created from the
|
16
|
+
# attributes of the entity object (when the entity is initialized either as a new object or from finding an existing object)
|
12
17
|
# and how it can be turned back into attributes (when the entity is saved to the database). Example:
|
13
18
|
#
|
14
19
|
# class Customer < ActiveRecord::Base
|
@@ -42,7 +47,7 @@ module ActiveRecord
|
|
42
47
|
#
|
43
48
|
# def <=>(other_money)
|
44
49
|
# if currency == other_money.currency
|
45
|
-
#
|
50
|
+
# amount <=> amount
|
46
51
|
# else
|
47
52
|
# amount <=> other_money.exchange_to(currency).amount
|
48
53
|
# end
|
@@ -65,7 +70,7 @@ module ActiveRecord
|
|
65
70
|
# end
|
66
71
|
#
|
67
72
|
# 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
|
73
|
+
# composition the same as the attribute's name, it will be the only way to access that attribute. That's the case with our
|
69
74
|
# +balance+ attribute. You interact with the value objects just like you would any other attribute, though:
|
70
75
|
#
|
71
76
|
# customer.balance = Money.new(20) # sets the Money value object and the attribute
|
@@ -87,78 +92,88 @@ module ActiveRecord
|
|
87
92
|
#
|
88
93
|
# == Writing value objects
|
89
94
|
#
|
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
|
92
|
-
# sense). This is unlike
|
95
|
+
# Value objects are immutable and interchangeable objects that represent a given value, such as a +Money+ object representing
|
96
|
+
# $5. Two +Money+ objects both representing $5 should be equal (through methods such as == and <=> from +Comparable+ if ranking
|
97
|
+
# makes sense). This is unlike entity objects where equality is determined by identity. An entity class such as +Customer+ can
|
93
98
|
# 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.
|
99
|
+
# relational unique identifiers (such as primary keys). Normal <tt>ActiveRecord::Base</tt> classes are entity objects.
|
95
100
|
#
|
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
|
101
|
+
# It's also important to treat the value objects as immutable. Don't allow the +Money+ object to have its amount changed after
|
102
|
+
# creation. Create a new +Money+ object with the new value instead. This is exemplified by the <tt>Money#exchanged_to</tt> method that
|
98
103
|
# 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
|
104
|
+
# changed through means other than the writer method.
|
100
105
|
#
|
101
106
|
# 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
|
107
|
+
# change it afterwards will result in a <tt>TypeError</tt>.
|
103
108
|
#
|
104
109
|
# Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not keeping value objects
|
105
110
|
# immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
|
106
111
|
module ClassMethods
|
107
|
-
# Adds
|
108
|
-
# <tt>composed_of :address</tt>
|
112
|
+
# Adds reader and writer methods for manipulating a value object:
|
113
|
+
# <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods.
|
109
114
|
#
|
110
115
|
# Options are:
|
111
|
-
# * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be
|
116
|
+
# * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be inferred
|
112
117
|
# from the part id. So <tt>composed_of :address</tt> will by default be linked to the +Address+ class, but
|
113
118
|
# if the real class name is +CompanyAddress+, you'll have to specify it with this option.
|
114
119
|
# * <tt>:mapping</tt> - specifies a number of mapping arrays (attribute, parameter) that bind an attribute name
|
115
120
|
# to a constructor parameter on the value class.
|
121
|
+
# * <tt>:allow_nil</tt> - specifies that the aggregate object will not be instantiated when all mapped
|
122
|
+
# attributes are +nil+. Setting the aggregate class to +nil+ has the effect of writing +nil+ to all mapped attributes.
|
123
|
+
# This defaults to +false+.
|
124
|
+
#
|
125
|
+
# An optional block can be passed to convert the argument that is passed to the writer method into an instance of
|
126
|
+
# <tt>:class_name</tt>. The block will only be called if the argument is not already an instance of <tt>:class_name</tt>.
|
116
127
|
#
|
117
128
|
# Option examples:
|
118
129
|
# composed_of :temperature, :mapping => %w(reading celsius)
|
119
|
-
# composed_of
|
130
|
+
# composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) {|balance| balance.to_money }
|
120
131
|
# composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
|
121
|
-
|
122
|
-
|
132
|
+
# composed_of :gps_location
|
133
|
+
# composed_of :gps_location, :allow_nil => true
|
134
|
+
#
|
135
|
+
def composed_of(part_id, options = {}, &block)
|
136
|
+
options.assert_valid_keys(:class_name, :mapping, :allow_nil)
|
123
137
|
|
124
138
|
name = part_id.id2name
|
125
|
-
class_name = options[:class_name] ||
|
126
|
-
mapping = options[:mapping]
|
139
|
+
class_name = options[:class_name] || name.camelize
|
140
|
+
mapping = options[:mapping] || [ name, name ]
|
141
|
+
mapping = [ mapping ] unless mapping.first.is_a?(Array)
|
142
|
+
allow_nil = options[:allow_nil] || false
|
127
143
|
|
128
|
-
reader_method(name, class_name, mapping)
|
129
|
-
writer_method(name, class_name, mapping)
|
144
|
+
reader_method(name, class_name, mapping, allow_nil)
|
145
|
+
writer_method(name, class_name, mapping, allow_nil, block)
|
146
|
+
|
147
|
+
create_reflection(:composed_of, part_id, options, self)
|
130
148
|
end
|
131
149
|
|
132
150
|
private
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
151
|
+
def reader_method(name, class_name, mapping, allow_nil)
|
152
|
+
module_eval do
|
153
|
+
define_method(name) do |*args|
|
154
|
+
force_reload = args.first || false
|
155
|
+
if (instance_variable_get("@#{name}").nil? || force_reload) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? })
|
156
|
+
instance_variable_set("@#{name}", class_name.constantize.new(*mapping.collect {|pair| read_attribute(pair.first)}))
|
157
|
+
end
|
158
|
+
return instance_variable_get("@#{name}")
|
159
|
+
end
|
160
|
+
end
|
138
161
|
|
139
|
-
def name_to_class_name(name)
|
140
|
-
name.capitalize.gsub(/_(.)/) { |s| $1.capitalize }
|
141
162
|
end
|
142
|
-
|
143
|
-
def
|
144
|
-
module_eval
|
145
|
-
|
146
|
-
if
|
147
|
-
|
163
|
+
|
164
|
+
def writer_method(name, class_name, mapping, allow_nil, conversion)
|
165
|
+
module_eval do
|
166
|
+
define_method("#{name}=") do |part|
|
167
|
+
if part.nil? && allow_nil
|
168
|
+
mapping.each { |pair| @attributes[pair.first] = nil }
|
169
|
+
instance_variable_set("@#{name}", nil)
|
170
|
+
else
|
171
|
+
part = conversion.call(part) unless part.is_a?(class_name.constantize) || conversion.nil?
|
172
|
+
mapping.each { |pair| @attributes[pair.first] = part.send(pair.last) }
|
173
|
+
instance_variable_set("@#{name}", part.freeze)
|
148
174
|
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
175
|
end
|
161
|
-
|
176
|
+
end
|
162
177
|
end
|
163
178
|
end
|
164
179
|
end
|
@@ -1,70 +1,240 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
1
3
|
module ActiveRecord
|
2
4
|
module Associations
|
3
|
-
class AssociationCollection #:nodoc:
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
|
8
|
-
@owner = owner
|
9
|
-
@options = options
|
10
|
-
@association_name = association_name
|
11
|
-
@association_class = eval(association_class_name)
|
12
|
-
@association_class_primary_key_name = association_class_primary_key_name
|
5
|
+
class AssociationCollection < AssociationProxy #:nodoc:
|
6
|
+
def to_ary
|
7
|
+
load_target
|
8
|
+
@target.to_ary
|
13
9
|
end
|
14
|
-
|
15
|
-
def
|
16
|
-
|
17
|
-
@
|
10
|
+
|
11
|
+
def reset
|
12
|
+
reset_target!
|
13
|
+
@loaded = false
|
18
14
|
end
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
15
|
+
|
16
|
+
# Add +records+ to this association. Returns +self+ so method calls may be chained.
|
17
|
+
# Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
|
18
|
+
def <<(*records)
|
19
|
+
result = true
|
20
|
+
load_target if @owner.new_record?
|
21
|
+
|
22
|
+
@owner.transaction do
|
23
|
+
flatten_deeper(records).each do |record|
|
24
|
+
raise_on_type_mismatch(record)
|
25
|
+
callback(:before_add, record)
|
26
|
+
result &&= insert_record(record) unless @owner.new_record?
|
27
|
+
@target << record
|
28
|
+
callback(:after_add, record)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
result && self
|
23
33
|
end
|
24
|
-
|
25
|
-
|
26
|
-
|
34
|
+
|
35
|
+
alias_method :push, :<<
|
36
|
+
alias_method :concat, :<<
|
37
|
+
|
38
|
+
# Remove all records from this association
|
39
|
+
def delete_all
|
40
|
+
load_target
|
41
|
+
delete(@target)
|
42
|
+
reset_target!
|
27
43
|
end
|
28
|
-
|
29
|
-
|
30
|
-
|
44
|
+
|
45
|
+
# Calculate sum using SQL, not Enumerable
|
46
|
+
def sum(*args, &block)
|
47
|
+
calculate(:sum, *args, &block)
|
31
48
|
end
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
records
|
49
|
+
|
50
|
+
# Remove +records+ from this association. Does not destroy +records+.
|
51
|
+
def delete(*records)
|
52
|
+
records = flatten_deeper(records)
|
53
|
+
records.each { |record| raise_on_type_mismatch(record) }
|
54
|
+
records.reject! { |record| @target.delete(record) if record.new_record? }
|
55
|
+
return if records.empty?
|
56
|
+
|
57
|
+
@owner.transaction do
|
58
|
+
records.each { |record| callback(:before_remove, record) }
|
59
|
+
delete_records(records)
|
60
|
+
records.each do |record|
|
61
|
+
@target.delete(record)
|
62
|
+
callback(:after_remove, record)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Removes all records from this association. Returns +self+ so method calls may be chained.
|
68
|
+
def clear
|
69
|
+
return self if length.zero? # forces load_target if it hasn't happened already
|
70
|
+
|
71
|
+
if @reflection.options[:dependent] && @reflection.options[:dependent] == :destroy
|
72
|
+
destroy_all
|
73
|
+
else
|
74
|
+
delete_all
|
75
|
+
end
|
76
|
+
|
77
|
+
self
|
36
78
|
end
|
37
79
|
|
38
80
|
def destroy_all
|
39
|
-
|
40
|
-
|
41
|
-
|
81
|
+
@owner.transaction do
|
82
|
+
each { |record| record.destroy }
|
83
|
+
end
|
84
|
+
|
85
|
+
reset_target!
|
42
86
|
end
|
43
87
|
|
88
|
+
def create(attrs = {})
|
89
|
+
if attrs.is_a?(Array)
|
90
|
+
attrs.collect { |attr| create(attr) }
|
91
|
+
else
|
92
|
+
create_record(attrs) { |record| record.save }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def create!(attrs = {})
|
97
|
+
create_record(attrs) { |record| record.save! }
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
|
101
|
+
# calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
|
102
|
+
# and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
|
44
103
|
def size
|
45
|
-
|
104
|
+
if @owner.new_record? || (loaded? && !@reflection.options[:uniq])
|
105
|
+
@target.size
|
106
|
+
elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
|
107
|
+
unsaved_records = Array(@target.detect { |r| r.new_record? })
|
108
|
+
unsaved_records.size + count_records
|
109
|
+
else
|
110
|
+
count_records
|
111
|
+
end
|
46
112
|
end
|
47
|
-
|
113
|
+
|
114
|
+
# Returns the size of the collection by loading it and calling size on the array. If you want to use this method to check
|
115
|
+
# whether the collection is empty, use collection.length.zero? instead of collection.empty?
|
116
|
+
def length
|
117
|
+
load_target.size
|
118
|
+
end
|
119
|
+
|
48
120
|
def empty?
|
49
|
-
size
|
121
|
+
size.zero?
|
50
122
|
end
|
51
|
-
|
52
|
-
|
53
|
-
|
123
|
+
|
124
|
+
def any?(&block)
|
125
|
+
if block_given?
|
126
|
+
method_missing(:any?, &block)
|
127
|
+
else
|
128
|
+
!empty?
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def uniq(collection = self)
|
133
|
+
seen = Set.new
|
134
|
+
collection.inject([]) do |kept, record|
|
135
|
+
unless seen.include?(record.id)
|
136
|
+
kept << record
|
137
|
+
seen << record.id
|
138
|
+
end
|
139
|
+
kept
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Replace this collection with +other_array+
|
144
|
+
# This will perform a diff and delete/add only records that have changed.
|
145
|
+
def replace(other_array)
|
146
|
+
other_array.each { |val| raise_on_type_mismatch(val) }
|
147
|
+
|
148
|
+
load_target
|
149
|
+
other = other_array.size < 100 ? other_array : other_array.to_set
|
150
|
+
current = @target.size < 100 ? @target : @target.to_set
|
151
|
+
|
152
|
+
@owner.transaction do
|
153
|
+
delete(@target.select { |v| !other.include?(v) })
|
154
|
+
concat(other_array.select { |v| !current.include?(v) })
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
|
159
|
+
protected
|
160
|
+
def method_missing(method, *args, &block)
|
161
|
+
if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
|
162
|
+
super
|
163
|
+
else
|
164
|
+
@reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.send(method, *args, &block) }
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# overloaded in derived Association classes to provide useful scoping depending on association type.
|
169
|
+
def construct_scope
|
170
|
+
{}
|
171
|
+
end
|
172
|
+
|
173
|
+
def reset_target!
|
174
|
+
@target = Array.new
|
175
|
+
end
|
176
|
+
|
177
|
+
def find_target
|
178
|
+
records =
|
179
|
+
if @reflection.options[:finder_sql]
|
180
|
+
@reflection.klass.find_by_sql(@finder_sql)
|
181
|
+
else
|
182
|
+
find(:all)
|
183
|
+
end
|
184
|
+
|
185
|
+
@reflection.options[:uniq] ? uniq(records) : records
|
186
|
+
end
|
187
|
+
|
54
188
|
private
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
@collection_array = []
|
61
|
-
end
|
189
|
+
|
190
|
+
def create_record(attrs, &block)
|
191
|
+
ensure_owner_is_not_new
|
192
|
+
record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { @reflection.klass.new(attrs) }
|
193
|
+
add_record_to_target_with_callbacks(record, &block)
|
62
194
|
end
|
195
|
+
|
196
|
+
def build_record(attrs, &block)
|
197
|
+
record = @reflection.klass.new(attrs)
|
198
|
+
add_record_to_target_with_callbacks(record, &block)
|
199
|
+
end
|
200
|
+
|
201
|
+
def add_record_to_target_with_callbacks(record)
|
202
|
+
callback(:before_add, record)
|
203
|
+
yield(record) if block_given?
|
204
|
+
@target ||= [] unless loaded?
|
205
|
+
@target << record
|
206
|
+
callback(:after_add, record)
|
207
|
+
record
|
208
|
+
end
|
209
|
+
|
210
|
+
def callback(method, record)
|
211
|
+
callbacks_for(method).each do |callback|
|
212
|
+
case callback
|
213
|
+
when Symbol
|
214
|
+
@owner.send(callback, record)
|
215
|
+
when Proc, Method
|
216
|
+
callback.call(@owner, record)
|
217
|
+
else
|
218
|
+
if callback.respond_to?(method)
|
219
|
+
callback.send(method, @owner, record)
|
220
|
+
else
|
221
|
+
raise ActiveRecordError, "Callbacks must be a symbol denoting the method to call, a string to be evaluated, a block to be invoked, or an object responding to the callback method."
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def callbacks_for(callback_name)
|
228
|
+
full_callback_name = "#{callback_name}_for_#{@reflection.name}"
|
229
|
+
@owner.class.read_inheritable_attribute(full_callback_name.to_sym) || []
|
230
|
+
end
|
63
231
|
|
64
|
-
def
|
65
|
-
|
66
|
-
|
232
|
+
def ensure_owner_is_not_new
|
233
|
+
if @owner.new_record?
|
234
|
+
raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
|
235
|
+
end
|
67
236
|
end
|
237
|
+
|
68
238
|
end
|
69
239
|
end
|
70
240
|
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class AssociationProxy #:nodoc:
|
4
|
+
attr_reader :reflection
|
5
|
+
alias_method :proxy_respond_to?, :respond_to?
|
6
|
+
alias_method :proxy_extend, :extend
|
7
|
+
delegate :to_param, :to => :proxy_target
|
8
|
+
instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_)/ }
|
9
|
+
|
10
|
+
def initialize(owner, reflection)
|
11
|
+
@owner, @reflection = owner, reflection
|
12
|
+
Array(reflection.options[:extend]).each { |ext| proxy_extend(ext) }
|
13
|
+
reset
|
14
|
+
end
|
15
|
+
|
16
|
+
def proxy_owner
|
17
|
+
@owner
|
18
|
+
end
|
19
|
+
|
20
|
+
def proxy_reflection
|
21
|
+
@reflection
|
22
|
+
end
|
23
|
+
|
24
|
+
def proxy_target
|
25
|
+
@target
|
26
|
+
end
|
27
|
+
|
28
|
+
def respond_to?(symbol, include_priv = false)
|
29
|
+
proxy_respond_to?(symbol, include_priv) || (load_target && @target.respond_to?(symbol, include_priv))
|
30
|
+
end
|
31
|
+
|
32
|
+
# Explicitly proxy === because the instance method removal above
|
33
|
+
# doesn't catch it.
|
34
|
+
def ===(other)
|
35
|
+
load_target
|
36
|
+
other === @target
|
37
|
+
end
|
38
|
+
|
39
|
+
def aliased_table_name
|
40
|
+
@reflection.klass.table_name
|
41
|
+
end
|
42
|
+
|
43
|
+
def conditions
|
44
|
+
@conditions ||= interpolate_sql(sanitize_sql(@reflection.options[:conditions])) if @reflection.options[:conditions]
|
45
|
+
end
|
46
|
+
alias :sql_conditions :conditions
|
47
|
+
|
48
|
+
def reset
|
49
|
+
@loaded = false
|
50
|
+
@target = nil
|
51
|
+
end
|
52
|
+
|
53
|
+
def reload
|
54
|
+
reset
|
55
|
+
load_target
|
56
|
+
end
|
57
|
+
|
58
|
+
def loaded?
|
59
|
+
@loaded
|
60
|
+
end
|
61
|
+
|
62
|
+
def loaded
|
63
|
+
@loaded = true
|
64
|
+
end
|
65
|
+
|
66
|
+
def target
|
67
|
+
@target
|
68
|
+
end
|
69
|
+
|
70
|
+
def target=(target)
|
71
|
+
@target = target
|
72
|
+
loaded
|
73
|
+
end
|
74
|
+
|
75
|
+
def inspect
|
76
|
+
reload unless loaded?
|
77
|
+
@target.inspect
|
78
|
+
end
|
79
|
+
|
80
|
+
protected
|
81
|
+
def dependent?
|
82
|
+
@reflection.options[:dependent]
|
83
|
+
end
|
84
|
+
|
85
|
+
def quoted_record_ids(records)
|
86
|
+
records.map { |record| record.quoted_id }.join(',')
|
87
|
+
end
|
88
|
+
|
89
|
+
def interpolate_sql_options!(options, *keys)
|
90
|
+
keys.each { |key| options[key] &&= interpolate_sql(options[key]) }
|
91
|
+
end
|
92
|
+
|
93
|
+
def interpolate_sql(sql, record = nil)
|
94
|
+
@owner.send(:interpolate_sql, sql, record)
|
95
|
+
end
|
96
|
+
|
97
|
+
def sanitize_sql(sql)
|
98
|
+
@reflection.klass.send(:sanitize_sql, sql)
|
99
|
+
end
|
100
|
+
|
101
|
+
def set_belongs_to_association_for(record)
|
102
|
+
if @reflection.options[:as]
|
103
|
+
record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record?
|
104
|
+
record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
|
105
|
+
else
|
106
|
+
record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def merge_options_from_reflection!(options)
|
111
|
+
options.reverse_merge!(
|
112
|
+
:group => @reflection.options[:group],
|
113
|
+
:limit => @reflection.options[:limit],
|
114
|
+
:offset => @reflection.options[:offset],
|
115
|
+
:joins => @reflection.options[:joins],
|
116
|
+
:include => @reflection.options[:include],
|
117
|
+
:select => @reflection.options[:select]
|
118
|
+
)
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
def method_missing(method, *args, &block)
|
123
|
+
if load_target
|
124
|
+
@target.send(method, *args, &block)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def load_target
|
129
|
+
return nil unless defined?(@loaded)
|
130
|
+
|
131
|
+
if !loaded? and (!@owner.new_record? || foreign_key_present)
|
132
|
+
@target = find_target
|
133
|
+
end
|
134
|
+
|
135
|
+
@loaded = true
|
136
|
+
@target
|
137
|
+
rescue ActiveRecord::RecordNotFound
|
138
|
+
reset
|
139
|
+
end
|
140
|
+
|
141
|
+
# Can be overwritten by associations that might have the foreign key available for an association without
|
142
|
+
# having the object itself (and still being a new record). Currently, only belongs_to presents this scenario.
|
143
|
+
def foreign_key_present
|
144
|
+
false
|
145
|
+
end
|
146
|
+
|
147
|
+
def raise_on_type_mismatch(record)
|
148
|
+
unless record.is_a?(@reflection.klass)
|
149
|
+
raise ActiveRecord::AssociationTypeMismatch, "#{@reflection.klass} expected, got #{record.class}"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Array#flatten has problems with recursive arrays. Going one level deeper solves the majority of the problems.
|
154
|
+
def flatten_deeper(array)
|
155
|
+
array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class BelongsToAssociation < AssociationProxy #:nodoc:
|
4
|
+
def create(attributes = {})
|
5
|
+
replace(@reflection.klass.create(attributes))
|
6
|
+
end
|
7
|
+
|
8
|
+
def build(attributes = {})
|
9
|
+
replace(@reflection.klass.new(attributes))
|
10
|
+
end
|
11
|
+
|
12
|
+
def replace(record)
|
13
|
+
counter_cache_name = @reflection.counter_cache_column
|
14
|
+
|
15
|
+
if record.nil?
|
16
|
+
if counter_cache_name && @owner[counter_cache_name] && !@owner.new_record?
|
17
|
+
@reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name]
|
18
|
+
end
|
19
|
+
|
20
|
+
@target = @owner[@reflection.primary_key_name] = nil
|
21
|
+
else
|
22
|
+
raise_on_type_mismatch(record)
|
23
|
+
|
24
|
+
if counter_cache_name && !@owner.new_record?
|
25
|
+
@reflection.klass.increment_counter(counter_cache_name, record.id)
|
26
|
+
@reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name]
|
27
|
+
end
|
28
|
+
|
29
|
+
@target = (AssociationProxy === record ? record.target : record)
|
30
|
+
@owner[@reflection.primary_key_name] = record.id unless record.new_record?
|
31
|
+
@updated = true
|
32
|
+
end
|
33
|
+
|
34
|
+
loaded
|
35
|
+
record
|
36
|
+
end
|
37
|
+
|
38
|
+
def updated?
|
39
|
+
@updated
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def find_target
|
44
|
+
@reflection.klass.find(
|
45
|
+
@owner[@reflection.primary_key_name],
|
46
|
+
:conditions => conditions,
|
47
|
+
:include => @reflection.options[:include]
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
def foreign_key_present
|
52
|
+
!@owner[@reflection.primary_key_name].nil?
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|