dm-core 0.9.2 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. data/.autotest +26 -0
  2. data/{CHANGELOG → History.txt} +78 -77
  3. data/Manifest.txt +123 -0
  4. data/{README → README.txt} +0 -0
  5. data/Rakefile +29 -0
  6. data/SPECS +63 -0
  7. data/TODO +1 -0
  8. data/lib/dm-core.rb +6 -1
  9. data/lib/dm-core/adapters/data_objects_adapter.rb +29 -32
  10. data/lib/dm-core/adapters/mysql_adapter.rb +1 -1
  11. data/lib/dm-core/adapters/postgres_adapter.rb +1 -1
  12. data/lib/dm-core/adapters/sqlite3_adapter.rb +2 -2
  13. data/lib/dm-core/associations.rb +26 -0
  14. data/lib/dm-core/associations/many_to_many.rb +34 -25
  15. data/lib/dm-core/associations/many_to_one.rb +4 -4
  16. data/lib/dm-core/associations/one_to_many.rb +48 -13
  17. data/lib/dm-core/associations/one_to_one.rb +4 -4
  18. data/lib/dm-core/associations/relationship.rb +144 -42
  19. data/lib/dm-core/associations/relationship_chain.rb +31 -24
  20. data/lib/dm-core/auto_migrations.rb +0 -4
  21. data/lib/dm-core/collection.rb +40 -7
  22. data/lib/dm-core/dependency_queue.rb +31 -0
  23. data/lib/dm-core/hook.rb +2 -2
  24. data/lib/dm-core/is.rb +2 -2
  25. data/lib/dm-core/logger.rb +10 -10
  26. data/lib/dm-core/model.rb +94 -41
  27. data/lib/dm-core/property.rb +72 -41
  28. data/lib/dm-core/property_set.rb +8 -14
  29. data/lib/dm-core/query.rb +34 -9
  30. data/lib/dm-core/repository.rb +0 -0
  31. data/lib/dm-core/resource.rb +13 -13
  32. data/lib/dm-core/scope.rb +25 -2
  33. data/lib/dm-core/type.rb +3 -3
  34. data/lib/dm-core/types/discriminator.rb +10 -8
  35. data/lib/dm-core/types/object.rb +4 -0
  36. data/lib/dm-core/types/paranoid_boolean.rb +15 -4
  37. data/lib/dm-core/types/paranoid_datetime.rb +15 -4
  38. data/lib/dm-core/version.rb +3 -0
  39. data/script/all +5 -0
  40. data/script/performance.rb +191 -0
  41. data/script/profile.rb +86 -0
  42. data/spec/integration/association_spec.rb +288 -204
  43. data/spec/integration/association_through_spec.rb +9 -3
  44. data/spec/integration/associations/many_to_many_spec.rb +97 -31
  45. data/spec/integration/associations/many_to_one_spec.rb +41 -6
  46. data/spec/integration/associations/one_to_many_spec.rb +18 -2
  47. data/spec/integration/auto_migrations_spec.rb +0 -0
  48. data/spec/integration/collection_spec.rb +89 -42
  49. data/spec/integration/dependency_queue_spec.rb +58 -0
  50. data/spec/integration/model_spec.rb +67 -8
  51. data/spec/integration/postgres_adapter_spec.rb +19 -20
  52. data/spec/integration/property_spec.rb +17 -8
  53. data/spec/integration/query_spec.rb +273 -191
  54. data/spec/integration/resource_spec.rb +108 -10
  55. data/spec/integration/strategic_eager_loading_spec.rb +138 -0
  56. data/spec/integration/transaction_spec.rb +3 -3
  57. data/spec/integration/type_spec.rb +121 -0
  58. data/spec/lib/logging_helper.rb +18 -0
  59. data/spec/lib/model_loader.rb +91 -0
  60. data/spec/lib/publicize_methods.rb +28 -0
  61. data/spec/models/vehicles.rb +34 -0
  62. data/spec/models/zoo.rb +48 -0
  63. data/spec/spec.opts +3 -0
  64. data/spec/spec_helper.rb +25 -62
  65. data/spec/unit/adapters/data_objects_adapter_spec.rb +1 -0
  66. data/spec/unit/associations/many_to_many_spec.rb +3 -0
  67. data/spec/unit/associations/many_to_one_spec.rb +9 -1
  68. data/spec/unit/associations/one_to_many_spec.rb +12 -4
  69. data/spec/unit/associations/relationship_spec.rb +19 -15
  70. data/spec/unit/associations_spec.rb +37 -0
  71. data/spec/unit/collection_spec.rb +8 -0
  72. data/spec/unit/data_mapper_spec.rb +14 -0
  73. data/spec/unit/model_spec.rb +2 -2
  74. data/spec/unit/property_set_spec.rb +0 -13
  75. data/spec/unit/property_spec.rb +92 -21
  76. data/spec/unit/query_spec.rb +49 -4
  77. data/spec/unit/resource_spec.rb +122 -60
  78. data/spec/unit/scope_spec.rb +11 -0
  79. data/tasks/ci.rb +68 -0
  80. data/tasks/dm.rb +63 -0
  81. data/tasks/doc.rb +20 -0
  82. data/tasks/hoe.rb +38 -0
  83. data/tasks/install.rb +20 -0
  84. metadata +63 -22
