sequel 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +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
|