sequel 1.4.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|