dm-core 0.9.5 → 0.9.6
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/Manifest.txt +3 -0
- data/lib/dm-core.rb +14 -20
- data/lib/dm-core/adapters.rb +18 -0
- data/lib/dm-core/adapters/abstract_adapter.rb +17 -10
- data/lib/dm-core/adapters/data_objects_adapter.rb +17 -22
- data/lib/dm-core/adapters/in_memory_adapter.rb +87 -0
- data/lib/dm-core/adapters/mysql_adapter.rb +1 -1
- data/lib/dm-core/adapters/postgres_adapter.rb +2 -2
- data/lib/dm-core/adapters/sqlite3_adapter.rb +1 -1
- data/lib/dm-core/associations.rb +3 -2
- data/lib/dm-core/associations/many_to_many.rb +3 -3
- data/lib/dm-core/associations/one_to_many.rb +10 -2
- data/lib/dm-core/associations/relationship.rb +20 -16
- data/lib/dm-core/auto_migrations.rb +5 -4
- data/lib/dm-core/collection.rb +10 -6
- data/lib/dm-core/dependency_queue.rb +2 -1
- data/lib/dm-core/identity_map.rb +3 -6
- data/lib/dm-core/model.rb +48 -27
- data/lib/dm-core/property.rb +57 -37
- data/lib/dm-core/property_set.rb +29 -22
- data/lib/dm-core/query.rb +57 -49
- data/lib/dm-core/repository.rb +3 -3
- data/lib/dm-core/resource.rb +17 -15
- data/lib/dm-core/scope.rb +7 -7
- data/lib/dm-core/support/kernel.rb +6 -2
- data/lib/dm-core/transaction.rb +7 -7
- data/lib/dm-core/version.rb +1 -1
- data/script/performance.rb +114 -22
- data/spec/integration/association_spec.rb +31 -2
- data/spec/integration/association_through_spec.rb +2 -0
- data/spec/integration/associations/many_to_many_spec.rb +152 -0
- data/spec/integration/associations/one_to_many_spec.rb +40 -3
- data/spec/integration/dependency_queue_spec.rb +0 -12
- data/spec/integration/postgres_adapter_spec.rb +1 -1
- data/spec/integration/property_spec.rb +3 -3
- data/spec/integration/query_spec.rb +39 -8
- data/spec/integration/resource_spec.rb +10 -6
- data/spec/integration/sti_spec.rb +22 -0
- data/spec/integration/strategic_eager_loading_spec.rb +21 -6
- data/spec/integration/type_spec.rb +1 -0
- data/spec/lib/model_loader.rb +10 -1
- data/spec/models/content.rb +16 -0
- data/spec/spec_helper.rb +4 -1
- data/spec/unit/adapters/data_objects_adapter_spec.rb +11 -11
- data/spec/unit/adapters/in_memory_adapter_spec.rb +98 -0
- data/spec/unit/associations/many_to_many_spec.rb +16 -1
- data/spec/unit/model_spec.rb +0 -16
- data/spec/unit/property_set_spec.rb +8 -1
- data/spec/unit/property_spec.rb +476 -240
- data/spec/unit/query_spec.rb +41 -0
- data/spec/unit/resource_spec.rb +75 -56
- data/tasks/ci.rb +4 -36
- data/tasks/dm.rb +3 -3
- metadata +5 -2
data/lib/dm-core/transaction.rb
CHANGED
@@ -13,12 +13,12 @@ module DataMapper
|
|
13
13
|
# In fact, it just calls #link with the given arguments at the end of the
|
14
14
|
# constructor.
|
15
15
|
#
|
16
|
-
def initialize(*things
|
16
|
+
def initialize(*things)
|
17
17
|
@transaction_primitives = {}
|
18
18
|
@state = :none
|
19
19
|
@adapters = {}
|
20
20
|
link(*things)
|
21
|
-
commit(
|
21
|
+
commit { |*block_args| yield(*block_args) } if block_given?
|
22
22
|
end
|
23
23
|
|
24
24
|
#
|
@@ -39,7 +39,7 @@ module DataMapper
|
|
39
39
|
# within this transaction. The transaction will begin and commit around
|
40
40
|
# the block, and rollback if an exception is raised.
|
41
41
|
#
|
42
|
-
def link(*things
|
42
|
+
def link(*things)
|
43
43
|
raise "Illegal state for link: #{@state}" unless @state == :none
|
44
44
|
things.each do |thing|
|
45
45
|
if thing.is_a?(Array)
|
@@ -56,7 +56,7 @@ module DataMapper
|
|
56
56
|
raise "Unknown argument to #{self}#link: #{thing.inspect}"
|
57
57
|
end
|
58
58
|
end
|
59
|
-
return commit(
|
59
|
+
return commit { |*block_args| yield(*block_args) } if block_given?
|
60
60
|
return self
|
61
61
|
end
|
62
62
|
|
@@ -83,12 +83,12 @@ module DataMapper
|
|
83
83
|
# If no block is given, it will simply commit any changes made since the
|
84
84
|
# Transaction did #begin.
|
85
85
|
#
|
86
|
-
def commit
|
86
|
+
def commit
|
87
87
|
if block_given?
|
88
88
|
raise "Illegal state for commit with block: #{@state}" unless @state == :none
|
89
89
|
begin
|
90
90
|
self.begin
|
91
|
-
rval = within(
|
91
|
+
rval = within { |*block_args| yield(*block_args) }
|
92
92
|
self.commit if @state == :begin
|
93
93
|
return rval
|
94
94
|
rescue Exception => e
|
@@ -128,7 +128,7 @@ module DataMapper
|
|
128
128
|
# adapter it is associated with, and it will ensures that it will pop the
|
129
129
|
# Transaction away again after the block is finished.
|
130
130
|
#
|
131
|
-
def within
|
131
|
+
def within
|
132
132
|
raise "No block provided" unless block_given?
|
133
133
|
raise "Illegal state for within: #{@state}" unless @state == :begin
|
134
134
|
@adapters.each do |adapter, state|
|
data/lib/dm-core/version.rb
CHANGED
data/script/performance.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
require File.join(File.dirname(__FILE__), '..', 'lib', 'dm-core')
|
4
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'dm-core', 'version')
|
4
5
|
|
5
6
|
require 'rubygems'
|
6
7
|
require 'ftools'
|
@@ -38,11 +39,11 @@ configuration_options[:socket] = socket_file unless socket_file.nil?
|
|
38
39
|
log_dir = DataMapper.root / 'log'
|
39
40
|
log_dir.mkdir unless log_dir.directory?
|
40
41
|
|
41
|
-
DataMapper::Logger.new(log_dir / 'dm.log', :
|
42
|
+
DataMapper::Logger.new(log_dir / 'dm.log', :off)
|
42
43
|
adapter = DataMapper.setup(:default, "mysql://root@localhost/data_mapper_1?socket=#{socket_file}")
|
43
44
|
|
44
45
|
if configuration_options[:adapter]
|
45
|
-
sqlfile = File.join(File.dirname(__FILE__),'..','tmp','
|
46
|
+
sqlfile = File.join(File.dirname(__FILE__),'..','tmp','performance.sql')
|
46
47
|
mysql_bin = %w[mysql mysql5].select{|bin| `which #{bin}`.length > 0 }
|
47
48
|
mysqldump_bin = %w[mysqldump mysqldump5].select{|bin| `which #{bin}`.length > 0 }
|
48
49
|
end
|
@@ -54,6 +55,15 @@ ActiveRecord::Base.establish_connection(configuration_options)
|
|
54
55
|
|
55
56
|
class ARExhibit < ActiveRecord::Base #:nodoc:
|
56
57
|
set_table_name 'exhibits'
|
58
|
+
|
59
|
+
belongs_to :user, :class_name => 'ARUser', :foreign_key => 'user_id'
|
60
|
+
end
|
61
|
+
|
62
|
+
class ARUser < ActiveRecord::Base #:nodoc:
|
63
|
+
set_table_name 'users'
|
64
|
+
|
65
|
+
has_many :exhibits, :foreign_key => 'user_id'
|
66
|
+
|
57
67
|
end
|
58
68
|
|
59
69
|
ARExhibit.find_by_sql('SELECT 1')
|
@@ -64,17 +74,39 @@ class Exhibit
|
|
64
74
|
property :id, Serial
|
65
75
|
property :name, String
|
66
76
|
property :zoo_id, Integer
|
77
|
+
property :user_id, Integer
|
67
78
|
property :notes, Text, :lazy => true
|
68
79
|
property :created_on, Date
|
80
|
+
|
81
|
+
belongs_to :user
|
69
82
|
# property :updated_at, DateTime
|
70
83
|
end
|
71
84
|
|
85
|
+
class User
|
86
|
+
include DataMapper::Resource
|
87
|
+
|
88
|
+
property :id, Serial
|
89
|
+
property :name, String
|
90
|
+
property :email, String
|
91
|
+
property :about, Text, :lazy => true
|
92
|
+
property :created_on, Date
|
93
|
+
|
94
|
+
end
|
95
|
+
|
72
96
|
touch_attributes = lambda do |exhibits|
|
73
97
|
[*exhibits].each do |exhibit|
|
74
98
|
exhibit.id
|
75
99
|
exhibit.name
|
76
100
|
exhibit.created_on
|
77
|
-
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
touch_relationships = lambda do |exhibits|
|
105
|
+
[*exhibits].each do |exhibit|
|
106
|
+
exhibit.id
|
107
|
+
exhibit.name
|
108
|
+
exhibit.created_on
|
109
|
+
exhibit.user
|
78
110
|
end
|
79
111
|
end
|
80
112
|
|
@@ -87,27 +119,49 @@ if sqlfile && File.exists?(sqlfile)
|
|
87
119
|
`#{mysql_bin} -u #{c[:username]} #{"-p#{c[:password]}" unless c[:password].blank?} #{c[:database]} < #{sqlfile}`
|
88
120
|
else
|
89
121
|
|
122
|
+
puts "Generating data for benchmarking..."
|
123
|
+
|
124
|
+
User.auto_migrate!
|
90
125
|
Exhibit.auto_migrate!
|
91
126
|
|
127
|
+
users = []
|
92
128
|
exhibits = []
|
129
|
+
|
93
130
|
# pre-compute the insert statements and fake data compilation,
|
94
131
|
# so the benchmarks below show the actual runtime for the execute
|
95
132
|
# method, minus the setup steps
|
96
|
-
|
133
|
+
|
134
|
+
# Using the same paragraph for all exhibits because it is very slow
|
135
|
+
# to generate unique paragraphs for all exhibits.
|
136
|
+
paragraph = Faker::Lorem.paragraphs.join($/)
|
137
|
+
|
138
|
+
10_000.times do |i|
|
139
|
+
users << [
|
140
|
+
'INSERT INTO `users` (`name`,`email`,`created_on`) VALUES (?, ?, ?)',
|
141
|
+
Faker::Name.name,
|
142
|
+
Faker::Internet.email,
|
143
|
+
Date.today
|
144
|
+
]
|
145
|
+
|
97
146
|
exhibits << [
|
98
|
-
'INSERT INTO `exhibits` (`name`, `zoo_id`, `notes`, `created_on`) VALUES (?, ?, ?, ?)',
|
147
|
+
'INSERT INTO `exhibits` (`name`, `zoo_id`, `user_id`, `notes`, `created_on`) VALUES (?, ?, ?, ?, ?)',
|
99
148
|
Faker::Company.name,
|
100
149
|
rand(10).ceil,
|
101
|
-
|
150
|
+
i,
|
151
|
+
paragraph,#Faker::Lorem.paragraphs.join($/),
|
102
152
|
Date.today
|
103
153
|
]
|
104
154
|
end
|
155
|
+
|
156
|
+
puts "Inserting 10,000 users..."
|
157
|
+
10_000.times { |i| adapter.execute(*users.at(i)) }
|
158
|
+
puts "Inserting 10,000 exhibits..."
|
105
159
|
10_000.times { |i| adapter.execute(*exhibits.at(i)) }
|
106
160
|
|
107
161
|
if sqlfile
|
108
162
|
answer = nil
|
109
163
|
until answer && answer[/^$|y|yes|n|no/]
|
110
|
-
print("Would you like to dump data into tmp/
|
164
|
+
print("Would you like to dump data into tmp/performance.sql (for faster setup)? [Yn]");
|
111
165
|
STDOUT.flush
|
112
166
|
answer = gets
|
113
167
|
end
|
@@ -115,7 +169,7 @@ else
|
|
115
169
|
if answer[/^$|y|yes/]
|
116
170
|
File.makedirs(File.dirname(sqlfile))
|
117
171
|
#adapter.execute("SELECT * INTO OUTFILE '#{sqlfile}' FROM exhibits;")
|
118
|
-
`#{mysqldump_bin} -u #{c[:username]} #{"-p#{c[:password]}" unless c[:password].blank?} #{c[:database]} exhibits > #{sqlfile}`
|
172
|
+
`#{mysqldump_bin} -u #{c[:username]} #{"-p#{c[:password]}" unless c[:password].blank?} #{c[:database]} exhibits users > #{sqlfile}`
|
119
173
|
puts "File saved\n"
|
120
174
|
end
|
121
175
|
end
|
@@ -127,37 +181,63 @@ TIMES = ENV['x'] ? ENV['x'].to_i : 10_000
|
|
127
181
|
puts "You can specify how many times you want to run the benchmarks with rake:perf x=(number)"
|
128
182
|
puts "Some tasks will be run 10 and 1000 times less than (number)"
|
129
183
|
puts "Benchmarks will now run #{TIMES} times"
|
184
|
+
# Inform about slow benchmark
|
185
|
+
# answer = nil
|
186
|
+
# until answer && answer[/^$|y|yes|n|no/]
|
187
|
+
# print("A slow benchmark exposing problems with SEL is newly added. It takes approx. 20s\n");
|
188
|
+
# print("you have scheduled it to run #{TIMES / 100} times.\nWould you still include the particular benchmark? [Yn]")
|
189
|
+
# STDOUT.flush
|
190
|
+
# answer = gets
|
191
|
+
# end
|
192
|
+
# run_rel_bench = answer[/^$|y|yes/] ? true : false
|
193
|
+
|
130
194
|
|
131
195
|
RBench.run(TIMES) do
|
132
196
|
|
133
197
|
column :times
|
134
|
-
column :dm, :title => "DM 0.9.4"
|
135
198
|
column :ar, :title => "AR 2.1"
|
136
|
-
column :
|
199
|
+
column :dm, :title => "DM #{DataMapper::VERSION}"
|
200
|
+
column :diff, :compare => [:ar,:dm]
|
201
|
+
|
202
|
+
report "Model.new (instantiation)" do
|
203
|
+
ar { ARExhibit.new }
|
204
|
+
dm { Exhibit.new }
|
205
|
+
end
|
206
|
+
|
207
|
+
report "Model.new (setting attributes)" do
|
208
|
+
attrs = {:name => 'sam', :zoo_id => 1}
|
209
|
+
ar { ARExhibit.new(attrs) }
|
210
|
+
dm { Exhibit.new(attrs) }
|
211
|
+
end
|
137
212
|
|
138
213
|
report "Model.get specific (not cached)" do
|
139
|
-
dm { touch_attributes[Exhibit.get(1)] }
|
140
214
|
ActiveRecord::Base.uncached { ar { touch_attributes[ARExhibit.find(1)] } }
|
215
|
+
dm { touch_attributes[Exhibit.get(1)] }
|
141
216
|
end
|
142
217
|
|
143
218
|
report "Model.get specific (cached)" do
|
144
|
-
Exhibit.repository(:default) { dm { touch_attributes[Exhibit.get(1)] } }
|
145
219
|
ActiveRecord::Base.cache { ar { touch_attributes[ARExhibit.find(1)] } }
|
220
|
+
Exhibit.repository(:default) { dm { touch_attributes[Exhibit.get(1)] } }
|
146
221
|
end
|
147
222
|
|
148
223
|
report "Model.first" do
|
149
|
-
dm { touch_attributes[Exhibit.first] }
|
150
224
|
ar { touch_attributes[ARExhibit.first] }
|
225
|
+
dm { touch_attributes[Exhibit.first] }
|
151
226
|
end
|
152
227
|
|
153
|
-
report "Model.all limit(100)", TIMES / 10 do
|
154
|
-
dm { touch_attributes[Exhibit.all(:limit => 100)] }
|
228
|
+
report "Model.all limit(100)", (TIMES / 10.0).ceil do
|
155
229
|
ar { touch_attributes[ARExhibit.find(:all, :limit => 100)] }
|
230
|
+
dm { touch_attributes[Exhibit.all(:limit => 100)] }
|
156
231
|
end
|
157
232
|
|
158
|
-
report "Model.all limit(
|
159
|
-
|
233
|
+
report "Model.all limit(100) with relationship", (TIMES / 10.0).ceil do
|
234
|
+
ar { touch_relationships[ARExhibit.all(:limit => 100, :include => [:user])] }
|
235
|
+
dm { touch_relationships[Exhibit.all(:limit => 100)] }
|
236
|
+
end
|
237
|
+
|
238
|
+
report "Model.all limit(10,000)", (TIMES / 1000.0).ceil do
|
160
239
|
ar { touch_attributes[ARExhibit.find(:all, :limit => 10_000)] }
|
240
|
+
dm { touch_attributes[Exhibit.all(:limit => 10_000)] }
|
161
241
|
end
|
162
242
|
|
163
243
|
create_exhibit = {
|
@@ -168,25 +248,37 @@ RBench.run(TIMES) do
|
|
168
248
|
}
|
169
249
|
|
170
250
|
report "Model.create" do
|
171
|
-
dm { Exhibit.create(create_exhibit) }
|
172
251
|
ar { ARExhibit.create(create_exhibit) }
|
252
|
+
dm { Exhibit.create(create_exhibit) }
|
253
|
+
end
|
254
|
+
|
255
|
+
report "Resource#attributes" do
|
256
|
+
attrs_first = {:name => 'sam', :zoo_id => 1}
|
257
|
+
attrs_second = {:name => 'tom', :zoo_id => 1}
|
258
|
+
ar { e = ARExhibit.new(attrs_first); e.attributes = attrs_second }
|
259
|
+
dm { e = Exhibit.new(attrs_first); e.attributes = attrs_second }
|
173
260
|
end
|
174
261
|
|
175
262
|
report "Resource#update" do
|
176
|
-
|
177
|
-
|
263
|
+
ar { e = ARExhibit.find(1); e.name = 'bob'; e.save }
|
264
|
+
dm { e = Exhibit.get(1); e.name = 'bob'; e.save }
|
178
265
|
end
|
179
266
|
|
180
267
|
report "Resource#destroy" do
|
181
|
-
dm { Exhibit.first.destroy }
|
182
268
|
ar { ARExhibit.first.destroy }
|
269
|
+
dm { Exhibit.first.destroy }
|
183
270
|
end
|
184
271
|
|
185
|
-
|
272
|
+
report "Model.transaction" do
|
273
|
+
ar { ARExhibit.transaction { ARExhibit.new } }
|
274
|
+
dm { Exhibit.transaction { Exhibit.new } }
|
275
|
+
end
|
186
276
|
|
277
|
+
summary "Total"
|
187
278
|
end
|
188
279
|
|
189
280
|
connection = adapter.send(:create_connection)
|
190
281
|
command = connection.create_command("DROP TABLE exhibits")
|
282
|
+
command = connection.create_command("DROP TABLE users")
|
191
283
|
command.execute_non_query rescue nil
|
192
284
|
connection.close
|
@@ -302,6 +302,18 @@ if ADAPTER
|
|
302
302
|
area.should respond_to(:machine=)
|
303
303
|
end
|
304
304
|
|
305
|
+
it 'should create the foreign key property immediately' do
|
306
|
+
class Duck
|
307
|
+
include DataMapper::Resource
|
308
|
+
property :id, Serial
|
309
|
+
belongs_to :sky
|
310
|
+
end
|
311
|
+
Duck.properties.slice(:sky_id).compact.should_not be_empty
|
312
|
+
duck = Duck.new
|
313
|
+
duck.should respond_to(:sky_id)
|
314
|
+
duck.should respond_to(:sky_id=)
|
315
|
+
end
|
316
|
+
|
305
317
|
it 'should load without the parent'
|
306
318
|
|
307
319
|
it 'should allow substituting the parent' do
|
@@ -331,7 +343,7 @@ if ADAPTER
|
|
331
343
|
end
|
332
344
|
end
|
333
345
|
|
334
|
-
FlightlessBirds::Ostrich.properties.slice(:sky_id).should_not be_empty
|
346
|
+
FlightlessBirds::Ostrich.properties(ADAPTER).slice(:sky_id).compact.should_not be_empty
|
335
347
|
end
|
336
348
|
end
|
337
349
|
|
@@ -571,6 +583,15 @@ if ADAPTER
|
|
571
583
|
machine.areas.size.should == 4
|
572
584
|
end
|
573
585
|
|
586
|
+
it "#build should add exactly one instance of the built record" do
|
587
|
+
machine = Machine.create(:name => 'my machine')
|
588
|
+
|
589
|
+
original_size = machine.areas.size
|
590
|
+
machine.areas.build(:name => "an area", :machine => machine)
|
591
|
+
|
592
|
+
machine.areas.size.should == original_size + 1
|
593
|
+
end
|
594
|
+
|
574
595
|
it '#<< should add default values for relationships that have conditions' do
|
575
596
|
# it should add default values
|
576
597
|
machine = Machine.new(:name => 'my machine')
|
@@ -1169,8 +1190,16 @@ if ADAPTER
|
|
1169
1190
|
#
|
1170
1191
|
|
1171
1192
|
it 'should join tables in the right order during has 1 => has n => has 1 queries' do
|
1172
|
-
child = Sweets::Shop.first.children(:name => 'Snotling nr 3').booger(:name.like => '
|
1193
|
+
child = Sweets::Shop.first.children(:name => 'Snotling nr 3').booger(:name.like => 'Nasty booger')
|
1173
1194
|
child.should_not be_nil
|
1195
|
+
child.size.should eql(1)
|
1196
|
+
child.first.name.should eql("Nasty booger")
|
1197
|
+
end
|
1198
|
+
|
1199
|
+
it 'should join tables in the right order for belongs_to relations' do
|
1200
|
+
wife = Sweets::Wife.first(Sweets::Wife.shop_owner.name => "Betsy", Sweets::Wife.shop_owner.shop.name => "Betsy's")
|
1201
|
+
wife.should_not be_nil
|
1202
|
+
wife.name.should eql("Barry")
|
1174
1203
|
end
|
1175
1204
|
|
1176
1205
|
it 'should raise exception if you try to change it' do
|
@@ -50,10 +50,12 @@ if ADAPTER
|
|
50
50
|
has n, :relationships
|
51
51
|
has n, :related_posts,
|
52
52
|
:through => :relationships,
|
53
|
+
:child_key => [:post_id],
|
53
54
|
:class_name => "Post"
|
54
55
|
|
55
56
|
has n, :void_tags,
|
56
57
|
:through => :taggings,
|
58
|
+
:child_key => [:post_id],
|
57
59
|
:class_name => "Tag",
|
58
60
|
:remote_relationship_name => :tag,
|
59
61
|
Post.taggings.tag.voided => true
|
@@ -294,4 +294,156 @@ describe DataMapper::Associations::ManyToMany::Proxy do
|
|
294
294
|
end
|
295
295
|
end
|
296
296
|
|
297
|
+
describe "with renamed associations" do
|
298
|
+
before :all do
|
299
|
+
class Singer
|
300
|
+
include DataMapper::Resource
|
301
|
+
|
302
|
+
def self.default_repository_name; ADAPTER end
|
303
|
+
|
304
|
+
property :id, Serial
|
305
|
+
property :name, String
|
306
|
+
|
307
|
+
has n, :tunes, :through => Resource, :class_name => 'Song'
|
308
|
+
end
|
309
|
+
|
310
|
+
class Song
|
311
|
+
include DataMapper::Resource
|
312
|
+
|
313
|
+
def self.default_repository_name; ADAPTER end
|
314
|
+
|
315
|
+
property :id, Serial
|
316
|
+
property :title, String
|
317
|
+
|
318
|
+
has n, :performers, :through => Resource, :class_name => 'Singer'
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
before do
|
323
|
+
[ Singer, Song, SingerSong ].each { |k| k.auto_migrate! }
|
324
|
+
|
325
|
+
song_1 = Song.create(:title => "Dubliners")
|
326
|
+
song_2 = Song.create(:title => "Portrait of the Artist as a Young Man")
|
327
|
+
song_3 = Song.create(:title => "Ulysses")
|
328
|
+
|
329
|
+
singer_1 = Singer.create(:name => "Jon Doe")
|
330
|
+
singer_2 = Singer.create(:name => "Jane Doe")
|
331
|
+
|
332
|
+
SingerSong.create(:song => song_1, :singer => singer_1)
|
333
|
+
SingerSong.create(:song => song_2, :singer => singer_1)
|
334
|
+
SingerSong.create(:song => song_1, :singer => singer_2)
|
335
|
+
|
336
|
+
@parent = song_3
|
337
|
+
@association = @parent.performers
|
338
|
+
@other = [ singer_1 ]
|
339
|
+
end
|
340
|
+
|
341
|
+
it "should provide #replace" do
|
342
|
+
@association.should respond_to(:replace)
|
343
|
+
end
|
344
|
+
|
345
|
+
it "should correctly link records" do
|
346
|
+
Song.get(1).should have(2).performers
|
347
|
+
Song.get(2).should have(1).performers
|
348
|
+
Song.get(3).should have(0).performers
|
349
|
+
Singer.get(1).should have(2).tunes
|
350
|
+
Singer.get(2).should have(1).tunes
|
351
|
+
end
|
352
|
+
|
353
|
+
it "should be able to have associated objects manually added" do
|
354
|
+
song = Song.get(3)
|
355
|
+
song.should have(0).performers
|
356
|
+
|
357
|
+
be = SingerSong.new(:song_id => song.id, :singer_id => 2)
|
358
|
+
song.singer_songs << be
|
359
|
+
song.save
|
360
|
+
|
361
|
+
song.reload.should have(1).performers
|
362
|
+
end
|
363
|
+
|
364
|
+
it "should automatically added necessary through class" do
|
365
|
+
song = Song.get(3)
|
366
|
+
song.should have(0).performers
|
367
|
+
|
368
|
+
song.performers << Singer.get(1)
|
369
|
+
song.performers << Singer.new(:name => "Jimmy John")
|
370
|
+
song.save
|
371
|
+
|
372
|
+
song.reload.should have(2).performers
|
373
|
+
end
|
374
|
+
|
375
|
+
it "should react correctly to a new record" do
|
376
|
+
song = Song.new(:title => "Finnegan's Wake")
|
377
|
+
singer = Singer.get(2)
|
378
|
+
song.should have(0).performers
|
379
|
+
singer.should have(1).tunes
|
380
|
+
|
381
|
+
song.performers << singer
|
382
|
+
song.save
|
383
|
+
|
384
|
+
song.reload.should have(1).performers
|
385
|
+
singer.reload.should have(2).tunes
|
386
|
+
end
|
387
|
+
|
388
|
+
it "should be able to delete intermediate model" do
|
389
|
+
song = Song.get(1)
|
390
|
+
song.should have(2).singer_songs
|
391
|
+
song.should have(2).performers
|
392
|
+
|
393
|
+
be = SingerSong.get(1,1)
|
394
|
+
song.singer_songs.delete(be)
|
395
|
+
song.save
|
396
|
+
|
397
|
+
song.reload
|
398
|
+
song.should have(1).singer_songs
|
399
|
+
song.should have(1).performers
|
400
|
+
end
|
401
|
+
|
402
|
+
it "should be clearable" do
|
403
|
+
repository(ADAPTER) do
|
404
|
+
song = Song.get(2)
|
405
|
+
song.should have(1).singer_songs
|
406
|
+
song.should have(1).performers
|
407
|
+
|
408
|
+
song.performers.clear
|
409
|
+
song.save
|
410
|
+
|
411
|
+
song.reload
|
412
|
+
song.should have(0).singer_songs
|
413
|
+
song.should have(0).performers
|
414
|
+
end
|
415
|
+
repository(ADAPTER) do
|
416
|
+
Song.get(2).should have(0).performers
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
it "should be able to delete one object" do
|
421
|
+
song = Song.get(1)
|
422
|
+
song.should have(2).singer_songs
|
423
|
+
song.should have(2).performers
|
424
|
+
|
425
|
+
editor = song.performers.first
|
426
|
+
song.performers.delete(editor)
|
427
|
+
song.save
|
428
|
+
|
429
|
+
song.reload
|
430
|
+
song.should have(1).singer_songs
|
431
|
+
song.should have(1).performers
|
432
|
+
editor.reload.tunes.should_not include(song)
|
433
|
+
end
|
434
|
+
|
435
|
+
it "should be destroyable" do
|
436
|
+
pending "cannot destroy a collection yet" do
|
437
|
+
song = Song.get(2)
|
438
|
+
song.should have(1).performers
|
439
|
+
|
440
|
+
song.performers.destroy
|
441
|
+
song.save
|
442
|
+
|
443
|
+
song.reload
|
444
|
+
song.should have(0).performers
|
445
|
+
end
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
297
449
|
end
|