@@ -1,4 +1,4 @@
1
- gem 'do_mysql', '=0.9.2'
1
+ gem 'do_mysql', '=0.9.3'
2
2
  require 'do_mysql'
3
3
 
4
4
  module DataMapper
@@ -1,4 +1,4 @@
1
- gem 'do_postgres', '=0.9.2'
1
+ gem 'do_postgres', '=0.9.3'
2
2
  require 'do_postgres'
3
3
 
4
4
  module DataMapper
@@ -1,4 +1,4 @@
1
- gem 'do_sqlite3', '=0.9.2'
1
+ gem 'do_sqlite3', '=0.9.3'
2
2
  require 'do_sqlite3'
3
3
 
4
4
  module DataMapper
@@ -41,7 +41,7 @@ module DataMapper
41
41
  end
42
42
 
43
43
  module SQL
44
- private
44
+ # private ## This cannot be private for current migrations
45
45
 
46
46
  # TODO: move to dm-more/dm-migrations
47
47
  def supports_serial?
@@ -17,6 +17,32 @@ module DataMapper
17
17
  class UnsavedParentError < RuntimeError
18
18
  end
19
19
 
20
+ # Returns all relationships that are many-to-one for this model.
21
+ #
22
+ # Used to find the relationships that require properties in any Repository.
23
+ #
24
+ # Example:
25
+ # class Plur
26
+ # include DataMapper::Resource
27
+ # def self.default_repository_name
28
+ # :plur_db
29
+ # end
30
+ # repository(:plupp_db) do
31
+ # has 1, :plupp
32
+ # end
33
+ # end
34
+ #
35
+ # This resource has a many-to-one to the Plupp resource residing in the :plupp_db repository,
36
+ # but the Plur resource needs the plupp_id property no matter what repository itself lives in,
37
+ # ie we need to create that property when we migrate etc.
38
+ #
39
+ # Used in DataMapper::Model.properties_with_subclasses
40
+ #
41
+ # @api private
42
+ def many_to_one_relationships
43
+ @relationships.values.collect do |rels| rels.values end.flatten.select do |relationship| relationship.child_model == self end
44
+ end
45
+
20
46
  def relationships(repository_name = default_repository_name)
21
47
  @relationships ||= Hash.new { |h,k| h[k] = k == Repository.default_name ? {} : h[Repository.default_name].dup }
22
48
  @relationships[repository_name]
@@ -28,7 +28,7 @@ module DataMapper
28
28
  def #{name}_association
29
29
  @#{name}_association ||= begin
