datamapper 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +5 -1
- data/FAQ +96 -0
- data/QUICKLINKS +12 -0
- data/README +57 -155
- data/environment.rb +61 -43
- data/example.rb +30 -12
- data/lib/data_mapper.rb +6 -1
- data/lib/data_mapper/adapters/abstract_adapter.rb +0 -57
- data/lib/data_mapper/adapters/data_object_adapter.rb +203 -97
- data/lib/data_mapper/adapters/mysql_adapter.rb +4 -0
- data/lib/data_mapper/adapters/postgresql_adapter.rb +7 -1
- data/lib/data_mapper/adapters/sql/coersion.rb +3 -2
- data/lib/data_mapper/adapters/sql/commands/load_command.rb +29 -10
- data/lib/data_mapper/adapters/sql/mappings/associations_set.rb +4 -0
- data/lib/data_mapper/adapters/sql/mappings/column.rb +13 -9
- data/lib/data_mapper/adapters/sql/mappings/conditions.rb +172 -0
- data/lib/data_mapper/adapters/sql/mappings/table.rb +43 -17
- data/lib/data_mapper/adapters/sqlite3_adapter.rb +9 -2
- data/lib/data_mapper/associations.rb +75 -3
- data/lib/data_mapper/associations/belongs_to_association.rb +70 -36
- data/lib/data_mapper/associations/has_and_belongs_to_many_association.rb +195 -86
- data/lib/data_mapper/associations/has_many_association.rb +168 -61
- data/lib/data_mapper/associations/has_n_association.rb +23 -3
- data/lib/data_mapper/attributes.rb +73 -0
- data/lib/data_mapper/auto_migrations.rb +2 -6
- data/lib/data_mapper/base.rb +5 -9
- data/lib/data_mapper/database.rb +4 -3
- data/lib/data_mapper/embedded_value.rb +66 -30
- data/lib/data_mapper/identity_map.rb +1 -3
- data/lib/data_mapper/is/tree.rb +121 -0
- data/lib/data_mapper/migration.rb +155 -0
- data/lib/data_mapper/persistence.rb +532 -218
- data/lib/data_mapper/property.rb +306 -0
- data/lib/data_mapper/query.rb +164 -0
- data/lib/data_mapper/support/blank.rb +2 -2
- data/lib/data_mapper/support/connection_pool.rb +5 -6
- data/lib/data_mapper/support/enumerable.rb +3 -3
- data/lib/data_mapper/support/errors.rb +10 -1
- data/lib/data_mapper/support/inflector.rb +174 -238
- data/lib/data_mapper/support/object.rb +54 -0
- data/lib/data_mapper/support/serialization.rb +19 -1
- data/lib/data_mapper/support/string.rb +7 -16
- data/lib/data_mapper/support/symbol.rb +3 -15
- data/lib/data_mapper/support/typed_set.rb +68 -0
- data/lib/data_mapper/types/base.rb +44 -0
- data/lib/data_mapper/types/string.rb +34 -0
- data/lib/data_mapper/validations/number_validator.rb +40 -0
- data/lib/data_mapper/validations/string_validator.rb +20 -0
- data/lib/data_mapper/validations/validator.rb +13 -0
- data/performance.rb +26 -1
- data/profile_data_mapper.rb +1 -1
- data/rakefile.rb +42 -2
- data/spec/acts_as_tree_spec.rb +11 -3
- data/spec/adapters/data_object_adapter_spec.rb +31 -0
- data/spec/associations/belongs_to_association_spec.rb +98 -0
- data/spec/associations/has_and_belongs_to_many_association_spec.rb +377 -0
- data/spec/associations/has_many_association_spec.rb +337 -0
- data/spec/attributes_spec.rb +23 -1
- data/spec/auto_migrations_spec.rb +86 -29
- data/spec/callbacks_spec.rb +107 -0
- data/spec/column_spec.rb +5 -2
- data/spec/count_command_spec.rb +33 -1
- data/spec/database_spec.rb +18 -0
- data/spec/dependency_spec.rb +4 -2
- data/spec/embedded_value_spec.rb +8 -8
- data/spec/fixtures/people.yaml +1 -1
- data/spec/fixtures/projects.yaml +10 -1
- data/spec/fixtures/tasks.yaml +6 -0
- data/spec/fixtures/tasks_tasks.yaml +2 -0
- data/spec/fixtures/tomatoes.yaml +1 -0
- data/spec/is_a_tree_spec.rb +149 -0
- data/spec/load_command_spec.rb +71 -9
- data/spec/magic_columns_spec.rb +17 -2
- data/spec/migration_spec.rb +267 -0
- data/spec/models/animal.rb +1 -1
- data/spec/models/candidate.rb +8 -0
- data/spec/models/career.rb +1 -1
- data/spec/models/chain.rb +8 -0
- data/spec/models/comment.rb +1 -1
- data/spec/models/exhibit.rb +1 -1
- data/spec/models/fence.rb +7 -0
- data/spec/models/fruit.rb +2 -2
- data/spec/models/job.rb +8 -0
- data/spec/models/person.rb +2 -3
- data/spec/models/post.rb +1 -1
- data/spec/models/project.rb +21 -1
- data/spec/models/section.rb +1 -1
- data/spec/models/serializer.rb +1 -1
- data/spec/models/task.rb +9 -0
- data/spec/models/tomato.rb +27 -0
- data/spec/models/user.rb +8 -2
- data/spec/models/zoo.rb +2 -7
- data/spec/paranoia_spec.rb +1 -1
- data/spec/{base_spec.rb → persistence_spec.rb} +207 -18
- data/spec/postgres_spec.rb +48 -6
- data/spec/property_spec.rb +90 -9
- data/spec/query_spec.rb +71 -5
- data/spec/save_command_spec.rb +11 -0
- data/spec/spec_helper.rb +14 -11
- data/spec/support/blank_spec.rb +8 -0
- data/spec/support/inflector_spec.rb +41 -0
- data/spec/support/object_spec.rb +9 -0
- data/spec/{serialization_spec.rb → support/serialization_spec.rb} +1 -1
- data/spec/support/silence_spec.rb +15 -0
- data/spec/{support_spec.rb → support/string_spec.rb} +3 -3
- data/spec/support/struct_spec.rb +12 -0
- data/spec/support/typed_set_spec.rb +66 -0
- data/spec/table_spec.rb +3 -3
- data/spec/types/string.rb +81 -0
- data/spec/validates_uniqueness_of_spec.rb +17 -0
- data/spec/validations/number_validator.rb +59 -0
- data/spec/validations/string_validator.rb +14 -0
- metadata +59 -17
- data/do_performance.rb +0 -153
- data/lib/data_mapper/support/active_record_impersonation.rb +0 -103
- data/lib/data_mapper/support/weak_hash.rb +0 -46
- data/spec/active_record_impersonation_spec.rb +0 -129
- data/spec/associations_spec.rb +0 -232
- data/spec/conditions_spec.rb +0 -49
- data/spec/has_many_association_spec.rb +0 -173
- data/spec/models/animals_exhibit.rb +0 -8
@@ -24,11 +24,13 @@ module DataMapper
|
|
24
24
|
|
25
25
|
TABLE_QUOTING_CHARACTER = '"'.freeze
|
26
26
|
COLUMN_QUOTING_CHARACTER = '"'.freeze
|
27
|
+
TRUE_ALIASES << 't'.freeze
|
28
|
+
FALSE_ALIASES << 'f'.freeze
|
27
29
|
|
28
30
|
def create_connection
|
29
31
|
conn = DataObject::Sqlite3::Connection.new("dbname=#{@configuration.database}")
|
30
32
|
conn.logger = self.logger
|
31
|
-
conn.open
|
33
|
+
conn.open if conn.respond_to?(:open)
|
32
34
|
return conn
|
33
35
|
end
|
34
36
|
|
@@ -65,6 +67,11 @@ module DataMapper
|
|
65
67
|
PRAGMA TABLE_INFO(?)
|
66
68
|
EOS
|
67
69
|
end
|
70
|
+
|
71
|
+
def to_truncate_sql
|
72
|
+
"DELETE FROM #{to_sql}"
|
73
|
+
end
|
74
|
+
|
68
75
|
alias_method :to_columns_sql, :to_column_exists_sql
|
69
76
|
|
70
77
|
def unquote_default(default)
|
@@ -148,4 +155,4 @@ module DataMapper
|
|
148
155
|
end # class Sqlite3Adapter
|
149
156
|
|
150
157
|
end # module Adapters
|
151
|
-
end # module DataMapper
|
158
|
+
end # module DataMapper
|
@@ -5,25 +5,97 @@ require 'data_mapper/associations/has_and_belongs_to_many_association'
|
|
5
5
|
|
6
6
|
module DataMapper
|
7
7
|
module Associations
|
8
|
-
|
8
|
+
|
9
|
+
# Extends +base+ with methods for setting up associations between different models.
|
9
10
|
def self.included(base)
|
10
11
|
base.extend(ClassMethods)
|
11
12
|
end
|
12
13
|
|
13
14
|
module ClassMethods
|
14
|
-
|
15
|
+
# Adds the following methods for query of a single associated object:
|
16
|
+
# * <tt>collection(</tt> - returns a set containing the associated objects. Returns
|
17
|
+
# an empty set if no objects are found.
|
18
|
+
# * <tt>collection << object</tt> - adds an object to the collection.
|
19
|
+
# * <tt>collection = [objects]</tt> - replaces the collections content by deleting and
|
20
|
+
# adding objects as appropriate.
|
21
|
+
# * <tt>collection.empty?</tt> - returns +true+ if there is no associated objects.
|
22
|
+
# * <tt>collection.size</tt> - returns the number of associated objects.
|
23
|
+
#
|
24
|
+
# Options are:
|
25
|
+
# * <tt>:class</tt> - specify the class name of the association. So has_many :animals will by
|
26
|
+
# default be linked to the Animal class, but if you want the association to use a
|
27
|
+
# different class, you'll have to specify it with this option. DM also lets you specify
|
28
|
+
# this with <tt>:class_name</tt>, for AR compability.
|
29
|
+
# * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default
|
30
|
+
# this is guessed to be the name of this class in lower-case and _id suffixed.
|
31
|
+
# * <tt>:dependent</tt> - if set to :destroy, the associated objects have their destroy! methods
|
32
|
+
# called in a chain meaning all callbacks are also called for each object.
|
33
|
+
# if set to :delete, the associated objects are deleted from the database
|
34
|
+
# without their callbacks being triggered.
|
35
|
+
# if set to :protect and the collection is not empty an AssociatedProtectedError will be raised.
|
36
|
+
# if set to :nullify, the associated objects foreign key is set to NULL.
|
37
|
+
# default is :nullify
|
38
|
+
#
|
39
|
+
# Option examples:
|
40
|
+
# has_many :favourite_fruits, :class => 'Fruit', :dependent => :destroy
|
15
41
|
def has_many(association_name, options = {})
|
16
42
|
database.schema[self].associations << HasManyAssociation.new(self, association_name, options)
|
17
43
|
end
|
18
44
|
|
45
|
+
# Adds the following methods for query of a single associated object:
|
46
|
+
# * <tt>association(</tt> - returns the associated object. Returns an empty set if no
|
47
|
+
# object is found.
|
48
|
+
# * <tt>association=(associate)</tt> - assigns the associate object, extracts the
|
49
|
+
# primary key, and sets it as the foreign key.
|
50
|
+
# * <tt>association.nil?</tt> - returns +true+ if there is no associated object.
|
51
|
+
#
|
52
|
+
# The declaration can also include an options hash to specialize the behavior of the
|
53
|
+
# association.
|
54
|
+
#
|
55
|
+
# Options are:
|
56
|
+
# * <tt>:class</tt> - specify the class name of the association. So has_one :animal will by
|
57
|
+
# default be linked to the Animal class, but if you want the association to use a
|
58
|
+
# different class, you'll have to specify it with this option. DM also lets you specify
|
59
|
+
# this with <tt>:class_name</tt>, for AR compability.
|
60
|
+
# * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default
|
61
|
+
# this is guessed to be the name of this class in lower-case and _id suffixed.
|
62
|
+
# * <tt>:dependent</tt> - has_one is secretly a has_many so this option performs the same
|
63
|
+
# as the has_many
|
64
|
+
#
|
65
|
+
# Option examples:
|
66
|
+
# has_one :favourite_fruit, :class => 'Fruit', :foreign_key => 'devourer_id'
|
19
67
|
def has_one(association_name, options = {})
|
20
68
|
database.schema[self].associations << HasManyAssociation.new(self, association_name, options)
|
21
69
|
end
|
22
70
|
|
71
|
+
# Adds the following methods for query of a single associated object:
|
72
|
+
# * <tt>association(</tt> - returns the associated object. Returns an empty set if no
|
73
|
+
# object is found.
|
74
|
+
# * <tt>association=(associate)</tt> - assigns the associate object, extracts the
|
75
|
+
# primary key, and sets it as the foreign key.
|
76
|
+
# * <tt>association.nil?</tt> - returns +true+ if there is no associated object.
|
77
|
+
# * <tt>build_association</tt> - builds a new object of the associated type, without
|
78
|
+
# saving it to the database.
|
79
|
+
# * <tt>create_association</tt> - creates and saves a new object of the associated type.
|
23
80
|
def belongs_to(association_name, options = {})
|
24
81
|
database.schema[self].associations << BelongsToAssociation.new(self, association_name, options)
|
25
82
|
end
|
26
83
|
|
84
|
+
# Associates two classes via an intermediate join table.
|
85
|
+
#
|
86
|
+
# Options are:
|
87
|
+
# * <tt>:dependent</tt> - if set to :destroy, the associated objects have their destroy! methods
|
88
|
+
# called in a chain meaning all callbacks are also called for each object. Beware that this
|
89
|
+
# is a cascading delete and will affect all records that have a remote relationship with the
|
90
|
+
# record being destroyed!
|
91
|
+
# if set to :delete, the associated objects are deleted from the database without their
|
92
|
+
# callbacks being triggered. This does NOT cascade the deletes. All associated objects will
|
93
|
+
# have their relationships removed from other records before being deleted. The record calling
|
94
|
+
# destroy will only delete those records directly associated to it.
|
95
|
+
# if set to :protect and the collection is not empty an AssociatedProtectedError will be raised.
|
96
|
+
# if set to :nullify, the join table will have the relationship records removed which is
|
97
|
+
# effectively nullifying the foreign key.
|
98
|
+
# default is :nullify
|
27
99
|
def has_and_belongs_to_many(association_name, options = {})
|
28
100
|
database.schema[self].associations << HasAndBelongsToManyAssociation.new(self, association_name, options)
|
29
101
|
end
|
@@ -31,4 +103,4 @@ module DataMapper
|
|
31
103
|
end
|
32
104
|
|
33
105
|
end
|
34
|
-
end
|
106
|
+
end
|
@@ -2,90 +2,108 @@ require 'data_mapper/associations/has_n_association'
|
|
2
2
|
|
3
3
|
module DataMapper
|
4
4
|
module Associations
|
5
|
-
|
5
|
+
|
6
6
|
class BelongsToAssociation < HasNAssociation
|
7
7
|
|
8
|
-
def define_accessor(klass)
|
8
|
+
def define_accessor(klass)
|
9
9
|
klass.class_eval <<-EOS
|
10
|
-
|
10
|
+
|
11
11
|
def create_#{@association_name}(options = {})
|
12
12
|
#{@association_name}_association.create(options)
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
def build_#{@association_name}(options = {})
|
16
16
|
#{@association_name}_association.build(options)
|
17
17
|
end
|
18
|
-
|
18
|
+
|
19
19
|
def #{@association_name}
|
20
20
|
#{@association_name}_association.instance
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
23
|
def #{@association_name}=(value)
|
24
24
|
#{@association_name}_association.set(value)
|
25
25
|
end
|
26
|
-
|
26
|
+
|
27
27
|
private
|
28
28
|
def #{@association_name}_association
|
29
29
|
@#{@association_name} || (@#{@association_name} = DataMapper::Associations::BelongsToAssociation::Instance.new(self, #{@association_name.inspect}))
|
30
30
|
end
|
31
31
|
EOS
|
32
32
|
end
|
33
|
-
|
33
|
+
|
34
34
|
# Reverse the natural order for BelongsToAssociations
|
35
35
|
alias constant associated_constant
|
36
36
|
def associated_constant
|
37
37
|
@constant
|
38
38
|
end
|
39
|
-
|
39
|
+
|
40
40
|
def foreign_key_name
|
41
41
|
@foreign_key_name || @foreign_key_name = (@options[:foreign_key] || "#{name}_#{key_table.key.name}".to_sym)
|
42
42
|
end
|
43
|
-
|
44
|
-
def
|
43
|
+
|
44
|
+
def complementary_association
|
45
|
+
@complementary_association || begin
|
46
|
+
@complementary_association = key_table.associations.find do |mapping|
|
47
|
+
mapping.is_a?(HasManyAssociation) &&
|
48
|
+
mapping.foreign_key_column.name == foreign_key_column.name &&
|
49
|
+
mapping.associated_table.name == associated_table.name
|
50
|
+
end
|
51
|
+
|
52
|
+
if @complementary_association
|
53
|
+
class << self
|
54
|
+
attr_accessor :complementary_association
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
return @complementary_association
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_sql # :nodoc:
|
45
63
|
"JOIN #{key_table.to_sql} ON #{foreign_key_column.to_sql(true)} = #{primary_key_column.to_sql(true)}"
|
46
64
|
end
|
47
|
-
|
65
|
+
|
48
66
|
class Instance < Associations::Reference
|
49
|
-
|
50
|
-
def dirty?
|
51
|
-
@associated && @new_member
|
67
|
+
|
68
|
+
def dirty?(cleared = ::Set.new)
|
69
|
+
@associated && (@new_member || @key_not_set)
|
52
70
|
end
|
53
|
-
|
71
|
+
|
54
72
|
def validate_recursively(event, cleared)
|
55
73
|
@associated.nil? || cleared.include?(@associated) || @associated.validate_recursively(event, cleared)
|
56
74
|
end
|
57
|
-
|
58
|
-
def save_without_validation(database_context)
|
75
|
+
|
76
|
+
def save_without_validation(database_context, cleared)
|
59
77
|
@new_member = false
|
60
78
|
unless @associated.nil?
|
61
79
|
@instance.instance_variable_set(
|
62
80
|
association.foreign_key_column.instance_variable_name,
|
63
81
|
@associated.key
|
64
82
|
)
|
65
|
-
@instance.database_context.adapter.save_without_validation(database_context, @instance)
|
83
|
+
@instance.database_context.adapter.save_without_validation(database_context, @instance, cleared)
|
66
84
|
end
|
67
85
|
end
|
68
|
-
|
86
|
+
|
69
87
|
def reload!
|
70
88
|
@new_member = false
|
71
89
|
@associated = nil
|
72
90
|
instance
|
73
91
|
end
|
74
|
-
|
92
|
+
|
75
93
|
def instance
|
76
|
-
@associated || @associated = begin
|
94
|
+
@associated || @associated = begin
|
77
95
|
if @instance.loaded_set.nil?
|
78
96
|
nil
|
79
97
|
else
|
80
|
-
|
98
|
+
|
81
99
|
# Temp variable for the instance variable name.
|
82
100
|
fk = association.foreign_key_column.to_sym
|
83
|
-
|
101
|
+
|
84
102
|
set = @instance.loaded_set.group_by { |instance| instance.send(fk) }
|
85
103
|
|
86
104
|
@instance.database_context.all(association.constant, association.associated_table.key.to_sym => set.keys).each do |assoc|
|
87
105
|
set[assoc.key].each do |primary_instance|
|
88
|
-
primary_instance.send(
|
106
|
+
primary_instance.send("#{@association_name}_association").shallow_append(assoc)
|
89
107
|
end
|
90
108
|
end
|
91
109
|
|
@@ -93,34 +111,50 @@ module DataMapper
|
|
93
111
|
end
|
94
112
|
end
|
95
113
|
end
|
96
|
-
|
114
|
+
|
97
115
|
def create(options)
|
98
116
|
@associated = association.associated_constant.create(options)
|
99
117
|
end
|
100
|
-
|
118
|
+
|
101
119
|
def build(options)
|
102
120
|
@associated = association.associated_constant.new(options)
|
103
121
|
end
|
104
|
-
|
122
|
+
|
105
123
|
def setter_method
|
106
124
|
"#{@association_name}=".to_sym
|
107
125
|
end
|
108
|
-
|
109
|
-
def set(
|
110
|
-
|
111
|
-
|
126
|
+
|
127
|
+
def set(member)
|
128
|
+
shallow_append(member)
|
129
|
+
|
130
|
+
if complement = association.complementary_association
|
131
|
+
member.send(complement.name).shallow_append(@instance)
|
132
|
+
end
|
133
|
+
|
134
|
+
return self
|
135
|
+
end
|
136
|
+
|
137
|
+
def shallow_append(val)
|
138
|
+
raise RecursionError.new if val == @instance
|
112
139
|
@instance.instance_variable_set(association.foreign_key_column.instance_variable_name, val.key)
|
113
140
|
@associated = val
|
141
|
+
@key_not_set = true if val.key.nil?
|
142
|
+
return self
|
143
|
+
end
|
144
|
+
|
145
|
+
def deactivate
|
114
146
|
end
|
115
|
-
|
147
|
+
|
148
|
+
private
|
149
|
+
|
116
150
|
def ensure_foreign_key!
|
117
151
|
if @associated
|
118
152
|
@instance.instance_variable_set(association.foreign_key.instance_variable_name, @associated.key)
|
119
153
|
end
|
120
154
|
end
|
121
|
-
|
155
|
+
|
122
156
|
end # class Instance
|
123
157
|
end
|
124
|
-
|
158
|
+
|
125
159
|
end
|
126
|
-
end
|
160
|
+
end
|
@@ -1,34 +1,43 @@
|
|
1
1
|
module DataMapper
|
2
2
|
module Associations
|
3
|
-
|
3
|
+
|
4
4
|
class HasAndBelongsToManyAssociation
|
5
|
-
|
5
|
+
|
6
6
|
attr_reader :adapter
|
7
|
-
|
7
|
+
|
8
8
|
def initialize(klass, association_name, options)
|
9
9
|
@adapter = database.adapter
|
10
10
|
@key_table = adapter.table(klass)
|
11
|
+
@self_referential = (association_name.to_s == @key_table.name)
|
11
12
|
@association_name = association_name.to_sym
|
12
13
|
@options = options
|
13
|
-
|
14
|
+
|
14
15
|
define_accessor(klass)
|
15
16
|
end
|
16
|
-
|
17
|
+
|
17
18
|
# def key_table
|
18
19
|
# @key_table
|
19
20
|
# end
|
20
|
-
|
21
|
+
|
21
22
|
def name
|
22
23
|
@association_name
|
23
24
|
end
|
24
25
|
|
26
|
+
def dependency
|
27
|
+
@options[:dependent]
|
28
|
+
end
|
29
|
+
|
25
30
|
def foreign_name
|
26
31
|
@foreign_name || (@foreign_name = (@options[:foreign_name] || @key_table.name).to_sym)
|
27
32
|
end
|
28
|
-
|
33
|
+
|
34
|
+
def self_referential?
|
35
|
+
@self_referential
|
36
|
+
end
|
37
|
+
|
29
38
|
def constant
|
30
39
|
@associated_class || @associated_class = begin
|
31
|
-
|
40
|
+
|
32
41
|
if @options.has_key?(:class) || @options.has_key?(:class_name)
|
33
42
|
associated_class_name = (@options[:class] || @options[:class_name])
|
34
43
|
if associated_class_name.kind_of?(String)
|
@@ -39,51 +48,56 @@ module DataMapper
|
|
39
48
|
else
|
40
49
|
Kernel.const_get(Inflector.classify(@association_name))
|
41
50
|
end
|
42
|
-
|
51
|
+
|
43
52
|
end
|
44
53
|
end
|
45
|
-
|
46
|
-
def activate!
|
47
|
-
join_table.
|
54
|
+
|
55
|
+
def activate!(force = false)
|
56
|
+
join_columns.each {|column| column unless join_table.mapped_column_exists?(column.name)}
|
57
|
+
join_table.create!(force)
|
48
58
|
end
|
49
59
|
|
50
60
|
def associated_columns
|
51
61
|
associated_table.columns.reject { |column| column.lazy? } + join_columns
|
52
62
|
end
|
53
|
-
|
63
|
+
|
54
64
|
def join_columns
|
55
65
|
[ left_foreign_key, right_foreign_key ]
|
56
66
|
end
|
57
|
-
|
67
|
+
|
58
68
|
def associated_table
|
59
69
|
@associated_table || (@associated_table = adapter.table(constant))
|
60
70
|
end
|
61
|
-
|
71
|
+
|
62
72
|
def join_table
|
63
|
-
@join_table || @join_table = begin
|
64
|
-
join_table_name = @options[:join_table] ||
|
73
|
+
@join_table || @join_table = begin
|
74
|
+
join_table_name = @options[:join_table] ||
|
65
75
|
[ @key_table.name.to_s, database.schema[constant].name.to_s ].sort.join('_')
|
66
|
-
|
76
|
+
|
67
77
|
adapter.table(join_table_name)
|
68
|
-
end
|
78
|
+
end
|
69
79
|
end
|
70
|
-
|
80
|
+
|
71
81
|
def left_foreign_key
|
72
82
|
@left_foreign_key || @left_foreign_key = begin
|
73
83
|
join_table.add_column(
|
74
84
|
(@options[:left_foreign_key] || @key_table.default_foreign_key),
|
75
|
-
:integer, :key => true)
|
85
|
+
:integer, :nullable => true, :key => true)
|
76
86
|
end
|
77
87
|
end
|
78
88
|
|
79
89
|
def right_foreign_key
|
90
|
+
if self_referential?
|
91
|
+
@options[:right_foreign_key] ||= ["related_", associated_table.default_foreign_key].to_s
|
92
|
+
end
|
93
|
+
|
80
94
|
@right_foreign_key || @right_foreign_key = begin
|
81
95
|
join_table.add_column(
|
82
96
|
(@options[:right_foreign_key] || associated_table.default_foreign_key),
|
83
|
-
:integer, :key => true)
|
97
|
+
:integer, :nullable => true, :key => true)
|
84
98
|
end
|
85
99
|
end
|
86
|
-
|
100
|
+
|
87
101
|
def to_sql
|
88
102
|
<<-EOS.compress_lines
|
89
103
|
JOIN #{join_table.to_sql} ON
|
@@ -92,14 +106,21 @@ module DataMapper
|
|
92
106
|
#{associated_table.key.to_sql(true)} = #{right_foreign_key.to_sql(true)}
|
93
107
|
EOS
|
94
108
|
end
|
95
|
-
|
109
|
+
|
96
110
|
def to_shallow_sql
|
111
|
+
if self_referential?
|
112
|
+
<<-EOS.compress_lines
|
113
|
+
JOIN #{join_table.to_sql} ON
|
114
|
+
#{right_foreign_key.to_sql(true)} = #{@key_table.key.to_sql(true)}
|
115
|
+
EOS
|
116
|
+
else
|
97
117
|
<<-EOS.compress_lines
|
98
118
|
JOIN #{join_table.to_sql} ON
|
99
119
|
#{left_foreign_key.to_sql(true)} = #{@key_table.key.to_sql(true)}
|
100
120
|
EOS
|
121
|
+
end
|
101
122
|
end
|
102
|
-
|
123
|
+
|
103
124
|
def to_insert_sql
|
104
125
|
<<-EOS.compress_lines
|
105
126
|
INSERT INTO #{join_table.to_sql}
|
@@ -107,14 +128,29 @@ module DataMapper
|
|
107
128
|
VALUES
|
108
129
|
EOS
|
109
130
|
end
|
110
|
-
|
131
|
+
|
111
132
|
def to_delete_sql
|
112
|
-
<<-EOS
|
133
|
+
<<-EOS.compress_lines
|
113
134
|
DELETE FROM #{join_table.to_sql}
|
114
135
|
WHERE #{left_foreign_key.to_sql} = ?
|
115
136
|
EOS
|
116
137
|
end
|
117
|
-
|
138
|
+
|
139
|
+
def to_delete_set_sql
|
140
|
+
<<-EOS.compress_lines
|
141
|
+
DELETE FROM #{join_table.to_sql}
|
142
|
+
WHERE #{left_foreign_key.to_sql} IN ?
|
143
|
+
OR #{right_foreign_key.to_sql} IN ?
|
144
|
+
EOS
|
145
|
+
end
|
146
|
+
|
147
|
+
def to_delete_members_sql
|
148
|
+
<<-EOS.compress_lines
|
149
|
+
DELETE FROM #{associated_table.to_sql}
|
150
|
+
WHERE #{associated_table.key.to_sql} IN ?
|
151
|
+
EOS
|
152
|
+
end
|
153
|
+
|
118
154
|
def to_delete_member_sql
|
119
155
|
<<-EOS
|
120
156
|
DELETE FROM #{join_table.to_sql}
|
@@ -122,29 +158,43 @@ module DataMapper
|
|
122
158
|
AND #{right_foreign_key.to_sql} = ?
|
123
159
|
EOS
|
124
160
|
end
|
125
|
-
|
161
|
+
|
162
|
+
def to_disassociate_sql
|
163
|
+
<<-EOS
|
164
|
+
UPDATE #{join_table.to_sql}
|
165
|
+
SET #{left_foreign_key.to_sql} = NULL
|
166
|
+
WHERE #{left_foreign_key.to_sql} = ?
|
167
|
+
EOS
|
168
|
+
end
|
169
|
+
|
126
170
|
# Define the association instance method (i.e. Project#tasks)
|
127
171
|
def define_accessor(klass)
|
128
172
|
klass.class_eval <<-EOS
|
129
173
|
def #{@association_name}
|
130
174
|
@#{@association_name} || (@#{@association_name} = HasAndBelongsToManyAssociation::Set.new(self, #{@association_name.inspect}))
|
131
175
|
end
|
132
|
-
|
176
|
+
|
133
177
|
def #{@association_name}=(value)
|
134
178
|
#{@association_name}.set(value)
|
135
179
|
end
|
180
|
+
|
181
|
+
private
|
182
|
+
def #{@association_name}_keys=(value)
|
183
|
+
#{@association_name}.clear
|
184
|
+
|
185
|
+
associated_constant = #{@association_name}.association.constant
|
186
|
+
associated_table = #{@association_name}.association.associated_table
|
187
|
+
associated_constant.all(associated_table.key => [*value]).each do |entry|
|
188
|
+
#{@association_name} << entry
|
189
|
+
end
|
190
|
+
end
|
136
191
|
EOS
|
137
192
|
end
|
138
|
-
|
193
|
+
|
139
194
|
class Set < Associations::Reference
|
140
|
-
|
195
|
+
|
141
196
|
include Enumerable
|
142
|
-
|
143
|
-
def initialize(*args)
|
144
|
-
super
|
145
|
-
@new_members = false
|
146
|
-
end
|
147
|
-
|
197
|
+
|
148
198
|
def each
|
149
199
|
entries.each { |item| yield item }
|
150
200
|
end
|
@@ -153,11 +203,11 @@ module DataMapper
|
|
153
203
|
entries.size
|
154
204
|
end
|
155
205
|
alias length size
|
156
|
-
|
206
|
+
|
157
207
|
def count
|
158
208
|
entries.size
|
159
209
|
end
|
160
|
-
|
210
|
+
|
161
211
|
def [](key)
|
162
212
|
entries[key]
|
163
213
|
end
|
@@ -165,48 +215,49 @@ module DataMapper
|
|
165
215
|
def empty?
|
166
216
|
entries.empty?
|
167
217
|
end
|
168
|
-
|
169
|
-
def dirty?
|
170
|
-
|
218
|
+
|
219
|
+
def dirty?(cleared = ::Set.new)
|
220
|
+
return false unless @entries
|
221
|
+
@entries.any? {|item| cleared.include?(item) || item.dirty?(cleared) } || @associated_keys != @entries.map { |entry| entry.keys }
|
171
222
|
end
|
172
|
-
|
223
|
+
|
173
224
|
def validate_recursively(event, cleared)
|
174
225
|
@entries.blank? || @entries.all? { |item| cleared.include?(item) || item.validate_recursively(event, cleared) }
|
175
226
|
end
|
176
|
-
|
177
|
-
def save_without_validation(database_context)
|
227
|
+
|
228
|
+
def save_without_validation(database_context, cleared)
|
178
229
|
unless @entries.nil?
|
179
|
-
|
180
|
-
if
|
230
|
+
|
231
|
+
if dirty?(cleared)
|
181
232
|
adapter = @instance.database_context.adapter
|
182
|
-
|
233
|
+
|
183
234
|
adapter.connection do |db|
|
184
235
|
command = db.create_command(association.to_delete_sql)
|
185
236
|
command.execute_non_query(@instance.key)
|
186
237
|
end
|
187
|
-
|
238
|
+
|
188
239
|
unless @entries.empty?
|
189
240
|
if adapter.batch_insertable?
|
190
241
|
sql = association.to_insert_sql
|
191
242
|
values = []
|
192
243
|
keys = []
|
193
|
-
|
244
|
+
|
194
245
|
@entries.each do |member|
|
195
|
-
adapter.save_without_validation(database_context, member)
|
246
|
+
adapter.save_without_validation(database_context, member, cleared)
|
196
247
|
values << "(?, ?)"
|
197
248
|
keys << @instance.key << member.key
|
198
249
|
end
|
199
|
-
|
250
|
+
|
200
251
|
adapter.connection do |db|
|
201
252
|
command = db.create_command(sql << ' ' << values.join(', '))
|
202
253
|
command.execute_non_query(*keys)
|
203
254
|
end
|
204
|
-
|
255
|
+
|
205
256
|
else # adapter doesn't support batch inserts...
|
206
257
|
@entries.each do |member|
|
207
|
-
adapter.save_without_validation(database_context, member)
|
258
|
+
adapter.save_without_validation(database_context, member, cleared)
|
208
259
|
end
|
209
|
-
|
260
|
+
|
210
261
|
# Just to keep the same flow as the batch-insert mode.
|
211
262
|
@entries.each do |member|
|
212
263
|
adapter.connection do |db|
|
@@ -216,30 +267,31 @@ module DataMapper
|
|
216
267
|
end
|
217
268
|
end # if adapter.batch_insertable?
|
218
269
|
end # unless @entries.empty?
|
219
|
-
|
220
|
-
@new_members = false
|
221
|
-
end # if @new_members || dirty?
|
270
|
+
end # if dirty?
|
222
271
|
end
|
223
272
|
end
|
224
|
-
|
273
|
+
|
225
274
|
def <<(member)
|
226
|
-
|
227
|
-
|
275
|
+
return nil unless member
|
276
|
+
|
277
|
+
if member.is_a?(Enumerable)
|
278
|
+
member.each { |entry| entries << entry }
|
279
|
+
else
|
280
|
+
entries << member
|
281
|
+
end
|
228
282
|
end
|
229
|
-
|
283
|
+
|
230
284
|
def clear
|
231
|
-
@
|
232
|
-
@entries = []
|
285
|
+
@entries = Support::TypedSet.new(association.constant)
|
233
286
|
end
|
234
|
-
|
287
|
+
|
235
288
|
def reload!
|
236
|
-
@new_members = false
|
237
289
|
@entries = nil
|
238
290
|
end
|
239
|
-
|
291
|
+
|
240
292
|
def delete(member)
|
241
|
-
|
242
|
-
|
293
|
+
if found_member = entries.detect { |entry| entry == member }
|
294
|
+
entries.delete?(found_member)
|
243
295
|
@instance.database_context.adapter.connection do |db|
|
244
296
|
command = db.create_command(association.to_delete_member_sql)
|
245
297
|
command.execute_non_query(@instance.key, member.key)
|
@@ -249,7 +301,7 @@ module DataMapper
|
|
249
301
|
nil
|
250
302
|
end
|
251
303
|
end
|
252
|
-
|
304
|
+
|
253
305
|
def method_missing(symbol, *args, &block)
|
254
306
|
if entries.respond_to?(symbol)
|
255
307
|
entries.send(symbol, *args, &block)
|
@@ -265,21 +317,21 @@ module DataMapper
|
|
265
317
|
super
|
266
318
|
end
|
267
319
|
end
|
268
|
-
|
320
|
+
|
269
321
|
def entries
|
270
322
|
@entries || @entries = begin
|
271
323
|
|
272
324
|
if @instance.loaded_set.nil?
|
273
|
-
|
325
|
+
Support::TypedSet.new(association.constant)
|
274
326
|
else
|
275
|
-
|
327
|
+
|
276
328
|
associated_items = Hash.new { |h,k| h[k] = [] }
|
277
329
|
left_key_index = nil
|
278
330
|
association_constant = association.constant
|
279
331
|
left_foreign_key = association.left_foreign_key
|
280
|
-
|
332
|
+
|
281
333
|
matcher = lambda do |instance,columns,row|
|
282
|
-
|
334
|
+
|
283
335
|
# Locate the column for the left-key.
|
284
336
|
unless left_key_index
|
285
337
|
columns.each_with_index do |column, index|
|
@@ -289,40 +341,97 @@ module DataMapper
|
|
289
341
|
end
|
290
342
|
end
|
291
343
|
end
|
292
|
-
|
344
|
+
|
293
345
|
if instance.kind_of?(association_constant)
|
294
346
|
associated_items[left_foreign_key.type_cast_value(row[left_key_index])] << instance
|
295
347
|
end
|
296
348
|
end
|
297
|
-
|
349
|
+
|
298
350
|
@instance.database_context.all(association.constant,
|
299
351
|
left_foreign_key => @instance.loaded_set.map(&:key),
|
300
352
|
:shallow_include => association.foreign_name,
|
301
353
|
:intercept_load => matcher
|
302
354
|
)
|
303
|
-
|
355
|
+
|
304
356
|
# do stsuff with associated_items hash.
|
305
357
|
setter_method = "#{@association_name}=".to_sym
|
306
|
-
|
358
|
+
|
307
359
|
@instance.loaded_set.each do |entry|
|
308
360
|
entry.send(setter_method, associated_items[entry.key])
|
309
361
|
end # @instance.loaded_set.each
|
310
|
-
|
311
|
-
@entries
|
362
|
+
|
363
|
+
@entries
|
312
364
|
end
|
313
365
|
end
|
314
366
|
end
|
315
367
|
|
316
368
|
def set(results)
|
317
|
-
|
369
|
+
if results.is_a?(Support::TypedSet)
|
370
|
+
@entries = results
|
371
|
+
else
|
372
|
+
@entries = Support::TypedSet.new(association.constant)
|
373
|
+
[*results].each { |item| @entries << item }
|
374
|
+
end
|
375
|
+
@associated_keys = @entries.map { |entry| entry.key }
|
376
|
+
return @entries
|
318
377
|
end
|
319
378
|
|
320
379
|
def inspect
|
321
380
|
entries.inspect
|
322
381
|
end
|
382
|
+
|
383
|
+
def first
|
384
|
+
entries.entries.first
|
385
|
+
end
|
386
|
+
|
387
|
+
def last
|
388
|
+
entries.entries.last
|
389
|
+
end
|
390
|
+
|
391
|
+
def deactivate
|
392
|
+
case association.dependency
|
393
|
+
when :destroy
|
394
|
+
entries.each do |member|
|
395
|
+
member.destroy! unless member.new_record?
|
396
|
+
end
|
397
|
+
when :delete
|
398
|
+
delete_association
|
399
|
+
when :protect
|
400
|
+
unless entries.empty?
|
401
|
+
raise AssociationProtectedError.new("You cannot delete this model while it has items associated with it.")
|
402
|
+
end
|
403
|
+
when :nullify
|
404
|
+
nullify_association
|
405
|
+
else
|
406
|
+
nullify_association
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
def delete_association
|
411
|
+
@instance.database_context.adapter.connection do |db|
|
412
|
+
associated_keys = entries.collect do |item|
|
413
|
+
item.key unless item.new_record?
|
414
|
+
end.compact
|
415
|
+
parameters = [@instance.key] + associated_keys
|
416
|
+
|
417
|
+
sql = association.to_delete_set_sql
|
418
|
+
db.create_command(sql).execute_non_query(*[parameters, parameters])
|
419
|
+
|
420
|
+
sql = association.to_delete_members_sql
|
421
|
+
db.create_command(sql).execute_non_query(associated_keys)
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
def nullify_association
|
426
|
+
@instance.database_context.adapter.connection do |db|
|
427
|
+
sql = association.to_delete_sql
|
428
|
+
parameters = [@instance.key]
|
429
|
+
db.create_command(sql).execute_non_query(*parameters)
|
430
|
+
end
|
431
|
+
end
|
323
432
|
end
|
324
|
-
|
433
|
+
|
325
434
|
end # class HasAndBelongsToManyAssociation
|
326
|
-
|
435
|
+
|
327
436
|
end # module Associations
|
328
|
-
end # module DataMapper
|
437
|
+
end # module DataMapper
|