sequel 1.4.0 → 1.5.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 +56 -0
- data/README +257 -154
- data/Rakefile +8 -16
- data/lib/sequel_model.rb +15 -257
- data/lib/sequel_model/associations.rb +70 -33
- data/lib/sequel_model/base.rb +80 -35
- data/lib/sequel_model/caching.rb +3 -3
- data/lib/sequel_model/deprecated.rb +81 -0
- data/lib/sequel_model/eager_loading.rb +303 -43
- data/lib/sequel_model/hooks.rb +29 -25
- data/lib/sequel_model/inflections.rb +112 -0
- data/lib/sequel_model/inflector.rb +279 -0
- data/lib/sequel_model/plugins.rb +15 -14
- data/lib/sequel_model/record.rb +87 -75
- data/lib/sequel_model/schema.rb +2 -0
- data/lib/sequel_model/validations.rb +300 -2
- data/spec/associations_spec.rb +175 -9
- data/spec/base_spec.rb +37 -18
- data/spec/caching_spec.rb +7 -4
- data/spec/deprecated_relations_spec.rb +3 -43
- data/spec/eager_loading_spec.rb +295 -7
- data/spec/hooks_spec.rb +7 -4
- data/spec/inflector_spec.rb +34 -0
- data/spec/model_spec.rb +30 -53
- data/spec/record_spec.rb +191 -33
- data/spec/spec_helper.rb +17 -2
- data/spec/validations_spec.rb +414 -15
- metadata +7 -22
- data/lib/sequel_model/pretty_table.rb +0 -73
data/lib/sequel_model/base.rb
CHANGED
@@ -1,9 +1,26 @@
|
|
1
1
|
module Sequel
|
2
2
|
class Model
|
3
|
+
# If possible, set the dataset for the model subclass as soon as it
|
4
|
+
# is created.
|
5
|
+
def self.inherited(subclass)
|
6
|
+
begin
|
7
|
+
if subclass.superclass == Model
|
8
|
+
unless subclass.name.empty?
|
9
|
+
subclass.set_dataset(Model.db[subclass.implicit_table_name])
|
10
|
+
end
|
11
|
+
elsif ds = subclass.superclass.instance_variable_get(:@dataset)
|
12
|
+
subclass.set_dataset(ds.clone)
|
13
|
+
end
|
14
|
+
rescue StandardError
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
3
18
|
# Returns the database associated with the Model class.
|
4
19
|
def self.db
|
5
|
-
@db
|
6
|
-
|
20
|
+
return @db if @db
|
21
|
+
@db = self == Model ? ::Sequel::DATABASES.first : superclass.db
|
22
|
+
raise(Error, "No database associated with #{self}") unless @db
|
23
|
+
@db
|
7
24
|
end
|
8
25
|
|
9
26
|
# Sets the database associated with the Model class.
|
@@ -14,12 +31,6 @@ module Sequel
|
|
14
31
|
end
|
15
32
|
end
|
16
33
|
|
17
|
-
# Called when a database is opened in order to automatically associate the
|
18
|
-
# first opened database with model classes.
|
19
|
-
def self.database_opened(db)
|
20
|
-
@db = db if (self == Model) && !@db
|
21
|
-
end
|
22
|
-
|
23
34
|
# Returns the implicit table name for the model class.
|
24
35
|
def self.implicit_table_name
|
25
36
|
name.demodulize.underscore.pluralize.to_sym
|
@@ -27,34 +38,39 @@ module Sequel
|
|
27
38
|
|
28
39
|
# Returns the dataset associated with the Model class.
|
29
40
|
def self.dataset
|
30
|
-
|
31
|
-
if ds = super_dataset
|
32
|
-
set_dataset(ds.clone)
|
33
|
-
elsif !name.empty?
|
34
|
-
set_dataset(db[implicit_table_name])
|
35
|
-
else
|
36
|
-
raise Error, "No dataset associated with #{self}"
|
37
|
-
end
|
38
|
-
end
|
39
|
-
@dataset
|
41
|
+
@dataset || raise(Error, "No dataset associated with #{self}")
|
40
42
|
end
|
41
|
-
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
47
|
-
|
48
|
-
def self.
|
49
|
-
|
43
|
+
|
44
|
+
# If a block is given, define a method on the dataset with the given argument name using
|
45
|
+
# the given block as well as a method on the model that calls the
|
46
|
+
# dataset method.
|
47
|
+
#
|
48
|
+
# If a block is not given, define a method on the model for each argument
|
49
|
+
# that calls the dataset method of the same argument name.
|
50
|
+
def self.def_dataset_method(*args, &block)
|
51
|
+
raise(Error, "No arguments given") if args.empty?
|
52
|
+
if block_given?
|
53
|
+
raise(Error, "Defining a dataset method using a block requires only one argument") if args.length > 1
|
54
|
+
dataset.meta_def(args.first, &block)
|
55
|
+
end
|
56
|
+
args.each{|arg| instance_eval("def #{arg}(*args, &block); dataset.#{arg}(*args, &block) end", __FILE__, __LINE__)}
|
50
57
|
end
|
51
58
|
|
52
59
|
# Returns the columns in the result set in their original order.
|
53
60
|
#
|
54
61
|
# See Dataset#columns for more information.
|
55
62
|
def self.columns
|
56
|
-
@columns
|
63
|
+
return @columns if @columns
|
64
|
+
@columns = dataset.naked.columns or
|
57
65
|
raise Error, "Could not fetch columns for #{self}"
|
66
|
+
def_column_accessor(*@columns)
|
67
|
+
@str_columns = nil
|
68
|
+
@columns
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns the columns as a list of frozen strings.
|
72
|
+
def self.str_columns
|
73
|
+
@str_columns ||= columns.map{|c| c.to_s.freeze}
|
58
74
|
end
|
59
75
|
|
60
76
|
# Sets the dataset associated with the Model class.
|
@@ -64,6 +80,32 @@ module Sequel
|
|
64
80
|
@dataset.set_model(self)
|
65
81
|
@dataset.extend(Associations::EagerLoading)
|
66
82
|
@dataset.transform(@transform) if @transform
|
83
|
+
begin
|
84
|
+
@columns = nil
|
85
|
+
columns
|
86
|
+
rescue StandardError
|
87
|
+
end
|
88
|
+
end
|
89
|
+
class << self; alias :dataset= :set_dataset; end
|
90
|
+
|
91
|
+
class << self
|
92
|
+
private
|
93
|
+
def def_column_accessor(*columns)
|
94
|
+
Thread.exclusive do
|
95
|
+
columns.each do |column|
|
96
|
+
im = instance_methods
|
97
|
+
meth = "#{column}="
|
98
|
+
define_method(column){self[column]} unless im.include?(column.to_s)
|
99
|
+
unless im.include?(meth)
|
100
|
+
define_method(meth) do |*v|
|
101
|
+
len = v.length
|
102
|
+
raise(ArgumentError, "wrong number of arguments (#{len} for 1)") unless len == 1
|
103
|
+
self[column] = v.first
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
67
109
|
end
|
68
110
|
|
69
111
|
# Returns the database assoiated with the object's Model class.
|
@@ -83,6 +125,11 @@ module Sequel
|
|
83
125
|
model.columns
|
84
126
|
end
|
85
127
|
|
128
|
+
# Returns the str_columns associated with the object's Model class.
|
129
|
+
def str_columns
|
130
|
+
model.str_columns
|
131
|
+
end
|
132
|
+
|
86
133
|
# Serializes column with YAML or through marshalling.
|
87
134
|
def self.serialize(*columns)
|
88
135
|
format = columns.pop[:format] if Hash === columns.last
|
@@ -108,13 +155,11 @@ module Sequel
|
|
108
155
|
# # ...
|
109
156
|
#
|
110
157
|
# end
|
158
|
+
@models = {}
|
111
159
|
def self.Model(source)
|
112
|
-
@models
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
end
|
117
|
-
end
|
160
|
+
return @models[source] if @models[source]
|
161
|
+
klass = Class.new(Sequel::Model)
|
162
|
+
klass.set_dataset(source.is_a?(Dataset) ? source : Model.db[source])
|
163
|
+
@models[source] = klass
|
118
164
|
end
|
119
|
-
|
120
165
|
end
|
data/lib/sequel_model/caching.rb
CHANGED
@@ -18,8 +18,8 @@ module Sequel
|
|
18
18
|
obj
|
19
19
|
end
|
20
20
|
|
21
|
-
class_def(:
|
22
|
-
class_def(:save) {store.delete(cache_key)
|
21
|
+
class_def(:update_values) {|v| store.delete(cache_key); super}
|
22
|
+
class_def(:save) {store.delete(cache_key) unless new?; super}
|
23
23
|
class_def(:delete) {store.delete(cache_key); super}
|
24
24
|
end
|
25
25
|
|
@@ -39,4 +39,4 @@ module Sequel
|
|
39
39
|
"#{self}:#{values.join(',')}"
|
40
40
|
end
|
41
41
|
end
|
42
|
-
end
|
42
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Model
|
3
|
+
include Sequel::Deprecation
|
4
|
+
extend Sequel::Deprecation
|
5
|
+
|
6
|
+
# Check the Model.associate method to remove the :from option
|
7
|
+
|
8
|
+
def self.is_dataset_magic_method?(m) #:nodoc:
|
9
|
+
method_name = m.to_s
|
10
|
+
Sequel::Dataset::MAGIC_METHODS.each_key do |r|
|
11
|
+
return true if method_name =~ r
|
12
|
+
end
|
13
|
+
false
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.method_missing(m, *args, &block) #:nodoc:
|
17
|
+
Thread.exclusive do
|
18
|
+
if dataset.respond_to?(m) || is_dataset_magic_method?(m)
|
19
|
+
instance_eval("def #{m}(*args, &block); deprecate('Sequel::Model.method_missing', 'Please define Sequel::Model.#{m} or use def_dataset_method :#{m}'); dataset.#{m}(*args, &block); end")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
respond_to?(m) ? send(m, *args, &block) : super(m, *args)
|
23
|
+
end
|
24
|
+
|
25
|
+
def method_missing(m, *args, &block) #:nodoc:
|
26
|
+
if m.to_s =~ /=\z/
|
27
|
+
attribute = m.to_s.chop
|
28
|
+
values.keys.each do |k|
|
29
|
+
next unless k.to_s == attribute
|
30
|
+
deprecate("Sequel::Model#method_missing", "Use model[:#{attribute}] = ...")
|
31
|
+
return self[attribute.to_sym] = args.first
|
32
|
+
end
|
33
|
+
super
|
34
|
+
else
|
35
|
+
attribute = m.to_s
|
36
|
+
values.keys.each do |k|
|
37
|
+
next unless k.to_s == attribute
|
38
|
+
deprecate("Sequel::Model#method_missing", "Use model[:#{attribute}]")
|
39
|
+
return self[attribute.to_sym]
|
40
|
+
end
|
41
|
+
super
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.create_with_params(params) #:nodoc:
|
46
|
+
deprecate("Sequel::Model.create_with_params", "Use .create")
|
47
|
+
create(params)
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.create_with(params) #:nodoc:
|
51
|
+
deprecate("Sequel::Model.create_with", "Use .create")
|
52
|
+
create(params)
|
53
|
+
end
|
54
|
+
|
55
|
+
def update_with(params) #:nodoc:
|
56
|
+
deprecate("Sequel::Model#update_with", "Use #update_with_params")
|
57
|
+
update_with_params(params)
|
58
|
+
end
|
59
|
+
|
60
|
+
def new_record? #:nodoc:
|
61
|
+
deprecate("Sequel::Model#new_record?", "Use #new?")
|
62
|
+
new?
|
63
|
+
end
|
64
|
+
|
65
|
+
def set(values) #:nodoc:
|
66
|
+
deprecate("Sequel::Model#set", "Use #update_values")
|
67
|
+
update_values(values)
|
68
|
+
end
|
69
|
+
|
70
|
+
def update(values) #:nodoc:
|
71
|
+
deprecate("Sequel::Model#update", "Use #update_values")
|
72
|
+
update_values(values)
|
73
|
+
end
|
74
|
+
|
75
|
+
# deprecated, please use many_to_one instead
|
76
|
+
def self.one_to_one(*args, &block) #:nodoc:
|
77
|
+
deprecate("Sequel::Model.one_to_one", "Use many_to_one")
|
78
|
+
many_to_one(*args, &block)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -1,68 +1,321 @@
|
|
1
1
|
# Eager loading makes it so that you can load all associated records for a
|
2
2
|
# set of objects in a single query, instead of a separate query for each object.
|
3
3
|
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
# This implementation avoids the complexity of extracting an object graph out
|
12
|
-
# of a single dataset, by building the object graph out of multiple datasets,
|
13
|
-
# one for each association. By using a separate dataset for each association,
|
14
|
-
# it avoids problems such as aliasing conflicts and creating cartesian product
|
15
|
-
# result sets if multiple *_to_many eager associations are requested.
|
16
|
-
#
|
17
|
-
# One limitation of using this method is that you cannot filter the dataset
|
18
|
-
# based on values of columns in an associated table, since the associations are loaded
|
19
|
-
# in separate queries. To do that you need to load all associations in the
|
20
|
-
# same query, and extract an object graph from the results of that query.
|
4
|
+
# Two separate implementations are provided. .eager should be used most of the
|
5
|
+
# time, as it loads associated records using one query per association. However,
|
6
|
+
# it does not allow you the ability to filter based on columns in associated tables. .eager_graph loads
|
7
|
+
# all records in one query. Using .eager_graph you can filter based on columns in associated
|
8
|
+
# tables. However, .eager_graph can be much slower than .eager, especially if multiple
|
9
|
+
# *_to_many associations are joined.
|
21
10
|
#
|
22
11
|
# You can cascade the eager loading (loading associations' associations)
|
23
|
-
# with no limit to the depth of the cascades. You do this by passing a hash to .eager
|
12
|
+
# with no limit to the depth of the cascades. You do this by passing a hash to .eager or .eager_graph
|
24
13
|
# with the keys being associations of the current model and values being
|
25
14
|
# associations of the model associated with the current model via the key.
|
15
|
+
#
|
16
|
+
# You cannot eagerly load an association with a block argument, as the block argument is
|
17
|
+
# evaluated in terms of a specific instance of the model, and no specific instance exists.
|
26
18
|
#
|
27
|
-
# The
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
19
|
+
# The arguments can be symbols or hashes with symbol keys (for cascaded
|
20
|
+
# eager loading). Examples:
|
21
|
+
#
|
22
|
+
# Album.eager(:artist).all
|
23
|
+
# Album.eager_graph(:artist).all
|
24
|
+
# Album.eager(:artist, :genre).all
|
25
|
+
# Album.eager_graph(:artist, :genre).all
|
26
|
+
# Album.eager(:artist).eager(:genre).all
|
27
|
+
# Album.eager_graph(:artist).eager(:genre).all
|
28
|
+
# Artist.eager(:albums=>:tracks).all
|
29
|
+
# Artist.eager_graph(:albums=>:tracks).all
|
30
|
+
# Artist.eager(:albums=>{:tracks=>:genre}).all
|
31
|
+
# Artist.eager_graph(:albums=>{:tracks=>:genre}).all
|
31
32
|
module Sequel::Model::Associations::EagerLoading
|
32
|
-
# Add
|
33
|
-
|
34
|
-
|
33
|
+
# Add the .eager! and .eager_graph! mutation methods to the dataset.
|
34
|
+
def self.extended(obj)
|
35
|
+
obj.def_mutation_method(:eager, :eager_graph)
|
36
|
+
end
|
37
|
+
|
38
|
+
# The preferred eager loading method. Loads all associated records using one
|
39
|
+
# query for each association.
|
40
|
+
#
|
41
|
+
# The basic idea for how it works is that the dataset is first loaded normally.
|
42
|
+
# Then it goes through all associations that have been specified via .eager.
|
43
|
+
# It loads each of those associations separately, then associates them back
|
44
|
+
# to the original dataset via primary/foreign keys. Due to the necessity of
|
45
|
+
# all objects being present, you need to use .all to use eager loading, as it
|
46
|
+
# can't work with .each.
|
47
|
+
#
|
48
|
+
# This implementation avoids the complexity of extracting an object graph out
|
49
|
+
# of a single dataset, by building the object graph out of multiple datasets,
|
50
|
+
# one for each association. By using a separate dataset for each association,
|
51
|
+
# it avoids problems such as aliasing conflicts and creating cartesian product
|
52
|
+
# result sets if multiple *_to_many eager associations are requested.
|
53
|
+
#
|
54
|
+
# One limitation of using this method is that you cannot filter the dataset
|
55
|
+
# based on values of columns in an associated table, since the associations are loaded
|
56
|
+
# in separate queries. To do that you need to load all associations in the
|
57
|
+
# same query, and extract an object graph from the results of that query. If you
|
58
|
+
# need to filter based on columns in associated tables, look at .eager_graph
|
59
|
+
# or join the tables you need to filter on manually.
|
35
60
|
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
# Album.eager(:artist).eager(:genre).all
|
39
|
-
# Artist.eager(:albums=>:tracks).all
|
40
|
-
# Artist.eager(:albums=>{:tracks=>:genre}).all
|
61
|
+
# Each association's order, if definied, is respected. Eager also works
|
62
|
+
# on a limited dataset.
|
41
63
|
def eager(*associations)
|
42
|
-
|
64
|
+
model = check_model
|
43
65
|
opt = @opts[:eager]
|
44
66
|
opt = opt ? opt.dup : {}
|
45
|
-
check = Proc.new do |a|
|
46
|
-
raise(ArgumentError, 'Invalid association') unless reflection = model.association_reflection(a)
|
47
|
-
raise(ArgumentError, 'Cannot eagerly load associations with block arguments') if reflection[:block]
|
48
|
-
end
|
49
67
|
associations.flatten.each do |association|
|
50
68
|
case association
|
51
69
|
when Symbol
|
52
|
-
|
70
|
+
check_association(model, association)
|
53
71
|
opt[association] = nil
|
54
72
|
when Hash
|
55
|
-
association.keys.each{|assoc|
|
73
|
+
association.keys.each{|assoc| check_association(model, assoc)}
|
56
74
|
opt.merge!(association)
|
57
75
|
else raise(ArgumentError, 'Associations must be in the form of a symbol or hash')
|
58
76
|
end
|
59
77
|
end
|
60
|
-
|
61
|
-
|
62
|
-
|
78
|
+
clone(:eager=>opt)
|
79
|
+
end
|
80
|
+
|
81
|
+
# The secondary eager loading method. Loads all associations in a single query. This
|
82
|
+
# method should only be used if you need to filter based on columns in associated tables.
|
83
|
+
#
|
84
|
+
# This method builds an object graph using the .graph method. Then it uses the graph
|
85
|
+
# to build the associations, and finally replaces the graph with a simple array
|
86
|
+
# of model objects.
|
87
|
+
#
|
88
|
+
# Be very careful when using this with multiple *_to_many associations, as you can
|
89
|
+
# create large cartesian products. If you must graph multiple *_to_many associations,
|
90
|
+
# make sure your filters are specific if you have a large database.
|
91
|
+
#
|
92
|
+
# This does not respect each association's order, as all associations are loaded in
|
93
|
+
# a single query. If you want to order the results, you must manually call .order.
|
94
|
+
#
|
95
|
+
# eager_graph probably won't work the way you suspect with limit, unless you are
|
96
|
+
# only graphing many_to_one associations.
|
97
|
+
def eager_graph(*associations)
|
98
|
+
model = check_model
|
99
|
+
table_name = model.table_name
|
100
|
+
ds = if @opts[:eager_graph]
|
101
|
+
self
|
102
|
+
else
|
103
|
+
# Each of the following have a symbol key for the table alias, with the following values:
|
104
|
+
# :requirements - array of requirements for this association
|
105
|
+
# :alias_association_type_map - the type of association for this association
|
106
|
+
# :alias_association_name_map - the name of the association for this association
|
107
|
+
clone(:eager_graph=>{:requirements=>{}, :master=>model.table_name, :alias_association_type_map=>{}, :alias_association_name_map=>{}, :reciprocals=>{}})
|
108
|
+
end
|
109
|
+
ds.eager_graph_associations(ds, model, table_name, [], *associations)
|
63
110
|
end
|
64
111
|
|
112
|
+
protected
|
113
|
+
# Call graph on the association with the correct arguments,
|
114
|
+
# update the eager_graph data structure, and recurse into
|
115
|
+
# eager_graph_associations if there are any passed in associations
|
116
|
+
# (which would be dependencies of the current association)
|
117
|
+
#
|
118
|
+
# Arguments:
|
119
|
+
# * ds - Current dataset
|
120
|
+
# * model - Current Model
|
121
|
+
# * ta - table_alias used for the parent association
|
122
|
+
# * requirements - an array, used as a stack for requirements
|
123
|
+
# * r - association reflection for the current association
|
124
|
+
# * *associations - any associations dependent on this one
|
125
|
+
def eager_graph_association(ds, model, ta, requirements, r, *associations)
|
126
|
+
klass = model.send(:associated_class, r)
|
127
|
+
assoc_name = r[:name]
|
128
|
+
assoc_table_alias = ds.eager_unique_table_alias(ds, assoc_name)
|
129
|
+
ds = case assoc_type = r[:type]
|
130
|
+
when :many_to_one
|
131
|
+
ds.graph(klass, {klass.primary_key=>:"#{ta}__#{r[:key]}"}, :table_alias=>assoc_table_alias)
|
132
|
+
when :one_to_many
|
133
|
+
ds = ds.graph(klass, {r[:key]=>:"#{ta}__#{model.primary_key}"}, :table_alias=>assoc_table_alias)
|
134
|
+
# We only load reciprocals for one_to_many associations, as other reciprocals don't make sense
|
135
|
+
ds.opts[:eager_graph][:reciprocals][assoc_table_alias] = model.send(:reciprocal_association, r)
|
136
|
+
ds
|
137
|
+
when :many_to_many
|
138
|
+
ds = ds.graph(r[:join_table], {r[:left_key]=>:"#{ta}__#{model.primary_key}"}, :select=>false, :table_alias=>ds.eager_unique_table_alias(ds, r[:join_table]))
|
139
|
+
ds.graph(klass, {klass.primary_key=>r[:right_key]}, :table_alias=>assoc_table_alias)
|
140
|
+
end
|
141
|
+
eager_graph = ds.opts[:eager_graph]
|
142
|
+
eager_graph[:requirements][assoc_table_alias] = requirements.dup
|
143
|
+
eager_graph[:alias_association_name_map][assoc_table_alias] = assoc_name
|
144
|
+
eager_graph[:alias_association_type_map][assoc_table_alias] = assoc_type
|
145
|
+
ds = ds.eager_graph_associations(ds, klass, assoc_table_alias, requirements + [assoc_table_alias], *associations) unless associations.empty?
|
146
|
+
ds
|
147
|
+
end
|
148
|
+
|
149
|
+
# Check the associations are valid for the given model.
|
150
|
+
# Call eager_graph_association on each association.
|
151
|
+
#
|
152
|
+
# Arguments:
|
153
|
+
# * ds - Current dataset
|
154
|
+
# * model - Current Model
|
155
|
+
# * ta - table_alias used for the parent association
|
156
|
+
# * requirements - an array, used as a stack for requirements
|
157
|
+
# * *associations - the associations to add to the graph
|
158
|
+
def eager_graph_associations(ds, model, ta, requirements, *associations)
|
159
|
+
return ds if associations.empty?
|
160
|
+
associations.flatten.each do |association|
|
161
|
+
ds = case association
|
162
|
+
when Symbol
|
163
|
+
ds.eager_graph_association(ds, model, ta, requirements, check_association(model, association))
|
164
|
+
when Hash
|
165
|
+
association.each do |assoc, assoc_assocs|
|
166
|
+
ds = ds.eager_graph_association(ds, model, ta, requirements, check_association(model, assoc), assoc_assocs)
|
167
|
+
end
|
168
|
+
ds
|
169
|
+
else raise(ArgumentError, 'Associations must be in the form of a symbol or hash')
|
170
|
+
end
|
171
|
+
end
|
172
|
+
ds
|
173
|
+
end
|
174
|
+
|
175
|
+
# Build associations out of the array of returned object graphs.
|
176
|
+
def eager_graph_build_associations(record_graphs)
|
177
|
+
# Dup the tables that will be used, so that self is not modified.
|
178
|
+
eager_graph = @opts[:eager_graph]
|
179
|
+
master = eager_graph[:master]
|
180
|
+
requirements = eager_graph[:requirements]
|
181
|
+
alias_map = eager_graph[:alias_association_name_map]
|
182
|
+
type_map = eager_graph[:alias_association_type_map]
|
183
|
+
reciprocal_map = eager_graph[:reciprocals]
|
184
|
+
|
185
|
+
# Make dependency map hash out of requirements array for each association.
|
186
|
+
# This builds a tree of dependencies that will be used for recursion
|
187
|
+
# to ensure that all parts of the object graph are loaded into the
|
188
|
+
# appropriate subordinate association.
|
189
|
+
dependency_map = {}
|
190
|
+
# Sort the associations be requirements length, so that
|
191
|
+
# requirements are added to the dependency hash before their
|
192
|
+
# dependencies.
|
193
|
+
requirements.sort_by{|a| a[1].length}.each do |ta, deps|
|
194
|
+
if deps.empty?
|
195
|
+
dependency_map[ta] = {}
|
196
|
+
else
|
197
|
+
deps = deps.dup
|
198
|
+
hash = dependency_map[deps.shift]
|
199
|
+
deps.each do |dep|
|
200
|
+
hash = hash[dep]
|
201
|
+
end
|
202
|
+
hash[ta] = {}
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# This mapping is used to make sure that duplicate entries in the
|
207
|
+
# result set are mapped to a single record. For example, using a
|
208
|
+
# single one_to_many association with 10 associated records,
|
209
|
+
# the main object will appear in the object graph 10 times.
|
210
|
+
# We map by primary key, if available, or by the object's entire values,
|
211
|
+
# if not. The mapping must be per table, so create sub maps for each table
|
212
|
+
# alias.
|
213
|
+
records_map = {master=>{}}
|
214
|
+
alias_map.keys.each{|ta| records_map[ta] = {}}
|
215
|
+
|
216
|
+
# This will hold the final record set that we will be replacing the object graph with.
|
217
|
+
records = []
|
218
|
+
record_graphs.each do |record_graph|
|
219
|
+
primary_record = record_graph[master]
|
220
|
+
key = primary_record.pk || primary_record.values.sort_by{|x| x[0].to_s}
|
221
|
+
if cached_pr = records_map[master][key]
|
222
|
+
primary_record = cached_pr
|
223
|
+
else
|
224
|
+
records_map[master][key] = primary_record
|
225
|
+
# Only add it to the list of records to return if it is a new record
|
226
|
+
records.push(primary_record)
|
227
|
+
end
|
228
|
+
# Build all associations for the current object and it's dependencies
|
229
|
+
eager_graph_build_associations_graph(dependency_map, alias_map, type_map, reciprocal_map, records_map, primary_record, record_graph)
|
230
|
+
end
|
231
|
+
|
232
|
+
# Remove duplicate records from all associations if this graph could possibly be a cartesian product
|
233
|
+
eager_graph_make_associations_unique(records, dependency_map, alias_map, type_map) if type_map.reject{|k,v| v == :many_to_one}.length > 1
|
234
|
+
|
235
|
+
# Replace the array of object graphs with an array of model objects
|
236
|
+
record_graphs.replace(records)
|
237
|
+
end
|
238
|
+
|
239
|
+
# Creates a unique table alias that hasn't already been used in the query.
|
240
|
+
# Will either be the table_alias itself or table_alias_N for some integer
|
241
|
+
# N (starting at 0 and increasing until an unused one is found).
|
242
|
+
def eager_unique_table_alias(ds, table_alias)
|
243
|
+
if (graph = ds.opts[:graph]) && (table_aliases = graph[:table_aliases]) && (table_aliases.include?(table_alias))
|
244
|
+
i = 0
|
245
|
+
loop do
|
246
|
+
ta = :"#{table_alias}_#{i}"
|
247
|
+
return ta unless table_aliases[ta]
|
248
|
+
i += 1
|
249
|
+
end
|
250
|
+
else
|
251
|
+
table_alias
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
65
255
|
private
|
256
|
+
# Make sure a standard (non-polymorphic model) is used for this dataset, and return the model
|
257
|
+
def check_model
|
258
|
+
raise(ArgumentError, 'No model for this dataset') unless @opts[:models] && model = @opts[:models][nil]
|
259
|
+
model
|
260
|
+
end
|
261
|
+
|
262
|
+
# Make sure the association is valid for this model, and return the association's reflection
|
263
|
+
def check_association(model, association)
|
264
|
+
raise(ArgumentError, 'Invalid association') unless reflection = model.association_reflection(association)
|
265
|
+
raise(ArgumentError, 'Cannot eagerly load associations with block arguments') if reflection[:block]
|
266
|
+
reflection
|
267
|
+
end
|
268
|
+
|
269
|
+
# Build associations for the current object. This is called recursively
|
270
|
+
# to build object's dependencies.
|
271
|
+
def eager_graph_build_associations_graph(dependency_map, alias_map, type_map, reciprocal_map, records_map, current, record_graph)
|
272
|
+
return if dependency_map.empty?
|
273
|
+
# Don't clobber the instance variable array for *_to_many associations if it has already been setup
|
274
|
+
dependency_map.keys.each do |ta|
|
275
|
+
current.instance_variable_set("@#{alias_map[ta]}", []) unless type_map[ta] == :many_to_one || current.instance_variable_get("@#{alias_map[ta]}")
|
276
|
+
end
|
277
|
+
dependency_map.each do |ta, deps|
|
278
|
+
rec = record_graph[ta]
|
279
|
+
key = rec.pk || rec.values.sort_by{|x| x[0].to_s}
|
280
|
+
if cached_rec = records_map[ta][key]
|
281
|
+
rec = cached_rec
|
282
|
+
else
|
283
|
+
records_map[ta][rec.pk] = rec
|
284
|
+
end
|
285
|
+
ivar = "@#{alias_map[ta]}"
|
286
|
+
case assoc_type = type_map[ta]
|
287
|
+
when :many_to_one
|
288
|
+
current.instance_variable_set(ivar, rec)
|
289
|
+
else
|
290
|
+
list = current.instance_variable_get(ivar)
|
291
|
+
list.push(rec)
|
292
|
+
if (assoc_type == :one_to_many) && (reciprocal = reciprocal_map[ta])
|
293
|
+
rec.instance_variable_set(reciprocal, current)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
# Recurse into dependencies of the current object
|
297
|
+
eager_graph_build_associations_graph(deps, alias_map, type_map, reciprocal_map, records_map, rec, record_graph)
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
# If the result set is the result of a cartesian product, then it is possible that
|
302
|
+
# there a multiple records for each association when there should only be one.
|
303
|
+
def eager_graph_make_associations_unique(records, dependency_map, alias_map, type_map)
|
304
|
+
records.each do |record|
|
305
|
+
dependency_map.each do |ta, deps|
|
306
|
+
list = if type_map[ta] == :many_to_one
|
307
|
+
item = record.send(alias_map[ta])
|
308
|
+
[item] if item
|
309
|
+
else
|
310
|
+
list = record.send(alias_map[ta])
|
311
|
+
list.uniq!
|
312
|
+
# Recurse into dependencies
|
313
|
+
list.each{|rec| eager_graph_make_associations_unique(rec, deps, alias_map, type_map)}
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
66
319
|
# Eagerly load all specified associations
|
67
320
|
def eager_load(a)
|
68
321
|
return if a.empty?
|
@@ -136,9 +389,9 @@ module Sequel::Model::Associations::EagerLoading
|
|
136
389
|
right = reflection[:right_key]
|
137
390
|
right_pk = (reflection[:right_primary_key] || :"#{assoc_table}__#{assoc_class.primary_key}")
|
138
391
|
join_table = reflection[:join_table]
|
139
|
-
fkey = (
|
140
|
-
table_selection = (
|
141
|
-
key_selection = (
|
392
|
+
fkey = (reflection[:left_key_alias] ||= :"x_foreign_key_x")
|
393
|
+
table_selection = (reflection[:select] ||= assoc_table.*)
|
394
|
+
key_selection = (reflection[:left_key_select] ||= :"#{join_table}__#{left}___#{fkey}")
|
142
395
|
h = key_hash[model.primary_key]
|
143
396
|
ds = assoc_class.select(table_selection, key_selection).inner_join(join_table, right=>right_pk, left=>h.keys)
|
144
397
|
end
|
@@ -166,4 +419,11 @@ module Sequel::Model::Associations::EagerLoading
|
|
166
419
|
end
|
167
420
|
end
|
168
421
|
end
|
422
|
+
|
423
|
+
# Build associations from the graph if .eager_graph was used,
|
424
|
+
# and/or load other associations if .eager was used.
|
425
|
+
def post_load(all_records)
|
426
|
+
eager_graph_build_associations(all_records) if @opts[:eager_graph]
|
427
|
+
eager_load(all_records) if @opts[:eager]
|
428
|
+
end
|
169
429
|
end
|