30
30
  unless relationship = model.relationships(#{repository_name.inspect})[#{name.inspect}]
31
- raise ArgumentError, 'Relationship #{name.inspect} does not exist'
31
+ raise ArgumentError, "Relationship #{name.inspect} does not exist in \#{model}"
32
32
  end
33
33
  association = Proxy.new(relationship, self)
34
34
  parent_associations << association
@@ -40,14 +40,14 @@ module DataMapper
40
40
  opts = options.dup
41
41
  opts.delete(:through)
42
42
  opts[:child_model] ||= opts.delete(:class_name) || Extlib::Inflection.classify(name)
43
- opts[:parent_model] = model.name
43
+ opts[:parent_model] = model
44
44
  opts[:repository_name] = repository_name
45
45
  opts[:remote_relationship_name] ||= opts.delete(:remote_name) || name
46
46
  opts[:parent_key] = opts[:parent_key]
47
47
  opts[:child_key] = opts[:child_key]
48
48
  opts[:mutable] = true
49
49
 
50
- names = [ opts[:child_model], opts[:parent_model] ].sort
50
+ names = [ opts[:child_model], opts[:parent_model].name ].sort
51
51
  model_name = names.join
52
52
  storage_name = Extlib::Inflection.tableize(Extlib::Inflection.pluralize(names[0]) + names[1])
53
53
 
@@ -77,39 +77,50 @@ module DataMapper
77
77
  end
78
78
 
79
79
  class Proxy < DataMapper::Associations::OneToMany::Proxy
80
-
81
- def <<(resource)
82
- resource.save if resource.new_record?
83
- through = @relationship.child_model.new
84
- @relationship.child_key.each_with_index do |key, index|
85
- through.send("#{key.name}=", @relationship.parent_key.key[index].get(@parent))
86
- end
87
- remote_relationship.child_key.each_with_index do |key, index|
88
- through.send("#{key.name}=", remote_relationship.parent_key.key[index].get(resource))
89
- end
90
- near_model << through
91
- super
92
- end
93
-
94
80
  def delete(resource)
95
- through = near_model.get(*(@parent.key + resource.key))
96
- near_model.delete(through)
81
+ through = near_association.get(*(@parent.key + resource.key))
82
+ near_association.delete(through)
97
83
  orphan_resource(super)
98
84
  end
99
85
 
100
86
  def clear
101
- near_model.clear
87
+ near_association.clear
102
88
  super
103
89
  end
104
90
 
105
91
  def destroy
106
- near_model.destroy
92
+ near_association.destroy
107
93
  super
108
94
  end
109
95
 
110
96
  def save
111
97
  end
112
98
 
99
+ private
100
+
101
+ def new_child(attributes)
102
+ remote_relationship.parent_model.new(attributes)
103
+ end
104
+
105
+ def relate_resource(resource)
106
+ assert_mutable
107
+ add_default_association_values(resource)
108
+ @orphans.delete(resource)
109
+
110
+ # TODO: fix this so it does not automatically save on append, if possible
111
+ resource.save if resource.new_record?
112
+ through_resource = @relationship.child_model.new
113
+ @relationship.child_key.zip(@relationship.parent_key) do |child_key,parent_key|
114
+ through_resource.send("#{child_key.name}=", parent_key.get(@parent))
115
+ end
116
+ remote_relationship.child_key.zip(remote_relationship.parent_key) do |child_key,parent_key|
117
+ through_resource.send("#{child_key.name}=", parent_key.get(resource))
118
+ end
119
+ near_association << through_resource
120
+
121
+ resource
122
+ end
123
+
113
124
  def orphan_resource(resource)
114
125
  assert_mutable
115
126
  @orphans << resource
@@ -119,14 +130,12 @@ module DataMapper
119
130
  def assert_mutable
120
131
  end
121
132
 
122
- private
123
-
124
133
  def remote_relationship
125
134
  @remote_relationship ||= @relationship.send(:remote_relationship)
126
135
  end
127
136
 
128
- def near_model
129
- @near_model ||= @parent.send(near_relationship_name)
137
+ def near_association
138
+ @near_association ||= @parent.send(near_relationship_name)
130
139
  end
131
140
 
132
141
  def near_relationship_name
@@ -27,7 +27,7 @@ module DataMapper
27
27
  def #{name}_association
28
28
  @#{name}_association ||= begin
29
29
  unless relationship = model.relationships(#{repository_name.inspect})[:#{name}]
30
- raise ArgumentError, 'Relationship #{name.inspect} does not exist'
30
+ raise ArgumentError, "Relationship #{name.inspect} does not exist in \#{model}"
31
31
  end
32
32
  association = Proxy.new(relationship, self)
33
33
  child_associations << association
@@ -39,7 +39,7 @@ module DataMapper
39
39
  model.relationships(repository_name)[name] = Relationship.new(
40
40
  name,
41
41
  repository_name,
42
- model.name,
42
+ model,
43
43
  options.fetch(:class_name, Extlib::Inflection.classify(name)),
44
44
  options
45
45
  )
@@ -48,7 +48,7 @@ module DataMapper
48
48
  class Proxy
49
49
  include Assertions
50
50
 
51
- instance_methods.each { |m| undef_method m unless %w[ __id__ __send__ class kind_of? respond_to? assert_kind_of should should_not ].include?(m) }
51
+ instance_methods.each { |m| undef_method m unless %w[ __id__ __send__ class kind_of? respond_to? assert_kind_of should should_not instance_variable_set instance_variable_get ].include?(m) }
52
52
 
53
53
  def replace(parent)
54
54
  @parent = parent
@@ -60,7 +60,7 @@ module DataMapper
60
60
  return false if @parent.nil?
61
61
  return true unless parent.new_record?
62
62
 
63
- DataMapper.repository(@relationship.repository_name) do
63
+ @relationship.with_repository(parent) do
64
64
  parent.save
65
65
  end
66
66
  end
@@ -1,5 +1,3 @@
1
- require 'forwardable'
2
-
3
1
  module DataMapper
4
2
  module Associations
5
3
  module OneToMany
@@ -29,7 +27,7 @@ module DataMapper
29
27
  def #{name}_association
30
28
  @#{name}_association ||= begin
31
29
  unless relationship = model.relationships(#{repository_name.inspect})[#{name.inspect}]
32
- raise ArgumentError, 'Relationship #{name.inspect} does not exist'
30
+ raise ArgumentError, "Relationship #{name.inspect} does not exist in \#{model}"
33
31
  end
34
32
  association = Proxy.new(relationship, self)
35
33
  parent_associations << association
@@ -41,8 +39,14 @@ module DataMapper
41
39
  model.relationships(repository_name)[name] = if options.has_key?(:through)
42
40
  opts = options.dup
43
41
 
42
+ if opts.key?(:class_name) && !opts.key?(:child_key)
43
+ warn(<<-EOS.margin)
44
+ You have specified #{model.base_model.name}.has(#{name.inspect}) with :class_name => #{opts[:class_name].inspect}. You probably also want to specify the :child_key option.
45
+ EOS
46
+ end
47
+
44
48
  opts[:child_model] ||= opts.delete(:class_name) || Extlib::Inflection.classify(name)
45
- opts[:parent_model] = model.name
49
+ opts[:parent_model] = model
46
50
  opts[:repository_name] = repository_name
47
51
  opts[:near_relationship_name] = opts.delete(:through)
48
52
  opts[:remote_relationship_name] ||= opts.delete(:remote_name) || name
@@ -52,10 +56,10 @@ module DataMapper
52
56
  RelationshipChain.new( opts )
53
57
  else
54
58
  Relationship.new(
55
- Extlib::Inflection.underscore(Extlib::Inflection.demodulize(model.name)).to_sym,
59
+ name,
56
60
  repository_name,
57
61
  options.fetch(:class_name, Extlib::Inflection.classify(name)),
58
- model.name,
62
+ model,
59
63
  options
60
64
  )
61
65
  end
@@ -68,7 +72,7 @@ module DataMapper
68
72
  class Proxy
69
73
  include Assertions
70
74
 
71
- instance_methods.each { |m| undef_method m unless %w[ __id__ __send__ class kind_of? respond_to? assert_kind_of should should_not ].include?(m) }
75
+ instance_methods.each { |m| undef_method m unless %w[ __id__ __send__ class kind_of? respond_to? assert_kind_of should should_not instance_variable_set instance_variable_get ].include?(m) }
72
76
 
73
77
  # FIXME: remove when RelationshipChain#get_children can return a Collection
74
78
  def all(query = {})
@@ -87,6 +91,7 @@ module DataMapper
87
91
 
88
92
  def <<(resource)
89
93
  assert_mutable
94
+ return self if !resource.new_record? && self.include?(resource)
90
95
  super
91
96
  relate_resource(resource)
92
97
  self
@@ -94,6 +99,7 @@ module DataMapper
94
99
 
95
100
  def push(*resources)
96
101
  assert_mutable
102
+ resources.reject { |resource| !resource.new_record? && self.include?(resource) }
97
103
  super
98
104
  resources.each { |resource| relate_resource(resource) }
99
105
  self
@@ -101,6 +107,7 @@ module DataMapper
101
107
 
102
108
  def unshift(*resources)
103
109
  assert_mutable
110
+ resources.reject { |resource| !resource.new_record? && self.include?(resource) }
104
111
  super
105
112
  resources.each { |resource| relate_resource(resource) }
106
113
  self
@@ -109,7 +116,7 @@ module DataMapper
109
116
  def replace(other)
110
117
  assert_mutable
111
118
  each { |resource| orphan_resource(resource) }
112
- other = other.map { |resource| Hash === resource ? @relationship.child_model.new(resource) : resource }
119
+ other = other.map { |resource| resource.kind_of?(Hash) ? new_child(resource) : resource }
113
120
  super
114
121
  other.each { |resource| relate_resource(resource) }
115
122
  self
@@ -142,10 +149,21 @@ module DataMapper
142
149
  self
143
150
  end
144
151
 
152
+ def build(attributes = {})
153
+ assert_mutable
154
+ attributes = default_attributes.merge(attributes)
155
+ resource = children.respond_to?(:build) ? super(attributes) : new_child(attributes)
156
+ self << resource
157
+ resource
158
+ end
159
+
145
160
  def create(attributes = {})
146
161
  assert_mutable
147
162
  raise UnsavedParentError, 'You cannot create until the parent is saved' if @parent.new_record?
148
- super
163
+ attributes = default_attributes.merge(attributes)
164
+ resource = children.respond_to?(:create) ? super(attributes) : @relationship.child_model.create(attributes)
165
+ self << resource
166
+ resource
149
167
  end
150
168
 
151
169
  def update(attributes = {})
@@ -229,15 +247,32 @@ module DataMapper
229
247
  raise ImmutableAssociationError, 'You can not modify this assocation' if children.frozen?
230
248
  end
231
249
 
232
- def add_default_association_values(resource)
233
- return if respond_to?(:default_attributes)
250
+ def default_attributes
251
+ default_attributes = {}
234
252
 
235
253
  @relationship.query.each do |attribute, value|
236
- next if Query::OPTIONS.include?(attribute) || attribute.kind_of?(Query::Operator) || resource.attribute_loaded?(attribute)
254
+ next if Query::OPTIONS.include?(attribute) || attribute.kind_of?(Query::Operator)
255
+ default_attributes[attribute] = value
256
+ end
257
+
258
+ @relationship.child_key.zip(@relationship.parent_key.get(@parent)) do |property,value|
259
+ default_attributes[property.name] = value
260
+ end
261
+
262
+ default_attributes
263
+ end
264
+
265
+ def add_default_association_values(resource)
266
+ default_attributes.each do |attribute, value|
267
+ next if !resource.respond_to?("#{attribute}=") || resource.attribute_loaded?(attribute)
237
268
  resource.send("#{attribute}=", value)
238
269
  end
239
270
  end
240
271
 
272
+ def new_child(attributes)
273
+ @relationship.child_model.new(default_attributes.merge(attributes))
274
+ end
275
+
241
276
  def relate_resource(resource)
242
277
  assert_mutable
243
278
  add_default_association_values(resource)
@@ -252,7 +287,7 @@ module DataMapper
252
287
  end
253
288
 
254
289
  def save_resource(resource, parent = @parent)
255
- DataMapper.repository(@relationship.repository_name) do
290
+ @relationship.with_repository(resource) do |r|
256
291
  if parent.nil? && resource.model.respond_to?(:many_to_many)
257
292
  resource.destroy
258
293
  else
@@ -27,7 +27,7 @@ module DataMapper
27
27
  def #{name}_association
28
28
  @#{name}_association ||= begin
29
29
  unless relationship = model.relationships(#{repository_name.inspect})[:#{name}]
30
- raise ArgumentError, 'Relationship #{name.inspect} does not exist'
30
+ raise ArgumentError, "Relationship #{name.inspect} does not exist in \#{model}"
31
31
  end
32
32
  association = Associations::OneToMany::Proxy.new(relationship, self)
33
33
  parent_associations << association
@@ -39,7 +39,7 @@ module DataMapper
39
39
  model.relationships(repository_name)[name] = if options.has_key?(:through)
40
40
  RelationshipChain.new(
41
41
  :child_model => options.fetch(:class_name, Extlib::Inflection.classify(name)),
42
- :parent_model => model.name,
42
+ :parent_model => model,
43
43
  :repository_name => repository_name,
44
44
  :near_relationship_name => options[:through],
45
45
  :remote_relationship_name => options.fetch(:remote_name, name),
@@ -48,10 +48,10 @@ module DataMapper
48
48
  )
49
49
  else
50
50
  Relationship.new(
51
- Extlib::Inflection.underscore(Extlib::Inflection.demodulize(model.name)).to_sym,
51
+ name,
52
52
  repository_name,
53
53
  options.fetch(:class_name, Extlib::Inflection.classify(name)),
54
- model.name,
54
+ model,
55
55
  options
56
56
  )
57
57
  end
@@ -5,75 +5,166 @@ module DataMapper
5
5
 
6
6
  OPTIONS = [ :class_name, :child_key, :parent_key, :min, :max, :through ]
7
7
 
8
- attr_reader :name, :repository_name, :options, :query
8
+ # @api private
9
+ attr_reader :name, :options, :query
9
10
 
11
+ # @api private
10
12
  def child_key
11
13
  @child_key ||= begin
12
- model_properties = child_model.properties(repository_name)
13
-
14
- child_key = parent_key.zip(@child_properties || []).map do |parent_property,property_name|
15
- # TODO: use something similar to DM::NamingConventions to determine the property name
16
- property_name ||= "#{name}_#{parent_property.name}".to_sym
17
-
18
- model_properties[property_name] || DataMapper.repository(repository_name) do
19
- attributes = {}
20
-
21
- [ :length, :precision, :scale ].each do |attribute|
22
- attributes[attribute] = parent_property.send(attribute)
23
- end
24
-
25
- # NOTE: hack to make each many to many child_key a true key,
26
- # until I can figure out a better place for this check
27
- if child_model.respond_to?(:many_to_many)
28
- attributes[:key] = true
14
+ child_key = nil
15
+ child_model.repository.scope do |r|
16
+ model_properties = child_model.properties(r.name)
17
+
18
+ child_key = parent_key.zip(@child_properties || []).map do |parent_property,property_name|
19
+ # TODO: use something similar to DM::NamingConventions to determine the property name
20
+ parent_name = Extlib::Inflection.underscore(Extlib::Inflection.demodulize(parent_model.base_model.name))
21
+ property_name ||= "#{parent_name}_#{parent_property.name}".to_sym
22
+
23
+ if model_properties.has_property?(property_name)
24
+ model_properties[property_name]
25
+ else
26
+ options = {}
27
+
28
+ [ :length, :precision, :scale ].each do |option|
29
+ options[option] = parent_property.send(option)
30
+ end
31
+
32
+ # NOTE: hack to make each many to many child_key a true key,
33
+ # until I can figure out a better place for this check
34
+ if child_model.respond_to?(:many_to_many)
35
+ options[:key] = true
36
+ end
37
+
38
+ child_model.property(property_name, parent_property.primitive, options)
29
39
  end
30
-
31
- child_model.property(property_name, parent_property.primitive, attributes)
32
40
  end
33
41
  end
34
42
  PropertySet.new(child_key)
35
43
  end
36
44
  end
37
45
 
46
+ # @api private
38
47
  def parent_key
39
48
  @parent_key ||= begin
40
- parent_key = if @parent_properties
41
- parent_model.properties(repository_name).slice(*@parent_properties)
42
- else
43
- parent_model.key(repository_name)
49
+ parent_key = nil
50
+ parent_model.repository.scope do |r|
51
+ parent_key = if @parent_properties
52
+ parent_model.properties(r.name).slice(*@parent_properties)
53
+ else
54
+ parent_model.key
55
+ end
44
56
  end
45
-
46
57
  PropertySet.new(parent_key)
47
58
  end
48
59
  end
49
60
 
61
+ # @api private
50
62
  def parent_model
51
- Class === @parent_model ? @parent_model : self.class.find_const(@parent_model)
63
+ Class === @parent_model ? @parent_model : (Class === @child_model ? @child_model.find_const(@parent_model) : Object.find_const(@parent_model))
64
+ rescue NameError
65
+ raise NameError, "Cannot find the parent_model #{@parent_model} for #{@child_model}"
52
66
  end
53
67
 
68
+ # @api private
54
69
  def child_model
55
- Class === @child_model ? @child_model : self.class.find_const(@child_model)
70
+ Class === @child_model ? @child_model : (Class === @parent_model ? @parent_model.find_const(@child_model) : Object.find_const(@child_model))
71
+ rescue NameError
72
+ raise NameError, "Cannot find the child_model #{@child_model} for #{@parent_model}"
56
73
  end
57
74
 
58
75
  # @api private
59
76
  def get_children(parent, options = {}, finder = :all, *args)
60
- bind_values = parent_key.get(parent)
61
- return [] if bind_values.any? { |bind_value| bind_value.nil? }
62
- query = child_key.to_query(bind_values)
77
+ bind_values = parent_key.get(parent)
78
+ parent_values = bind_values.dup
79
+
80
+ with_repository(child_model) do |r|
81
+ parent_identity_map = parent.repository.identity_map(parent_model)
82
+ child_identity_map = r.identity_map(child_model)
83
+
84
+ query_values = parent_identity_map.keys.flatten
85
+ query_values.reject! { |k| child_identity_map[[k]] }
86
+
87
+ bind_values = query_values unless query_values.empty?
88
+ query = child_key.map { |k| [ k, bind_values ] }.to_hash
89
+
90
+ collection = child_model.send(finder, *(args.dup << @query.merge(options).merge(query)))
91
+ return collection unless collection.kind_of?(Collection) && collection.any?
92
+
93
+ grouped_collection = Hash.new { |h,k| h[k] = [] }
94
+ collection.each do |resource|
95
+ grouped_collection[get_parent(resource, parent)] << resource
96
+ end
97
+
98
+ association_accessor = "#{self.name}_association"
99
+
100
+ ret = nil
101
+ grouped_collection.each do |parent, children|
102
+ association = parent.send(association_accessor)
103
+
104
+ query = collection.query.dup
105
+ query.conditions.map! do |operator, property, bind_value|
106
+ if child_key.has_property?(property.name)
107
+ bind_value = *children.map { |child| property.get(child) }.uniq
108
+ end
109
+ [ operator, property, bind_value ]
110
+ end
111
+
112
+ parents_children = Collection.new(query) do |collection|
113
+ children.each { |child| collection.send(:add, child) }
114
+ end
115
+
116
+ if parent_key.get(parent) == parent_values
117
+ ret = parents_children
118
+ else
119
+ association.instance_variable_set(:@children, parents_children)
120
+ end
121
+ end
63
122
 
64
- DataMapper.repository(repository_name) do
65
- child_model.send(finder, *(args << @query.merge(options).merge(query)))
123
+ ret || child_model.send(finder, *(args.dup << @query.merge(options).merge(child_key.zip(parent_values).to_hash)))
66
124
  end
67
125
  end
68
126
 
69
127
  # @api private
70
- def get_parent(child)
71
- bind_values = child_key.get(child)
72
- return nil if bind_values.any? { |bind_value| bind_value.nil? }
73
- query = parent_key.to_query(bind_values)
128
+ def get_parent(child, parent = nil)
129
+ child_value = child_key.get(child)
130
+ return nil unless child_value.nitems == child_value.size
131
+
132
+ with_repository(parent || parent_model) do
133
+ parent_identity_map = (parent || parent_model).repository.identity_map(parent_model)
134
+ child_identity_map = child.repository.identity_map(child_model)
135
+
136
+ if parent = parent_identity_map[child_value]
137
+ return parent
138
+ end
139
+
140
+ children = child_identity_map.values
141
+
142
+ bind_values = *children.map { |c| child_key.get(c) }.uniq
143
+ query_values = bind_values.reject { |k| parent_identity_map[[k]] }
144
+
145
+ bind_values = query_values unless query_values.empty?
146
+ query = parent_key.map { |k| [ k, bind_values ] }.to_hash
74
147
 
75
- DataMapper.repository(repository_name) do
76
- parent_model.first(@query.merge(query))
148
+ association_accessor = "#{self.name}_association"
149
+
150
+ collection = parent_model.send(:all, query)
151
+ collection.send(:lazy_load)
152
+ children.each do |c|
153
+ c.send(association_accessor).instance_variable_set(:@parent, collection.get(*child_key.get(c)))
154
+ end
155
+ child.send(association_accessor).instance_variable_get(:@parent)
156
+ end
157
+ end
158
+
159
+ # @api private
160
+ def with_repository(object = nil, &block)
161
+ other_model = object.model == child_model ? parent_model : child_model if object.respond_to?(:model)
162
+ other_model = object == child_model ? parent_model : child_model if object.kind_of?(DataMapper::Resource)
163
+
164
+ if other_model && other_model.repository == object.repository && object.repository.name != @repository_name
165
+ object.repository.scope(&block)
166
+ else
167
+ repository(@repository_name, &block)
77
168
  end
78
169
  end
79
170
 
@@ -89,10 +180,10 @@ module DataMapper
89
180
  # http://edocs.bea.com/kodo/docs41/full/html/jdo_overview_mapping_join.html
90
181
  # I wash my hands of it!
91
182
  def initialize(name, repository_name, child_model, parent_model, options = {})
92
- assert_kind_of 'name', name, Symbol
93
- assert_kind_of 'repository_name', repository_name, Symbol
94
- assert_kind_of 'child_model', child_model, String, Class
95
- assert_kind_of 'parent_model', parent_model, String, Class
183
+ assert_kind_of 'name', name, Symbol
184
+ assert_kind_of 'repository_name', repository_name, Symbol
185
+ assert_kind_of 'child_model', child_model, String, Class
186
+ assert_kind_of 'parent_model', parent_model, String, Class
96
187
 
97
188
  if child_properties = options[:child_key]
98
189
  assert_kind_of 'options[:child_key]', child_properties, Array
@@ -110,6 +201,17 @@ module DataMapper
110
201
  @parent_model = parent_model
111
202
  @parent_properties = parent_properties # may be nil
112
203
  @options = options
204
+
205
+ # attempt to load the child_key if the parent and child model constants are defined
206
+ if model_defined?(@child_model) && model_defined?(@parent_model)
207
+ child_key
208
+ end
209
+ end
210
+
211
+ # @api private
212
+ def model_defined?(model)
213
+ # TODO: figure out other ways to see if the model is loaded
214
+ model.kind_of?(Class)
113
215
  end
114
216
  end # class Relationship
115
217
  end # module Associations