sequel 3.5.0 → 3.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +108 -0
- data/README.rdoc +25 -14
- data/Rakefile +20 -1
- data/doc/advanced_associations.rdoc +61 -64
- data/doc/cheat_sheet.rdoc +16 -7
- data/doc/opening_databases.rdoc +3 -3
- data/doc/prepared_statements.rdoc +1 -1
- data/doc/reflection.rdoc +2 -1
- data/doc/release_notes/3.6.0.txt +366 -0
- data/doc/schema.rdoc +19 -14
- data/lib/sequel/adapters/amalgalite.rb +5 -27
- data/lib/sequel/adapters/jdbc.rb +13 -3
- data/lib/sequel/adapters/jdbc/h2.rb +17 -0
- data/lib/sequel/adapters/jdbc/mysql.rb +20 -7
- data/lib/sequel/adapters/mysql.rb +4 -3
- data/lib/sequel/adapters/oracle.rb +1 -1
- data/lib/sequel/adapters/postgres.rb +87 -28
- data/lib/sequel/adapters/shared/mssql.rb +47 -6
- data/lib/sequel/adapters/shared/mysql.rb +12 -31
- data/lib/sequel/adapters/shared/postgres.rb +15 -12
- data/lib/sequel/adapters/shared/sqlite.rb +18 -0
- data/lib/sequel/adapters/sqlite.rb +1 -16
- data/lib/sequel/connection_pool.rb +1 -1
- data/lib/sequel/core.rb +1 -1
- data/lib/sequel/database.rb +1 -1
- data/lib/sequel/database/schema_generator.rb +2 -0
- data/lib/sequel/database/schema_sql.rb +1 -1
- data/lib/sequel/dataset.rb +5 -179
- data/lib/sequel/dataset/actions.rb +123 -0
- data/lib/sequel/dataset/convenience.rb +18 -10
- data/lib/sequel/dataset/features.rb +65 -0
- data/lib/sequel/dataset/prepared_statements.rb +29 -23
- data/lib/sequel/dataset/query.rb +429 -0
- data/lib/sequel/dataset/sql.rb +67 -435
- data/lib/sequel/model/associations.rb +77 -13
- data/lib/sequel/model/base.rb +30 -8
- data/lib/sequel/model/errors.rb +4 -4
- data/lib/sequel/plugins/caching.rb +38 -15
- data/lib/sequel/plugins/force_encoding.rb +18 -7
- data/lib/sequel/plugins/hook_class_methods.rb +4 -0
- data/lib/sequel/plugins/many_through_many.rb +1 -1
- data/lib/sequel/plugins/nested_attributes.rb +40 -11
- data/lib/sequel/plugins/serialization.rb +17 -3
- data/lib/sequel/plugins/validation_helpers.rb +65 -18
- data/lib/sequel/sql.rb +23 -1
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/mssql_spec.rb +96 -10
- data/spec/adapters/mysql_spec.rb +19 -0
- data/spec/adapters/postgres_spec.rb +65 -2
- data/spec/adapters/sqlite_spec.rb +10 -0
- data/spec/core/core_sql_spec.rb +9 -0
- data/spec/core/database_spec.rb +8 -4
- data/spec/core/dataset_spec.rb +122 -29
- data/spec/core/expression_filters_spec.rb +17 -0
- data/spec/extensions/caching_spec.rb +43 -3
- data/spec/extensions/force_encoding_spec.rb +43 -1
- data/spec/extensions/nested_attributes_spec.rb +55 -2
- data/spec/extensions/validation_helpers_spec.rb +71 -0
- data/spec/integration/associations_test.rb +281 -0
- data/spec/integration/dataset_test.rb +383 -9
- data/spec/integration/eager_loader_test.rb +0 -65
- data/spec/integration/model_test.rb +110 -0
- data/spec/integration/plugin_test.rb +306 -0
- data/spec/integration/prepared_statement_test.rb +32 -0
- data/spec/integration/schema_test.rb +8 -3
- data/spec/integration/spec_helper.rb +1 -25
- data/spec/model/association_reflection_spec.rb +38 -0
- data/spec/model/associations_spec.rb +184 -8
- data/spec/model/eager_loading_spec.rb +23 -0
- data/spec/model/model_spec.rb +8 -0
- data/spec/model/record_spec.rb +84 -1
- metadata +9 -2
data/CHANGELOG
CHANGED
@@ -1,3 +1,111 @@
|
|
1
|
+
=== 3.6.0 (2009-11-02)
|
2
|
+
|
3
|
+
* Make the MSSQL shared adapter correctly parse the column schema information for tables in the non-default database schema (rohit.namjoshi)
|
4
|
+
|
5
|
+
* Use save_changes instead of save when updating existing associated objects in the nested_attributes plugin (jeremyevans)
|
6
|
+
|
7
|
+
* Allow Model#save_changes to accept an option hash that is passed to save, so you can save changes without validating (jeremyevans)
|
8
|
+
|
9
|
+
* Make nested_attributes plugin add newly created objects to cached association array immediately (jeremyevans)
|
10
|
+
|
11
|
+
* Make add_ association method not add the associated object to the cached array if it's already there (jeremyevans)
|
12
|
+
|
13
|
+
* Add Model#modified! for explicitly marking an object as modified, so save_changes/update will run callbacks even if no columns have been modified (jeremyevans)
|
14
|
+
|
15
|
+
* Add support for a :fields option in the nested attributes plugin, and only allow updating of the fields specified (jeremyevans)
|
16
|
+
|
17
|
+
* Don't allow modifying keys related to the association when updating existing objects in the nested_attributes plugin (jeremyevans)
|
18
|
+
|
19
|
+
* Add associated_object_keys method to AssociationReflection objects, specifying the key(s) in the associated model table related to the association (jeremyevans)
|
20
|
+
|
21
|
+
* Support the memcached protocol in the caching plugin via the new :ignore_exceptions option (EppO, jeremyevans)
|
22
|
+
|
23
|
+
* Don't modify array with a string and placeholders passed to Dataset#filter or related methods (jeremyevans)
|
24
|
+
|
25
|
+
* Speed up Amalgalite adapter (copiousfreetime)
|
26
|
+
|
27
|
+
* Fix bound variables on PostgreSQL when using nil and potentially other values (jeremyevans)
|
28
|
+
|
29
|
+
* Allow easier overriding of default options used in the validation_helpers plugin (jeremyevans)
|
30
|
+
|
31
|
+
* Have Dataset#literal_other call sql_literal on the object if it responds to it (heda, michaeldiamond)
|
32
|
+
|
33
|
+
* Fix Dataset#explain in the amalgalite adapter (jeremyevans)
|
34
|
+
|
35
|
+
* Have Model.table_name respect table aliases (jeremyevans)
|
36
|
+
|
37
|
+
* Allow marshalling of saved model records after calling #marshallable! (jeremyevans)
|
38
|
+
|
39
|
+
* one_to_many association methods now make sure that the removed object is currently associated to the receiver (jeremyevans)
|
40
|
+
|
41
|
+
* Model association add_ and remove_ methods now have more descriptive error messages (jeremyevans)
|
42
|
+
|
43
|
+
* Model association add_ and remove_ methods now make sure passed object is of the correct class (jeremyevans)
|
44
|
+
|
45
|
+
* Model association remove_ methods now accept a primary key value and disassociate the associated model object (natewiger, jeremyevans)
|
46
|
+
|
47
|
+
* Model association add_ methods now accept a hash and create a new associated model object (natewiger, jeremyevans)
|
48
|
+
|
49
|
+
* Dataset#window for PostgreSQL datasets now respects previous windows (jeremyevans)
|
50
|
+
|
51
|
+
* Dataset#simple_select_all? now ignores options that don't affect the SQL being issued (jeremyevans)
|
52
|
+
|
53
|
+
* Account for table aliases in eager_graph (mluu)
|
54
|
+
|
55
|
+
* Add support for MSSQL clustered index creation (mluu)
|
56
|
+
|
57
|
+
* Implement insert_select in the MSSQL adapter via OUTPUT. Can be disabled via disable_insert_output. (jfirebaugh, mluu)
|
58
|
+
|
59
|
+
* Correct error handling when beginning a transaction fails (jfirebaugh, mluu)
|
60
|
+
|
61
|
+
* Correct JDBC binding for Time objects in prepared statements (jfirebaugh, jeremyevans)
|
62
|
+
|
63
|
+
* Emulate JOIN USING clause poorly using JOIN ON if the database doesn't support JOIN USING (e.g. MSSQL, H2) (jfirebaugh, jeremyevans)
|
64
|
+
|
65
|
+
* Support column aliases in Dataset#group_and_count (jfirebaugh)
|
66
|
+
|
67
|
+
* Support preparing insert statements of the form insert(1,2,3) and insert(columns, values) (jfirebaugh)
|
68
|
+
|
69
|
+
* Fix add_index for tables in non-default schema (jfirebaugh)
|
70
|
+
|
71
|
+
* Allow named placeholders in placeholder literal strings (jeremyevans)
|
72
|
+
|
73
|
+
* Allow the force_encoding plugin to work when refreshing (jeremyevans)
|
74
|
+
|
75
|
+
* Add Dataset#bind for setting bound variable values before calling #call (jeremyevans)
|
76
|
+
|
77
|
+
* Add additional join methods to Dataset: (cross|natural|(natural_)?(full|left|right))_join (jeremyevans)
|
78
|
+
|
79
|
+
* Fix use a dataset aggregate methods (e.g. sum) on limited/grouped/etc. datasets (jeremyevans)
|
80
|
+
|
81
|
+
* Clear changed_columns when saving new model objects with a database adapter that supports insert_select, such as postgres (jeremyevans)
|
82
|
+
|
83
|
+
* Fix Dataset#replace with default values on MySQL, and respect insert-related options (jeremyevans)
|
84
|
+
|
85
|
+
* Fix Dataset#lock on PostgreSQL (jeremyevans)
|
86
|
+
|
87
|
+
* Fix Dataset#explain on SQLite (jeremyevans)
|
88
|
+
|
89
|
+
* Add Dataset#use_cursor to the native postgres adapter, for processing large datasets (jeremyevans)
|
90
|
+
|
91
|
+
* Don't ignore Class.inherited in Sequel::Model.inherited (antage) (#277)
|
92
|
+
|
93
|
+
* Optimize JDBC::MySQL::DatabaseMethods#last_insert_id to prevent additional queries (tmm1)
|
94
|
+
|
95
|
+
* Fix use of MSSQL with ruby 1.9 (cult hero)
|
96
|
+
|
97
|
+
* Don't try to load associated objects when the current object has NULL for one of the key fields (jeremyevans)
|
98
|
+
|
99
|
+
* No longer require GROUP BY to use HAVING, except on SQLite (jeremyevans)
|
100
|
+
|
101
|
+
* Add emulated support for the lack of multiple column IN/NOT IN support in MSSQL and SQLite (jeremyevans)
|
102
|
+
|
103
|
+
* Add emulated support for #ilike on MSSQL and H2 (jeremyevans)
|
104
|
+
|
105
|
+
* Add a :distinct option for all associations, which uses the SQL DISTINCT clause (jeremyevans)
|
106
|
+
|
107
|
+
* Don't require :: prefix for constant lookups in instance_evaled virtual row blocks on ruby 1.9 (jeremyevans)
|
108
|
+
|
1
109
|
=== 3.5.0 (2009-10-01)
|
2
110
|
|
3
111
|
* Correctly literalize timezones in timestamps when using Oracle (jeremyevans)
|
data/README.rdoc
CHANGED
@@ -75,13 +75,13 @@ Sequel is designed to take the hassle away from connecting to databases and mani
|
|
75
75
|
|
76
76
|
Sequel uses the concept of datasets to retrieve data. A Dataset object encapsulates an SQL query and supports chainability, letting you fetch data using a convenient Ruby DSL that is both concise and flexible.
|
77
77
|
|
78
|
-
For example, the following one-liner returns the average GDP for
|
78
|
+
For example, the following one-liner returns the average GDP for countries in the middle east region:
|
79
79
|
|
80
|
-
DB[:countries].filter(:region => 'Middle East').
|
80
|
+
DB[:countries].filter(:region => 'Middle East').avg(:GDP)
|
81
81
|
|
82
82
|
Which is equivalent to:
|
83
83
|
|
84
|
-
SELECT avg(GDP) FROM countries WHERE region = 'Middle East'
|
84
|
+
SELECT avg(GDP) FROM countries WHERE region = 'Middle East'
|
85
85
|
|
86
86
|
Since datasets retrieve records only when needed, they can be stored and later reused. Records are fetched as hashes (or custom model objects), and are accessed using an Enumerable interface:
|
87
87
|
|
@@ -120,8 +120,10 @@ You can specify a block to connect, which will disconnect from the database afte
|
|
120
120
|
|
121
121
|
=== Arbitrary SQL queries
|
122
122
|
|
123
|
-
|
124
|
-
|
123
|
+
You can execute arbitrary SQL code using Database#run:
|
124
|
+
|
125
|
+
DB.run("create table t (a text, b text)")
|
126
|
+
DB.run("insert into t values ('a', 'b')")
|
125
127
|
|
126
128
|
You can also create datasets based on raw SQL:
|
127
129
|
|
@@ -137,6 +139,7 @@ You can also fetch records with raw SQL through the dataset:
|
|
137
139
|
|
138
140
|
You can use placeholders in your SQL string as well:
|
139
141
|
|
142
|
+
name = 'Jim'
|
140
143
|
DB['select * from items where name = ?', name].each do |row|
|
141
144
|
p row
|
142
145
|
end
|
@@ -407,7 +410,7 @@ Sequel models allow you to use any column as a primary key, and even composite k
|
|
407
410
|
post = Post['ruby', 'hello world']
|
408
411
|
post.pk #=> ['ruby', 'hello world']
|
409
412
|
|
410
|
-
You can also define a model class that does not have a primary key, but then you lose the ability to update records.
|
413
|
+
You can also define a model class that does not have a primary key, but then you lose the ability to easily update records.
|
411
414
|
|
412
415
|
A model instance can also be fetched by specifying a condition:
|
413
416
|
|
@@ -439,9 +442,14 @@ You can read the record values as object attributes (assuming the attribute name
|
|
439
442
|
You can also change record values:
|
440
443
|
|
441
444
|
post.title = 'hey there'
|
445
|
+
# or
|
446
|
+
post.set(:title=>'hey there')
|
447
|
+
|
448
|
+
That will just change the value for the object, it will not persist the changes to the database. To persist the record, call the #save method:
|
449
|
+
|
442
450
|
post.save
|
443
451
|
|
444
|
-
|
452
|
+
If you want to modify record values and save the object after doing so, use the #update method:
|
445
453
|
|
446
454
|
post.update(:title => 'hey there')
|
447
455
|
|
@@ -451,7 +459,7 @@ New records can be created by calling Model.create:
|
|
451
459
|
|
452
460
|
post = Post.create(:title => 'hello world')
|
453
461
|
|
454
|
-
Another way is to construct a new instance and save it:
|
462
|
+
Another way is to construct a new instance and save it later:
|
455
463
|
|
456
464
|
post = Post.new
|
457
465
|
post.title = 'hello world'
|
@@ -459,27 +467,30 @@ Another way is to construct a new instance and save it:
|
|
459
467
|
|
460
468
|
You can also supply a block to Model.new and Model.create:
|
461
469
|
|
462
|
-
post = Post.create{|p| p.title = 'hello world'}
|
463
|
-
|
464
470
|
post = Post.new do |p|
|
465
471
|
p.title = 'hello world'
|
466
|
-
p.save
|
467
472
|
end
|
468
473
|
|
474
|
+
post = Post.create{|p| p.title = 'hello world'}
|
475
|
+
|
469
476
|
=== Hooks
|
470
477
|
|
471
478
|
You can execute custom code when creating, updating, or deleting records by defining hook methods. The before_create and after_create hook methods wrap record creation. The before_update and after_update hook methods wrap record updating. The before_save and after_save hook methods wrap record creation and updating. The before_destroy and after_destroy hook methods wrap destruction. The before_validation and after_validation hook methods wrap validation. Example:
|
472
479
|
|
473
480
|
class Post < Sequel::Model
|
474
481
|
def after_create
|
482
|
+
super
|
475
483
|
author.increase_post_count
|
476
484
|
end
|
477
485
|
|
478
486
|
def after_destroy
|
487
|
+
super
|
479
488
|
author.decrease_post_count
|
480
489
|
end
|
481
490
|
end
|
482
491
|
|
492
|
+
Note the use of super if you define your own hook methods. Almost all Sequel::Model class and instance methods (not just hook methods) can be overridden safely, but you have to make sure to call super when doing so, otherwise you risk breaking things.
|
493
|
+
|
483
494
|
For the example above, you should probably use a database trigger if you can. Hooks can be used for data integrity, but they will only enforce that integrity when you are using the model. If you plan on allowing any other access to the database, it's best to use database triggers for data integrity.
|
484
495
|
|
485
496
|
=== Deleting records
|
@@ -636,7 +647,7 @@ Sequel models also provide a short hand notation for filters:
|
|
636
647
|
|
637
648
|
=== Model Validations
|
638
649
|
|
639
|
-
You can define a validate method for your model, which save
|
650
|
+
You can define a validate method for your model, which #save
|
640
651
|
will check before attempting to save the model in the database.
|
641
652
|
If an attribute of the model isn't valid, you should add a error
|
642
653
|
message for that attribute to the model object's errors. If an
|
@@ -645,8 +656,8 @@ raise an error or return false depending on how it is configured.
|
|
645
656
|
|
646
657
|
class Post < Sequel::Model
|
647
658
|
def validate
|
648
|
-
errors
|
649
|
-
errors
|
659
|
+
errors.add(:name, "can't be empty") if name.empty?
|
660
|
+
errors.add(:written_on, "should be in the past") if written_on >= Time.now
|
650
661
|
end
|
651
662
|
end
|
652
663
|
|
data/Rakefile
CHANGED
@@ -168,12 +168,26 @@ begin
|
|
168
168
|
t.spec_opts = spec_opts.call
|
169
169
|
end
|
170
170
|
|
171
|
+
desc "Run integration tests with coverage"
|
172
|
+
Spec::Rake::SpecTask.new("integration_cov") do |t|
|
173
|
+
t.spec_files = Dir["spec/integration/*_test.rb"]
|
174
|
+
t.spec_opts = spec_opts.call
|
175
|
+
t.rcov, t.rcov_opts = rcov_opts.call
|
176
|
+
end
|
177
|
+
|
171
178
|
%w'postgres sqlite mysql informix oracle firebird mssql'.each do |adapter|
|
172
|
-
desc "Run #{adapter} specs
|
179
|
+
desc "Run #{adapter} specs"
|
173
180
|
Spec::Rake::SpecTask.new("spec_#{adapter}") do |t|
|
174
181
|
t.spec_files = ["spec/adapters/#{adapter}_spec.rb"] + Dir["spec/integration/*_test.rb"]
|
175
182
|
t.spec_opts = spec_opts.call
|
176
183
|
end
|
184
|
+
|
185
|
+
desc "Run #{adapter} specs with coverage"
|
186
|
+
Spec::Rake::SpecTask.new("spec_#{adapter}_cov") do |t|
|
187
|
+
t.spec_files = ["spec/adapters/#{adapter}_spec.rb"] + Dir["spec/integration/*_test.rb"]
|
188
|
+
t.spec_opts = spec_opts.call
|
189
|
+
t.rcov, t.rcov_opts = rcov_opts.call
|
190
|
+
end
|
177
191
|
end
|
178
192
|
rescue LoadError
|
179
193
|
end
|
@@ -197,3 +211,8 @@ desc "Print Sequel version"
|
|
197
211
|
task :version do
|
198
212
|
puts VERS.call
|
199
213
|
end
|
214
|
+
|
215
|
+
desc "Check syntax of all .rb files"
|
216
|
+
task :check_syntax do
|
217
|
+
Dir['**/*.rb'].each{|file| print `#{ENV['RUBY'] || :ruby} -c #{file} | fgrep -v "Syntax OK"`}
|
218
|
+
end
|
@@ -196,8 +196,8 @@ Sequel::Model:
|
|
196
196
|
@author = Author.first
|
197
197
|
@author.books
|
198
198
|
|
199
|
-
If you use an association other than belongs_to in the associated model,
|
200
|
-
|
199
|
+
If you use an association other than belongs_to in the associated model, you'll have
|
200
|
+
to specify some of the :*key options and write a short method.
|
201
201
|
|
202
202
|
ActiveRecord:
|
203
203
|
|
@@ -222,23 +222,7 @@ Sequel::Model:
|
|
222
222
|
|
223
223
|
class Firm < Sequel::Model
|
224
224
|
one_to_many :clients
|
225
|
-
|
226
|
-
:dataset=>proc{Invoice.eager_graph(:client).filter(:client__firm_id=>pk)}, \
|
227
|
-
:after_load=>(proc do |firm, invs|
|
228
|
-
invs.each do |inv|
|
229
|
-
inv.client.associations[:firm] = inv.associations[:firm] = firm
|
230
|
-
end
|
231
|
-
end), \
|
232
|
-
:eager_loader=>(proc do |key_hash, firms, associations|
|
233
|
-
id_map = key_hash[Firm.primary_key]
|
234
|
-
firms.each{|firm| firm.associations[:invoices] = []}
|
235
|
-
Invoice.eager_graph(:client).filter(:client__firm_id=>id_map.keys).all do |inv|
|
236
|
-
id_map[inv.client.firm_id].each do |firm|
|
237
|
-
inv.client.associations[:firm] = inv.associations[:firm] = firm
|
238
|
-
firm.associations[:invoices] << inv
|
239
|
-
end
|
240
|
-
end
|
241
|
-
end)
|
225
|
+
many_to_many :invoices, :join_table=>:clients, :right_key=>:id, :right_primary_key=>:client_id
|
242
226
|
end
|
243
227
|
|
244
228
|
class Client < Sequel::Model
|
@@ -248,55 +232,28 @@ Sequel::Model:
|
|
248
232
|
|
249
233
|
class Invoice < Sequel::Model
|
250
234
|
many_to_one :client
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
# client with this invoice, instead of all clients of the firm
|
256
|
-
inv.associations[:client] = firm.associations.delete(:clients).first
|
257
|
-
end), \
|
258
|
-
:eager_loader=>(proc do |key_hash, invoices, associations|
|
259
|
-
id_map = {}
|
260
|
-
invoices.each do |inv|
|
261
|
-
inv.associations[:firm] = nil
|
262
|
-
inv.associations[:client] = nil
|
263
|
-
(id_map[inv.client_id] ||= []) << inv
|
264
|
-
end
|
265
|
-
Firm.eager_graph(:clients).filter(:clients__id=>id_map.keys).all do |firm|
|
266
|
-
# Delete the cached associations from firm, because it only has the
|
267
|
-
# clients related the invoices being eagerly loaded, instead of all
|
268
|
-
# clients of the firm.
|
269
|
-
firm.associations.delete(:clients).each do |client|
|
270
|
-
id_map[client.pk].each do |inv|
|
271
|
-
inv.associations[:firm] = firm
|
272
|
-
inv.associations[:client] = client
|
273
|
-
end
|
274
|
-
end
|
275
|
-
end
|
276
|
-
end)
|
235
|
+
|
236
|
+
def firm
|
237
|
+
client.firm if client
|
238
|
+
end
|
277
239
|
end
|
278
|
-
Firm.find(:first).invoices
|
279
240
|
|
280
|
-
|
281
|
-
the intermediate associated record (the client) and the reciprocal association
|
282
|
-
in the associations cache for each object, which ActiveRecord won't do for you.
|
283
|
-
The reason you would want to do this is that then firm.invoices.first.firm or
|
284
|
-
firm.invoices.first.client doesn't do another query to get the firm/client.
|
241
|
+
Firm.first.invoices
|
285
242
|
|
286
243
|
=== Polymorphic Associations
|
287
244
|
|
288
|
-
|
289
|
-
|
245
|
+
Sequel discourages the use of polymorphic associations, which is the reason they
|
246
|
+
are not supported by default. All polymorphic associations can be made non-polymorphic
|
247
|
+
by using additional tables and/or columns instead of having a column
|
248
|
+
containing the associated class name as a string.
|
290
249
|
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
associations are a good idea.
|
250
|
+
Polymorphic associations break referential integrity and are significantly more
|
251
|
+
complex than non-polymorphic associations, so their use is not recommended unless
|
252
|
+
you are stuck with an existing design that uses them.
|
295
253
|
|
296
|
-
|
297
|
-
associations
|
298
|
-
|
299
|
-
uses them. You should never use them in new code.
|
254
|
+
If you must use them, look for the sequel_polymorphic plugin, as it makes using
|
255
|
+
polymorphic associations in Sequel about as easy as it is in ActiveRecord. However,
|
256
|
+
here's how they can be done using Sequel's custom associations:
|
300
257
|
|
301
258
|
ActiveRecord:
|
302
259
|
|
@@ -464,12 +421,22 @@ design, but sometimes you have to play with the cards you are dealt).
|
|
464
421
|
=== Joining on multiple keys
|
465
422
|
|
466
423
|
Let's say you have two tables that are associated with each other with multiple
|
467
|
-
keys.
|
424
|
+
keys. This can now be handled using Sequel's built in composite key support for
|
425
|
+
associations:
|
468
426
|
|
469
427
|
# Both of these models have an album_id, number, and disc_number fields.
|
470
428
|
# All FavoriteTracks have an associated track, but not all tracks have an
|
471
429
|
# associated favorite track
|
472
430
|
|
431
|
+
class Track < Sequel::Model
|
432
|
+
many_to_one :favorite_track, :key=>[:disc_number, :number, :album_id], :primary_key=>[:disc_number, :number, :album_id]
|
433
|
+
end
|
434
|
+
class FavoriteTrack < Sequel::Model
|
435
|
+
one_to_many :tracks, :key=>[:disc_number, :number, :album_id], :primary_key=>[:disc_number, :number, :album_id], :one_to_one=>true
|
436
|
+
end
|
437
|
+
|
438
|
+
Here's the old way to do it via custom associations:
|
439
|
+
|
473
440
|
class Track < Sequel::Model
|
474
441
|
many_to_one :favorite_track, \
|
475
442
|
:dataset=>(proc do
|
@@ -572,6 +539,37 @@ without knowing the depth of the tree?
|
|
572
539
|
end)
|
573
540
|
end
|
574
541
|
|
542
|
+
Note that unlike ActiveRecord, Sequel supports common table expressions, which allows you to use recursive queries.
|
543
|
+
The results are not the same as in the above case, as all descendents are stored in a single association,
|
544
|
+
but all descendants can be both lazy loaded or eager loaded in a single query (assuming your database
|
545
|
+
supports recursive common table expressions):
|
546
|
+
|
547
|
+
class Node < Sequel::Model
|
548
|
+
one_to_many :descendants, :class=>Node, :dataset=>(proc do
|
549
|
+
Node.from(:t).
|
550
|
+
with_recursive(:t, Node.filter(:parent_id=>pk),
|
551
|
+
Node.join(:t, :id=>:parent_id).
|
552
|
+
select(:nodes.*))
|
553
|
+
end),
|
554
|
+
:eager_loader=>(proc do |key_hash, nodes, associations|
|
555
|
+
id_map = key_hash[:id]
|
556
|
+
nodes.each{|n| n.associations[:descendants] = []}
|
557
|
+
Node.from(:t).
|
558
|
+
with_recursive(:t, Node.filter(:parent_id=>id_map.keys).
|
559
|
+
select(:parent_id___root, :id, :parent_id),
|
560
|
+
Node.join(:t, :id=>:parent_id).
|
561
|
+
select(:t__root, :nodes.*)).
|
562
|
+
all.each do |node|
|
563
|
+
if root = id_map[node.values.delete(:root)].first
|
564
|
+
root.associations[:descendants] << node
|
565
|
+
end
|
566
|
+
end
|
567
|
+
end)
|
568
|
+
end
|
569
|
+
|
570
|
+
You could modify the code to also store direct children relationships at the same time,
|
571
|
+
for functionality similar to the non-common table expression case.
|
572
|
+
|
575
573
|
=== Joining multiple keys to a single key, through a third table
|
576
574
|
|
577
575
|
Let's say you have a database, of songs, lyrics, and artists. Each song
|
@@ -614,7 +612,6 @@ tickets, and each ticket has a number of hours associated with it. You can use
|
|
614
612
|
association support to create a Project association that gives the sum of hours for all
|
615
613
|
associated tickets.
|
616
614
|
|
617
|
-
|
618
615
|
class Project < Sequel::Model
|
619
616
|
one_to_many :tickets
|
620
617
|
many_to_one :ticket_hours, :read_only=>true, :key=>:id,
|
@@ -644,6 +641,6 @@ associated tickets.
|
|
644
641
|
end
|
645
642
|
|
646
643
|
Note that it is often better to use a sum cache instead of this approach. You can implement
|
647
|
-
a sum cache using
|
644
|
+
a sum cache using after_create and after_delete hooks, or using a database trigger
|
648
645
|
(the preferred method if you only have to support one database and that database supports
|
649
646
|
triggers).
|