sequel 3.0.0 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +100 -0
- data/README.rdoc +3 -3
- data/bin/sequel +102 -19
- data/doc/reflection.rdoc +83 -0
- data/doc/release_notes/3.1.0.txt +406 -0
- data/lib/sequel/adapters/ado.rb +11 -0
- data/lib/sequel/adapters/amalgalite.rb +5 -20
- data/lib/sequel/adapters/do.rb +44 -36
- data/lib/sequel/adapters/firebird.rb +29 -43
- data/lib/sequel/adapters/jdbc.rb +17 -27
- data/lib/sequel/adapters/mysql.rb +35 -40
- data/lib/sequel/adapters/odbc.rb +4 -23
- data/lib/sequel/adapters/oracle.rb +22 -19
- data/lib/sequel/adapters/postgres.rb +6 -15
- data/lib/sequel/adapters/shared/mssql.rb +1 -1
- data/lib/sequel/adapters/shared/mysql.rb +29 -10
- data/lib/sequel/adapters/shared/oracle.rb +6 -8
- data/lib/sequel/adapters/shared/postgres.rb +28 -72
- data/lib/sequel/adapters/shared/sqlite.rb +5 -3
- data/lib/sequel/adapters/sqlite.rb +5 -20
- data/lib/sequel/adapters/utils/savepoint_transactions.rb +80 -0
- data/lib/sequel/adapters/utils/unsupported.rb +0 -12
- data/lib/sequel/core.rb +12 -3
- data/lib/sequel/core_sql.rb +1 -8
- data/lib/sequel/database.rb +107 -43
- data/lib/sequel/database/schema_generator.rb +1 -0
- data/lib/sequel/database/schema_methods.rb +38 -4
- data/lib/sequel/dataset.rb +6 -0
- data/lib/sequel/dataset/convenience.rb +2 -2
- data/lib/sequel/dataset/graph.rb +2 -2
- data/lib/sequel/dataset/prepared_statements.rb +3 -8
- data/lib/sequel/dataset/sql.rb +93 -19
- data/lib/sequel/extensions/blank.rb +2 -1
- data/lib/sequel/extensions/inflector.rb +4 -3
- data/lib/sequel/extensions/migration.rb +13 -2
- data/lib/sequel/extensions/pagination.rb +4 -0
- data/lib/sequel/extensions/pretty_table.rb +4 -0
- data/lib/sequel/extensions/query.rb +4 -0
- data/lib/sequel/extensions/schema_dumper.rb +100 -24
- data/lib/sequel/extensions/string_date_time.rb +3 -4
- data/lib/sequel/model.rb +2 -1
- data/lib/sequel/model/associations.rb +96 -38
- data/lib/sequel/model/base.rb +14 -14
- data/lib/sequel/model/plugins.rb +32 -21
- data/lib/sequel/plugins/caching.rb +13 -15
- data/lib/sequel/plugins/identity_map.rb +107 -0
- data/lib/sequel/plugins/lazy_attributes.rb +65 -0
- data/lib/sequel/plugins/many_through_many.rb +188 -0
- data/lib/sequel/plugins/schema.rb +13 -0
- data/lib/sequel/plugins/serialization.rb +53 -37
- data/lib/sequel/plugins/single_table_inheritance.rb +1 -1
- data/lib/sequel/plugins/tactical_eager_loading.rb +61 -0
- data/lib/sequel/plugins/validation_class_methods.rb +28 -7
- data/lib/sequel/plugins/validation_helpers.rb +31 -24
- data/lib/sequel/sql.rb +16 -0
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/ado_spec.rb +47 -1
- data/spec/adapters/firebird_spec.rb +39 -36
- data/spec/adapters/mysql_spec.rb +25 -9
- data/spec/adapters/postgres_spec.rb +11 -24
- data/spec/core/database_spec.rb +54 -13
- data/spec/core/dataset_spec.rb +147 -29
- data/spec/core/object_graph_spec.rb +6 -1
- data/spec/core/schema_spec.rb +34 -0
- data/spec/core/spec_helper.rb +0 -2
- data/spec/extensions/caching_spec.rb +7 -0
- data/spec/extensions/identity_map_spec.rb +158 -0
- data/spec/extensions/lazy_attributes_spec.rb +113 -0
- data/spec/extensions/many_through_many_spec.rb +813 -0
- data/spec/extensions/migration_spec.rb +4 -4
- data/spec/extensions/schema_dumper_spec.rb +114 -13
- data/spec/extensions/schema_spec.rb +19 -3
- data/spec/extensions/serialization_spec.rb +28 -0
- data/spec/extensions/single_table_inheritance_spec.rb +25 -1
- data/spec/extensions/spec_helper.rb +2 -7
- data/spec/extensions/tactical_eager_loading_spec.rb +65 -0
- data/spec/extensions/validation_class_methods_spec.rb +10 -5
- data/spec/integration/dataset_test.rb +39 -6
- data/spec/integration/eager_loader_test.rb +7 -7
- data/spec/integration/spec_helper.rb +0 -1
- data/spec/integration/transaction_test.rb +28 -1
- data/spec/model/association_reflection_spec.rb +29 -3
- data/spec/model/associations_spec.rb +1 -0
- data/spec/model/eager_loading_spec.rb +70 -1
- data/spec/model/plugins_spec.rb +236 -50
- data/spec/model/spec_helper.rb +0 -2
- metadata +18 -5
data/lib/sequel/model/base.rb
CHANGED
@@ -86,12 +86,7 @@ module Sequel
|
|
86
86
|
# the given argument(s).
|
87
87
|
def [](*args)
|
88
88
|
args = args.first if (args.size == 1)
|
89
|
-
|
90
|
-
if t = simple_table and p = simple_pk
|
91
|
-
with_sql("SELECT * FROM #{t} WHERE #{p} = #{dataset.literal(args)}").first
|
92
|
-
else
|
93
|
-
dataset[primary_key_hash(args)]
|
94
|
-
end
|
89
|
+
args.is_a?(Hash) ? dataset[args] : primary_key_lookup(args)
|
95
90
|
end
|
96
91
|
|
97
92
|
# Returns the columns in the result set in their original order.
|
@@ -380,8 +375,8 @@ module Sequel
|
|
380
375
|
im = instance_methods.collect{|x| x.to_s}
|
381
376
|
columns.each do |column|
|
382
377
|
meth = "#{column}="
|
383
|
-
overridable_methods_module.module_eval("def #{column}; self[:#{column}] end") unless im.include?(column.to_s)
|
384
|
-
overridable_methods_module.module_eval("def #{meth}(v); self[:#{column}] = v end") unless im.include?(meth)
|
378
|
+
overridable_methods_module.module_eval("def #{column}; self[:#{column}] end", __FILE__, __LINE__) unless im.include?(column.to_s)
|
379
|
+
overridable_methods_module.module_eval("def #{meth}(v); self[:#{column}] = v end", __FILE__, __LINE__) unless im.include?(meth)
|
385
380
|
end
|
386
381
|
end
|
387
382
|
|
@@ -432,6 +427,16 @@ module Sequel
|
|
432
427
|
include(@overridable_methods_module = Module.new) unless @overridable_methods_module
|
433
428
|
@overridable_methods_module
|
434
429
|
end
|
430
|
+
|
431
|
+
# Find the row in the dataset that matches the primary key. Uses
|
432
|
+
# an static SQL optimization if the table and primary key are simple.
|
433
|
+
def primary_key_lookup(pk)
|
434
|
+
if t = simple_table and p = simple_pk
|
435
|
+
with_sql("SELECT * FROM #{t} WHERE #{p} = #{dataset.literal(pk)}").first
|
436
|
+
else
|
437
|
+
dataset[primary_key_hash(pk)]
|
438
|
+
end
|
439
|
+
end
|
435
440
|
|
436
441
|
# Set the columns for this model and create accessor methods for each column.
|
437
442
|
def set_columns(new_columns)
|
@@ -634,12 +639,7 @@ module Sequel
|
|
634
639
|
# If the model has a composite primary key, returns an array of values.
|
635
640
|
def pk
|
636
641
|
raise(Error, "No primary key is associated with this model") unless key = primary_key
|
637
|
-
|
638
|
-
when Array
|
639
|
-
key.collect{|k| @values[k]}
|
640
|
-
else
|
641
|
-
@values[key]
|
642
|
-
end
|
642
|
+
key.is_a?(Array) ? key.map{|k| @values[k]} : @values[key]
|
643
643
|
end
|
644
644
|
|
645
645
|
# Returns a hash identifying the model instance. It should be true that:
|
data/lib/sequel/model/plugins.rb
CHANGED
@@ -3,14 +3,19 @@ module Sequel
|
|
3
3
|
# so they can be loaded via Model.plugin.
|
4
4
|
#
|
5
5
|
# Plugins should be modules with one of the following conditions:
|
6
|
-
# * A singleton method named apply, which takes a model
|
7
|
-
# additional arguments.
|
6
|
+
# * A singleton method named apply, which takes a model,
|
7
|
+
# additional arguments, and an optional block. This is called
|
8
|
+
# once, the first time the plugin is loaded, with the arguments
|
9
|
+
# and block provide to the call to Model.plugin.
|
8
10
|
# * A module inside the plugin module named InstanceMethods,
|
9
11
|
# which will be included in the model class.
|
10
12
|
# * A module inside the plugin module named ClassMethods,
|
11
13
|
# which will extend the model class.
|
12
14
|
# * A module inside the plugin module named DatasetMethods,
|
13
15
|
# which will extend the model's dataset.
|
16
|
+
# * A singleton method named configure, which takes a model,
|
17
|
+
# additional arguments, and an optional block. This is called
|
18
|
+
# every time the Model.plugin method is called.
|
14
19
|
module Plugins
|
15
20
|
end
|
16
21
|
|
@@ -20,32 +25,38 @@ module Sequel
|
|
20
25
|
# require the plugin from either sequel/plugins/#{plugin} or
|
21
26
|
# sequel_#{plugin}, and then attempt to load the module using a
|
22
27
|
# the camelized plugin name under Sequel::Plugins.
|
23
|
-
def self.plugin(plugin, *args)
|
28
|
+
def self.plugin(plugin, *args, &blk)
|
24
29
|
arg = args.first
|
25
|
-
block = lambda{arg}
|
30
|
+
block = args.length > 1 ? lambda{args} : lambda{arg}
|
26
31
|
m = plugin.is_a?(Module) ? plugin : plugin_module(plugin)
|
27
|
-
|
28
|
-
m
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
32
|
+
unless @plugins.include?(m)
|
33
|
+
@plugins << m
|
34
|
+
m.apply(self, *args, &blk) if m.respond_to?(:apply)
|
35
|
+
if m.const_defined?("InstanceMethods")
|
36
|
+
define_method(:"#{plugin}_opts", &block)
|
37
|
+
include(m::InstanceMethods)
|
38
|
+
end
|
39
|
+
if m.const_defined?("ClassMethods")
|
40
|
+
meta_def(:"#{plugin}_opts", &block)
|
41
|
+
extend(m::ClassMethods)
|
42
|
+
end
|
43
|
+
if m.const_defined?("DatasetMethods")
|
44
|
+
if @dataset
|
45
|
+
dataset.meta_def(:"#{plugin}_opts", &block)
|
46
|
+
dataset.extend(m::DatasetMethods)
|
47
|
+
end
|
48
|
+
dataset_method_modules << m::DatasetMethods
|
49
|
+
meths = m::DatasetMethods.public_instance_methods.reject{|x| NORMAL_METHOD_NAME_REGEXP !~ x.to_s}
|
50
|
+
def_dataset_method(*meths) unless meths.empty?
|
42
51
|
end
|
43
|
-
dataset_method_modules << m::DatasetMethods
|
44
|
-
def_dataset_method(*m::DatasetMethods.public_instance_methods.reject{|x| NORMAL_METHOD_NAME_REGEXP !~ x.to_s})
|
45
52
|
end
|
53
|
+
m.configure(self, *args, &blk) if m.respond_to?(:configure)
|
46
54
|
end
|
47
55
|
|
48
56
|
module ClassMethods
|
57
|
+
# Array of plugins loaded by this class
|
58
|
+
attr_reader :plugins
|
59
|
+
|
49
60
|
private
|
50
61
|
|
51
62
|
# Returns the new style location for the plugin name.
|
@@ -18,7 +18,7 @@ module Sequel
|
|
18
18
|
module Caching
|
19
19
|
# Set the cache_store and cache_ttl attributes for the given model.
|
20
20
|
# If the :ttl option is not given, 3600 seconds is the default.
|
21
|
-
def self.
|
21
|
+
def self.configure(model, store, opts={})
|
22
22
|
model.instance_eval do
|
23
23
|
@cache_store = store
|
24
24
|
@cache_ttl = opts[:ttl] || 3600
|
@@ -32,20 +32,6 @@ module Sequel
|
|
32
32
|
|
33
33
|
# The time to live for the cache store, in seconds.
|
34
34
|
attr_reader :cache_ttl
|
35
|
-
|
36
|
-
# Check the cache before a database lookup unless a hash is supplied.
|
37
|
-
def [](*args)
|
38
|
-
args = args.first if (args.size == 1)
|
39
|
-
return super(args) if args.is_a?(Hash)
|
40
|
-
ck = cache_key(args)
|
41
|
-
if obj = @cache_store.get(ck)
|
42
|
-
return obj
|
43
|
-
end
|
44
|
-
if obj = super(args)
|
45
|
-
@cache_store.set(ck, obj, @cache_ttl)
|
46
|
-
end
|
47
|
-
obj
|
48
|
-
end
|
49
35
|
|
50
36
|
# Set the time to live for the cache store, in seconds (default is 3600, # so 1 hour).
|
51
37
|
def set_cache_ttl(ttl)
|
@@ -75,6 +61,18 @@ module Sequel
|
|
75
61
|
def cache_key(pk)
|
76
62
|
"#{self}:#{Array(pk).join(',')}"
|
77
63
|
end
|
64
|
+
|
65
|
+
# Check the cache before a database lookup unless a hash is supplied.
|
66
|
+
def primary_key_lookup(pk)
|
67
|
+
ck = cache_key(pk)
|
68
|
+
if obj = @cache_store.get(ck)
|
69
|
+
return obj
|
70
|
+
end
|
71
|
+
if obj = super(pk)
|
72
|
+
@cache_store.set(ck, obj, @cache_ttl)
|
73
|
+
end
|
74
|
+
obj
|
75
|
+
end
|
78
76
|
end
|
79
77
|
|
80
78
|
module InstanceMethods
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Sequel
|
2
|
+
module Plugins
|
3
|
+
# The identity_map plugin allows the user to create temporary identity maps
|
4
|
+
# via the with_identity_map method, which takes a block. Inside the block,
|
5
|
+
# objects have a 1-1 correspondence with rows in the database.
|
6
|
+
#
|
7
|
+
# For example, the following is true, and wouldn't be true if you weren't
|
8
|
+
# using the identity map:
|
9
|
+
# Sequel::Model.with_identity_map do
|
10
|
+
# Album.filter{(id > 0) & (id < 2)}.first.object_id == Album.first(:id=>1).object_id
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# In additional to providing a 1-1 correspondence, the identity_map plugin
|
14
|
+
# also provides a cached looked up of records in two cases:
|
15
|
+
# * Model.[] (e.g. Album[1])
|
16
|
+
# * Model.many_to_one accessor methods (e.g. album.artist)
|
17
|
+
#
|
18
|
+
# If the object you are looking up using one of those two methods is already
|
19
|
+
# in the identity map, the record is returned without a database query being
|
20
|
+
# issued.
|
21
|
+
#
|
22
|
+
# Identity maps are thread-local and only presist for the duration of the block,
|
23
|
+
# so they should be should only be considered as a possible performance enhancer.
|
24
|
+
module IdentityMap
|
25
|
+
module ClassMethods
|
26
|
+
# Returns the current thread-local identity map. Should be a hash if
|
27
|
+
# there is an active identity map, and nil otherwise.
|
28
|
+
def identity_map
|
29
|
+
Thread.current[:sequel_identity_map]
|
30
|
+
end
|
31
|
+
|
32
|
+
# The identity map key for an object of the current class with the given pk.
|
33
|
+
# May not always be correct for a class which uses STI.
|
34
|
+
def identity_map_key(pk)
|
35
|
+
"#{self}:#{pk ? Array(pk).join(',') : "nil:#{rand}"}"
|
36
|
+
end
|
37
|
+
|
38
|
+
# If the identity map is in use, check it for a current copy of the object.
|
39
|
+
# If a copy does not exist, create a new object and add it to the identity map.
|
40
|
+
# If a copy exists, add any values in the given row that aren't currently
|
41
|
+
# in the object to the object's values. This allows you to only request
|
42
|
+
# certain fields in an initial query, make modifications to some of those
|
43
|
+
# fields and request other, potentially overlapping fields in a new query,
|
44
|
+
# and not have the second query override fields you modified.
|
45
|
+
def load(row)
|
46
|
+
return super unless idm = identity_map
|
47
|
+
if o = idm[identity_map_key(Array(primary_key).map{|x| row[x]})]
|
48
|
+
o.merge_db_update(row)
|
49
|
+
else
|
50
|
+
o = super
|
51
|
+
idm[identity_map_key(o.pk)] = o
|
52
|
+
end
|
53
|
+
o
|
54
|
+
end
|
55
|
+
|
56
|
+
# Take a block and inside that block use an identity map to ensure a 1-1
|
57
|
+
# correspondence of objects to the database row they represent.
|
58
|
+
def with_identity_map
|
59
|
+
return yield if identity_map
|
60
|
+
begin
|
61
|
+
self.identity_map = {}
|
62
|
+
yield
|
63
|
+
ensure
|
64
|
+
self.identity_map = nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
# Set the thread local identity map to the given value.
|
71
|
+
def identity_map=(v)
|
72
|
+
Thread.current[:sequel_identity_map] = v
|
73
|
+
end
|
74
|
+
|
75
|
+
# Check the current identity map if it exists for the object with
|
76
|
+
# the matching pk. If one is found, return it, otherwise call super.
|
77
|
+
def primary_key_lookup(pk)
|
78
|
+
(idm = identity_map and o = idm[identity_map_key(pk)]) ? o : super
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
module InstanceMethods
|
83
|
+
# Merge the current values into the values provided in the row, ensuring
|
84
|
+
# that current values are not overridden by new values.
|
85
|
+
def merge_db_update(row)
|
86
|
+
@values = row.merge(@values)
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
# If the association is a many_to_one and it has a :key option and the
|
92
|
+
# key option has a value and the association uses the primary key of
|
93
|
+
# the associated class as the :primary_key option, check the identity
|
94
|
+
# map for the associated object and return it if present.
|
95
|
+
def _load_associated_objects(opts)
|
96
|
+
klass = opts.associated_class
|
97
|
+
if idm = model.identity_map and !opts.returns_array? and opts[:key] and pk = send(opts[:key]) and
|
98
|
+
opts[:primary_key] == klass.primary_key and o = idm[klass.identity_map_key(pk)]
|
99
|
+
o
|
100
|
+
else
|
101
|
+
super
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Sequel
|
2
|
+
module Plugins
|
3
|
+
# The lazy_attributes plugin allows users to easily set that some attributes
|
4
|
+
# should not be loaded by default when loading model objects. If the attribute
|
5
|
+
# is needed after the instance has been retrieved, a database query is made to
|
6
|
+
# retreive the value of the attribute.
|
7
|
+
#
|
8
|
+
# This plugin depends on the identity_map and tactical_eager_loading plugin, and allows you to
|
9
|
+
# eagerly load lazy attributes for all objects retrieved with the current object.
|
10
|
+
# So the following code should issue one query to get the albums and one query to
|
11
|
+
# get the reviews for all of those albums:
|
12
|
+
#
|
13
|
+
# Album.plugin :lazy_attributes, :review
|
14
|
+
# Sequel::Model.with_identity_map do
|
15
|
+
# Album.filter{id<100}.all do |a|
|
16
|
+
# a.review
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
module LazyAttributes
|
20
|
+
# Tactical eager loading requires the tactical_eager_loading plugin
|
21
|
+
def self.apply(model, *attrs)
|
22
|
+
model.plugin :identity_map
|
23
|
+
model.plugin :tactical_eager_loading
|
24
|
+
end
|
25
|
+
|
26
|
+
# Set the attributes given as lazy attributes
|
27
|
+
def self.configure(model, *attrs)
|
28
|
+
model.lazy_attributes(*attrs) unless attrs.empty?
|
29
|
+
end
|
30
|
+
|
31
|
+
module ClassMethods
|
32
|
+
# Remove the given attributes from the list of columns selected by default.
|
33
|
+
# For each attribute given, create an accessor method that allows a lazy
|
34
|
+
# lookup of the attribute. Each attribute should be given as a symbol.
|
35
|
+
def lazy_attributes(*attrs)
|
36
|
+
set_dataset(dataset.select(*(columns - attrs)))
|
37
|
+
attrs.each do |a|
|
38
|
+
define_method(a) do
|
39
|
+
if !values.include?(a) && !new?
|
40
|
+
lazy_attribute_lookup(a)
|
41
|
+
else
|
42
|
+
super()
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
module InstanceMethods
|
50
|
+
private
|
51
|
+
|
52
|
+
# If the model was selected with other model objects, eagerly load the
|
53
|
+
# attribute for all of those objects. If not, query the database for
|
54
|
+
# the attribute for just the current object. Return the value of
|
55
|
+
# the attribute for the current object.
|
56
|
+
def lazy_attribute_lookup(a)
|
57
|
+
primary_key = model.primary_key
|
58
|
+
model.select(*(Array(primary_key) + [a])).filter(primary_key=>retrieved_with.map{|o| o.pk}.sql_array).all if model.identity_map && retrieved_with
|
59
|
+
values[a] = this.select(a).first[a] unless values.include?(a)
|
60
|
+
values[a]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
module Sequel
|
2
|
+
module Plugins
|
3
|
+
# The many_through_many plugin allow you to create a association to multiple objects using multiple join tables.
|
4
|
+
# For example, assume the following associations:
|
5
|
+
#
|
6
|
+
# Artist.many_to_many :albums
|
7
|
+
# Album.many_to_many :tags
|
8
|
+
#
|
9
|
+
# The many_through_many plugin would allow this:
|
10
|
+
#
|
11
|
+
# Artist.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]]
|
12
|
+
#
|
13
|
+
# Which will give you the tags for all of the artist's albums.
|
14
|
+
#
|
15
|
+
# Here are some more examples:
|
16
|
+
#
|
17
|
+
# # Same as Artist.many_to_many :albums
|
18
|
+
# Artist.many_through_many :albums, [[:albums_artists, :artist_id, :album_id]]
|
19
|
+
#
|
20
|
+
# # All artists that are associated to any album that this artist is associated to
|
21
|
+
# Artist.many_through_many :artists, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_artists, :album_id, :artist_id]]
|
22
|
+
#
|
23
|
+
# # All albums by artists that are associated to any album that this artist is associated to
|
24
|
+
# Artist.many_through_many :artist_albums, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], \
|
25
|
+
# [:albums_artists, :album_id, :artist_id], [:artists, :id, :id], [:albums_artists, :artist_id, :album_id]], \
|
26
|
+
# :class=>:Album
|
27
|
+
#
|
28
|
+
# # All tracks on albums by this artist
|
29
|
+
# Artist.many_through_many :tracks, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id]], \
|
30
|
+
# :right_primary_key=>:album_id
|
31
|
+
module ManyThroughMany
|
32
|
+
# The AssociationReflection subclass for many_through_many associations.
|
33
|
+
class ManyThroughManyAssociationReflection < Sequel::Model::Associations::ManyToManyAssociationReflection
|
34
|
+
Sequel::Model::Associations::ASSOCIATION_TYPES[:many_through_many] = self
|
35
|
+
|
36
|
+
# The table containing the column to use for the associated key when eagerly loading
|
37
|
+
def associated_key_table
|
38
|
+
self[:associated_key_table] = self[:final_reverse_edge][:alias]
|
39
|
+
end
|
40
|
+
|
41
|
+
# The list of joins to use when eager graphing
|
42
|
+
def edges
|
43
|
+
self[:edges] || calculate_edges || self[:edges]
|
44
|
+
end
|
45
|
+
|
46
|
+
# Many through many associations don't have a reciprocal
|
47
|
+
def reciprocal
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
|
51
|
+
# The list of joins to use when lazy loading or eager loading
|
52
|
+
def reverse_edges
|
53
|
+
self[:reverse_edges] || calculate_edges || self[:reverse_edges]
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# Make sure to use unique table aliases when lazy loading or eager loading
|
59
|
+
def calculate_reverse_edge_aliases(reverse_edges)
|
60
|
+
aliases = [associated_class.table_name]
|
61
|
+
reverse_edges.each do |e|
|
62
|
+
table_alias = e[:table]
|
63
|
+
if aliases.include?(table_alias)
|
64
|
+
i = 0
|
65
|
+
table_alias = loop do
|
66
|
+
ta = :"#{table_alias}_#{i}"
|
67
|
+
break ta unless aliases.include?(ta)
|
68
|
+
i += 1
|
69
|
+
end
|
70
|
+
end
|
71
|
+
aliases.push(e[:alias] = table_alias)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Transform the :through option into a list of edges and reverse edges to use to join tables when loading the association.
|
76
|
+
def calculate_edges
|
77
|
+
es = [{:left_table=>self[:model].table_name, :left_key=>self[:left_primary_key]}]
|
78
|
+
self[:through].each do |t|
|
79
|
+
es.last.merge!(:right_key=>t[:left], :right_table=>t[:table], :join_type=>t[:join_type]||self[:graph_join_type], :conditions=>(t[:conditions]||[]).to_a, :block=>t[:block])
|
80
|
+
es.last[:only_conditions] = t[:only_conditions] if t.include?(:only_conditions)
|
81
|
+
es << {:left_table=>t[:table], :left_key=>t[:right]}
|
82
|
+
end
|
83
|
+
es.last.merge!(:right_key=>right_primary_key, :right_table=>associated_class.table_name)
|
84
|
+
edges = es.map do |e|
|
85
|
+
h = {:table=>e[:right_table], :left=>e[:left_key], :right=>e[:right_key], :conditions=>e[:conditions], :join_type=>e[:join_type], :block=>e[:block]}
|
86
|
+
h[:only_conditions] = e[:only_conditions] if e.include?(:only_conditions)
|
87
|
+
h
|
88
|
+
end
|
89
|
+
reverse_edges = es.reverse.map{|e| {:table=>e[:left_table], :left=>e[:left_key], :right=>e[:right_key]}}
|
90
|
+
reverse_edges.pop
|
91
|
+
calculate_reverse_edge_aliases(reverse_edges)
|
92
|
+
self[:final_edge] = edges.pop
|
93
|
+
self[:final_reverse_edge] = reverse_edges.pop
|
94
|
+
self[:edges] = edges
|
95
|
+
self[:reverse_edges] = reverse_edges
|
96
|
+
nil
|
97
|
+
end
|
98
|
+
end
|
99
|
+
module ClassMethods
|
100
|
+
# Create a many_through_many association. Arguments:
|
101
|
+
# * name - Same as associate, the name of the association.
|
102
|
+
# * through - The tables and keys to join between the current table and the associated table.
|
103
|
+
# Must be an array, with elements that are either 3 element arrays, or hashes with keys :table, :left, and :right.
|
104
|
+
# The required entries in the array/hash are:
|
105
|
+
# * :table (first array element) - The name of the table to join.
|
106
|
+
# * :left (middle array element) - The key joining the table to the previous table
|
107
|
+
# * :right (last array element) - The key joining the table to the next table
|
108
|
+
# If a hash is provided, the following keys are respected when using eager_graph:
|
109
|
+
# * :block - A proc to use as the block argument to join.
|
110
|
+
# * :conditions - Extra conditions to add to the JOIN ON clause. Must be a hash or array of two pairs.
|
111
|
+
# * :join_type - The join type to use for the join, defaults to :left_outer.
|
112
|
+
# * :only_conditions - Conditions to use for the join instead of the ones specified by the keys.
|
113
|
+
# * opts - The options for the associaion. Takes the same options as associate, and supports these additional options:
|
114
|
+
# * :left_primary_key - column in current table that the first :left option in through points to, as a symbol. Defaults to primary key of current table.
|
115
|
+
# * :right_primary_key - column in associated table that the final :right option in through points to, as a symbol. Defaults to primary key of the associated table.
|
116
|
+
# * :uniq - Adds a after_load callback that makes the array of objects unique.
|
117
|
+
def many_through_many(name, through, opts={}, &block)
|
118
|
+
associate(:many_through_many, name, opts.merge(:through=>through), &block)
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
# Create the association methods and :eager_loader and :eager_grapher procs.
|
124
|
+
def def_many_through_many(opts)
|
125
|
+
name = opts[:name]
|
126
|
+
model = self
|
127
|
+
opts[:read_only] = true
|
128
|
+
opts[:class_name] ||= camelize(singularize(name))
|
129
|
+
opts[:after_load].unshift(:array_uniq!) if opts[:uniq]
|
130
|
+
opts[:cartesian_product_number] ||= 2
|
131
|
+
opts[:through] = opts[:through].map do |e|
|
132
|
+
case e
|
133
|
+
when Array
|
134
|
+
raise(Error, "array elements of the through option/argument for many_through_many associations must have at least three elements") unless e.length == 3
|
135
|
+
{:table=>e[0], :left=>e[1], :right=>e[2]}
|
136
|
+
when Hash
|
137
|
+
raise(Error, "hash elements of the through option/argument for many_through_many associations must contain :table, :left, and :right keys") unless e[:table] && e[:left] && e[:right]
|
138
|
+
e
|
139
|
+
else
|
140
|
+
raise(Error, "the through option/argument for many_through_many associations must be an enumerable of arrays or hashes")
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
left_key = opts[:left_key] = opts[:through].first[:left]
|
145
|
+
left_pk = (opts[:left_primary_key] ||= self.primary_key)
|
146
|
+
opts[:dataset] ||= lambda do
|
147
|
+
ds = opts.associated_class
|
148
|
+
opts.reverse_edges.each{|t| ds = ds.join(t[:table], [[t[:left], t[:right]]], :table_alias=>t[:alias])}
|
149
|
+
ft = opts[:final_reverse_edge]
|
150
|
+
ds.join(ft[:table], [[ft[:left], ft[:right]], [left_key, send(left_pk)]], :table_alias=>ft[:alias])
|
151
|
+
end
|
152
|
+
|
153
|
+
left_key_alias = opts[:left_key_alias] ||= opts.default_associated_key_alias
|
154
|
+
opts[:eager_loader] ||= lambda do |key_hash, records, associations|
|
155
|
+
h = key_hash[left_pk]
|
156
|
+
records.each{|object| object.associations[name] = []}
|
157
|
+
ds = opts.associated_class
|
158
|
+
opts.reverse_edges.each{|t| ds = ds.join(t[:table], [[t[:left], t[:right]]], :table_alias=>t[:alias])}
|
159
|
+
ft = opts[:final_reverse_edge]
|
160
|
+
ds = ds.join(ft[:table], [[ft[:left], ft[:right]], [left_key, h.keys]], :table_alias=>ft[:alias])
|
161
|
+
model.eager_loading_dataset(opts, ds, Array(opts.select), associations).all do |assoc_record|
|
162
|
+
next unless objects = h[assoc_record.values.delete(left_key_alias)]
|
163
|
+
objects.each{|object| object.associations[name].push(assoc_record)}
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
join_type = opts[:graph_join_type]
|
168
|
+
select = opts[:graph_select]
|
169
|
+
graph_block = opts[:graph_block]
|
170
|
+
only_conditions = opts[:graph_only_conditions]
|
171
|
+
use_only_conditions = opts.include?(:graph_only_conditions)
|
172
|
+
conditions = opts[:graph_conditions]
|
173
|
+
opts[:eager_grapher] ||= proc do |ds, assoc_alias, table_alias|
|
174
|
+
iq = table_alias
|
175
|
+
opts.edges.each do |t|
|
176
|
+
ds = ds.graph(t[:table], t.include?(:only_conditions) ? t[:only_conditions] : ([[t[:right], t[:left]]] + t[:conditions]), :select=>false, :table_alias=>ds.send(:eager_unique_table_alias, ds, t[:table]), :join_type=>t[:join_type], :implicit_qualifier=>iq, &t[:block])
|
177
|
+
iq = nil
|
178
|
+
end
|
179
|
+
fe = opts[:final_edge]
|
180
|
+
ds.graph(opts.associated_class, use_only_conditions ? only_conditions : ([[opts.right_primary_key, fe[:left]]] + conditions), :select=>select, :table_alias=>assoc_alias, :join_type=>join_type, &graph_block)
|
181
|
+
end
|
182
|
+
|
183
|
+
def_association_dataset_methods(opts)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|