sequel 3.9.0 → 3.10.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +56 -0
- data/README.rdoc +1 -1
- data/Rakefile +1 -1
- data/doc/advanced_associations.rdoc +7 -10
- data/doc/release_notes/3.10.0.txt +286 -0
- data/lib/sequel/adapters/do/mysql.rb +4 -0
- data/lib/sequel/adapters/jdbc.rb +5 -0
- data/lib/sequel/adapters/jdbc/as400.rb +58 -0
- data/lib/sequel/adapters/jdbc/oracle.rb +30 -0
- data/lib/sequel/adapters/shared/mssql.rb +23 -9
- data/lib/sequel/adapters/shared/mysql.rb +12 -1
- data/lib/sequel/adapters/shared/postgres.rb +7 -18
- data/lib/sequel/adapters/shared/sqlite.rb +5 -0
- data/lib/sequel/adapters/sqlite.rb +5 -0
- data/lib/sequel/connection_pool/single.rb +3 -3
- data/lib/sequel/database.rb +3 -2
- data/lib/sequel/dataset.rb +6 -5
- data/lib/sequel/dataset/convenience.rb +3 -3
- data/lib/sequel/dataset/query.rb +13 -0
- data/lib/sequel/dataset/sql.rb +31 -1
- data/lib/sequel/extensions/schema_dumper.rb +3 -3
- data/lib/sequel/model.rb +8 -6
- data/lib/sequel/model/associations.rb +144 -102
- data/lib/sequel/model/base.rb +21 -1
- data/lib/sequel/model/plugins.rb +3 -1
- data/lib/sequel/plugins/association_dependencies.rb +14 -7
- data/lib/sequel/plugins/caching.rb +4 -0
- data/lib/sequel/plugins/composition.rb +138 -0
- data/lib/sequel/plugins/identity_map.rb +2 -2
- data/lib/sequel/plugins/lazy_attributes.rb +1 -1
- data/lib/sequel/plugins/nested_attributes.rb +3 -2
- data/lib/sequel/plugins/rcte_tree.rb +281 -0
- data/lib/sequel/plugins/typecast_on_load.rb +16 -5
- data/lib/sequel/sql.rb +18 -1
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/mssql_spec.rb +4 -0
- data/spec/adapters/mysql_spec.rb +4 -0
- data/spec/adapters/postgres_spec.rb +55 -5
- data/spec/core/database_spec.rb +5 -3
- data/spec/core/dataset_spec.rb +86 -15
- data/spec/core/expression_filters_spec.rb +23 -6
- data/spec/extensions/association_dependencies_spec.rb +24 -5
- data/spec/extensions/association_proxies_spec.rb +3 -0
- data/spec/extensions/composition_spec.rb +194 -0
- data/spec/extensions/identity_map_spec.rb +16 -0
- data/spec/extensions/nested_attributes_spec.rb +44 -1
- data/spec/extensions/rcte_tree_spec.rb +205 -0
- data/spec/extensions/schema_dumper_spec.rb +6 -0
- data/spec/extensions/spec_helper.rb +6 -0
- data/spec/extensions/typecast_on_load_spec.rb +9 -0
- data/spec/extensions/validation_helpers_spec.rb +5 -5
- data/spec/integration/dataset_test.rb +13 -9
- data/spec/integration/eager_loader_test.rb +56 -1
- data/spec/integration/model_test.rb +8 -0
- data/spec/integration/plugin_test.rb +270 -0
- data/spec/integration/schema_test.rb +1 -1
- data/spec/model/associations_spec.rb +541 -118
- data/spec/model/eager_loading_spec.rb +24 -3
- data/spec/model/record_spec.rb +34 -0
- metadata +9 -2
data/lib/sequel/model/base.rb
CHANGED
@@ -301,7 +301,7 @@ module Sequel
|
|
301
301
|
# Example:
|
302
302
|
# class Tagging < Sequel::Model
|
303
303
|
# # composite key
|
304
|
-
# set_primary_key :taggable_id, :tag_id
|
304
|
+
# set_primary_key [:taggable_id, :tag_id]
|
305
305
|
# end
|
306
306
|
#
|
307
307
|
# class Person < Sequel::Model
|
@@ -437,6 +437,21 @@ module Sequel
|
|
437
437
|
end
|
438
438
|
schema_hash
|
439
439
|
end
|
440
|
+
|
441
|
+
# For the given opts hash and default name or :class option, add a
|
442
|
+
# :class_name option unless already present which contains the name
|
443
|
+
# of the class to use as a string. The purpose is to allow late
|
444
|
+
# binding to the class later using constantize.
|
445
|
+
def late_binding_class_option(opts, default)
|
446
|
+
case opts[:class]
|
447
|
+
when String, Symbol
|
448
|
+
# Delete :class to allow late binding
|
449
|
+
opts[:class_name] ||= opts.delete(:class).to_s
|
450
|
+
when Class
|
451
|
+
opts[:class_name] ||= opts[:class].name
|
452
|
+
end
|
453
|
+
opts[:class_name] ||= ((name || '').split("::")[0..-2] + [camelize(default)]).join('::')
|
454
|
+
end
|
440
455
|
|
441
456
|
# Module that the class includes that holds methods the class adds for column accessors and
|
442
457
|
# associations so that the methods can be overridden with super
|
@@ -653,6 +668,11 @@ module Sequel
|
|
653
668
|
@values.keys
|
654
669
|
end
|
655
670
|
|
671
|
+
# Refresh this record using for_update unless this is a new record. Returns self.
|
672
|
+
def lock!
|
673
|
+
new? ? self : _refresh(this.for_update)
|
674
|
+
end
|
675
|
+
|
656
676
|
# Remove elements of the model object that make marshalling fail. Returns self.
|
657
677
|
def marshallable!
|
658
678
|
@this = nil
|
data/lib/sequel/model/plugins.rb
CHANGED
@@ -64,7 +64,9 @@ module Sequel
|
|
64
64
|
# defined, the corresponding plugin gem is automatically loaded.
|
65
65
|
def plugin_module(plugin)
|
66
66
|
module_name = plugin.to_s.gsub(/(^|_)(.)/){|x| x[-1..-1].upcase}
|
67
|
-
if
|
67
|
+
if !Sequel::Plugins.const_defined?(module_name) ||
|
68
|
+
(Sequel.const_defined?(module_name) &&
|
69
|
+
Sequel::Plugins.const_get(module_name) == Sequel.const_get(module_name))
|
68
70
|
begin
|
69
71
|
Sequel.tsk_require "sequel/plugins/#{plugin}"
|
70
72
|
rescue LoadError
|
@@ -6,7 +6,7 @@ module Sequel
|
|
6
6
|
#
|
7
7
|
# * :many_to_many - :nullify (removes all related entries in join table)
|
8
8
|
# * :many_to_one - :delete, :destroy
|
9
|
-
# * :one_to_many - :delete, :destroy, :nullify (sets foreign key to NULL for all associated objects)
|
9
|
+
# * :one_to_many, one_to_one - :delete, :destroy, :nullify (sets foreign key to NULL for all associated objects)
|
10
10
|
#
|
11
11
|
# This plugin works directly with the association datasets and does not use any cached association values.
|
12
12
|
# The :delete action will delete all associated objects from the database in a single SQL call.
|
@@ -23,7 +23,7 @@ module Sequel
|
|
23
23
|
module AssociationDependencies
|
24
24
|
# Mapping of association types to when the dependency calls should be made (either
|
25
25
|
# :before for in before_destroy or :after for in after_destroy)
|
26
|
-
ASSOCIATION_MAPPING = {:one_to_many=>:before, :many_to_one=>:after, :many_to_many=>:before}
|
26
|
+
ASSOCIATION_MAPPING = {:one_to_many=>:before, :many_to_one=>:after, :many_to_many=>:before, :one_to_one=>:before}
|
27
27
|
|
28
28
|
# The valid dependence actions
|
29
29
|
DEPENDENCE_ACTIONS = [:delete, :destroy, :nullify]
|
@@ -52,13 +52,20 @@ module Sequel
|
|
52
52
|
def add_association_dependencies(hash)
|
53
53
|
hash.each do |association, action|
|
54
54
|
raise(Error, "Nonexistent association: #{association}") unless r = association_reflection(association)
|
55
|
+
type = r[:type]
|
55
56
|
raise(Error, "Invalid dependence action type: association: #{association}, dependence action: #{action}") unless DEPENDENCE_ACTIONS.include?(action)
|
56
|
-
raise(Error, "Invalid association type: association: #{association}, type: #{
|
57
|
+
raise(Error, "Invalid association type: association: #{association}, type: #{type}") unless time = ASSOCIATION_MAPPING[type]
|
57
58
|
association_dependencies[:"#{time}_#{action}"] << if action == :nullify
|
58
|
-
|
59
|
-
|
59
|
+
case type
|
60
|
+
when :one_to_many , :many_to_many
|
61
|
+
proc{send(r.remove_all_method)}
|
62
|
+
when :one_to_one
|
63
|
+
proc{send(r.setter_method, nil)}
|
64
|
+
else
|
65
|
+
raise(Error, "Can't nullify many_to_one associated objects: association: #{association}")
|
66
|
+
end
|
60
67
|
else
|
61
|
-
raise(Error, "Can only nullify many_to_many associations: association: #{association}") if
|
68
|
+
raise(Error, "Can only nullify many_to_many associations: association: #{association}") if type == :many_to_many
|
62
69
|
r.dataset_method
|
63
70
|
end
|
64
71
|
end
|
@@ -87,7 +94,7 @@ module Sequel
|
|
87
94
|
def before_destroy
|
88
95
|
model.association_dependencies[:before_delete].each{|m| send(m).delete}
|
89
96
|
model.association_dependencies[:before_destroy].each{|m| send(m).destroy}
|
90
|
-
model.association_dependencies[:before_nullify].each{|
|
97
|
+
model.association_dependencies[:before_nullify].each{|p| instance_eval(&p)}
|
91
98
|
super
|
92
99
|
end
|
93
100
|
end
|
@@ -15,11 +15,15 @@ module Sequel
|
|
15
15
|
# cache_store.get(key) => obj # Returns object set with same key.
|
16
16
|
# cache_store.get(key2) => nil # nil returned if there isn't an object
|
17
17
|
# # currently in the cache with that key.
|
18
|
+
# cache_store.delete(key) # Remove key from cache
|
18
19
|
#
|
19
20
|
# If the :ignore_exceptions option is true, exceptions raised by cache_store.get
|
20
21
|
# are ignored and nil is returned instead. The memcached API is to
|
21
22
|
# raise an exception for a missing record, so if you use memcached, you will
|
22
23
|
# want to use this option.
|
24
|
+
#
|
25
|
+
# Note that only Model.[] method calls with a primary key argument are cached
|
26
|
+
# using this plugin.
|
23
27
|
module Caching
|
24
28
|
# Set the cache_store and cache_ttl attributes for the given model.
|
25
29
|
# If the :ttl option is not given, 3600 seconds is the default.
|
@@ -0,0 +1,138 @@
|
|
1
|
+
module Sequel
|
2
|
+
module Plugins
|
3
|
+
# The composition plugin allows you to easily define getter and
|
4
|
+
# setter instance methods for a class where the backing data
|
5
|
+
# is composed of other getters and decomposed to other setters.
|
6
|
+
#
|
7
|
+
# A simple example of this is when you have a database table with
|
8
|
+
# separate columns for year, month, and day, but where you want
|
9
|
+
# to deal with Date objects in your ruby code. This can be handled
|
10
|
+
# with:
|
11
|
+
#
|
12
|
+
# Model.composition :date, :mapping=>[:year, :month, :day]
|
13
|
+
#
|
14
|
+
# The :mapping option is optional, but you can define custom
|
15
|
+
# composition and decomposition procs via the :composer and
|
16
|
+
# :decomposer options.
|
17
|
+
#
|
18
|
+
# Note that when using the composition object, you should not
|
19
|
+
# modify the underlying columns if you are also instantiating
|
20
|
+
# the composition, as otherwise the composition object values
|
21
|
+
# will override any underlying columns when the object is saved.
|
22
|
+
module Composition
|
23
|
+
# Define the necessary class instance variables.
|
24
|
+
def self.apply(model)
|
25
|
+
model.instance_eval{@compositions = {}}
|
26
|
+
end
|
27
|
+
|
28
|
+
module ClassMethods
|
29
|
+
# A hash with composition name keys and composition reflection
|
30
|
+
# hash values.
|
31
|
+
attr_reader :compositions
|
32
|
+
|
33
|
+
# A module included in the class holding the composition
|
34
|
+
# getter and setter methods.
|
35
|
+
attr_reader :composition_module
|
36
|
+
|
37
|
+
# Define a composition for this model, with name being the name of the composition.
|
38
|
+
# You must provide either a :mapping option or both the :composer and :decomposer options.
|
39
|
+
#
|
40
|
+
# Options:
|
41
|
+
# * :class - if using the :mapping option, the class to use, as a Class, String or Symbol.
|
42
|
+
# * :composer - A proc that is instance evaled when the composition getter method is called
|
43
|
+
# to create the composition.
|
44
|
+
# * :decomposer - A proc that is instance evaled before saving the model object,
|
45
|
+
# if the composition object exists, which sets the columns in the model object
|
46
|
+
# based on the value of the composition object.
|
47
|
+
# * :mapping - An array where each element is either a symbol or an array of two symbols.
|
48
|
+
# A symbol is treated like an array of two symbols where both symbols are the same.
|
49
|
+
# The first symbol represents the getter method in the model, and the second symbol
|
50
|
+
# represents the getter method in the composition object. Example:
|
51
|
+
# # Uses columns year, month, and day in the current model
|
52
|
+
# # Uses year, month, and day methods in the composition object
|
53
|
+
# :mapping=>[:year, :month, :day]
|
54
|
+
# # Uses columns year, month, and day in the current model
|
55
|
+
# # Uses y, m, and d methods in the composition object where
|
56
|
+
# # for example y in the composition object represents year
|
57
|
+
# # in the model object.
|
58
|
+
# :mapping=>[[:year, :y], [:month, :m], [:day, :d]]
|
59
|
+
def composition(name, opts={})
|
60
|
+
opts = opts.dup
|
61
|
+
compositions[name] = opts
|
62
|
+
if mapping = opts[:mapping]
|
63
|
+
keys = mapping.map{|k| k.is_a?(Array) ? k.first : k}
|
64
|
+
if !opts[:composer]
|
65
|
+
late_binding_class_option(opts, name)
|
66
|
+
klass = opts[:class]
|
67
|
+
class_proc = proc{klass || constantize(opts[:class_name])}
|
68
|
+
opts[:composer] = proc do
|
69
|
+
if values = keys.map{|k| send(k)} and values.any?{|v| !v.nil?}
|
70
|
+
class_proc.call.new(*values)
|
71
|
+
else
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
if !opts[:decomposer]
|
77
|
+
setter_meths = keys.map{|k| :"#{k}="}
|
78
|
+
cov_methods = mapping.map{|k| k.is_a?(Array) ? k.last : k}
|
79
|
+
setters = setter_meths.zip(cov_methods)
|
80
|
+
opts[:decomposer] = proc do
|
81
|
+
if (o = compositions[name]).nil?
|
82
|
+
setter_meths.each{|sm| send(sm, nil)}
|
83
|
+
else
|
84
|
+
setters.each{|sm, cm| send(sm, o.send(cm))}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
raise(Error, "Must provide :composer and :decomposer options, or :mapping option") unless opts[:composer] && opts[:decomposer]
|
90
|
+
define_composition_accessor(name, opts)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Copy the necessary class instance variables to the subclass.
|
94
|
+
def inherited(subclass)
|
95
|
+
super
|
96
|
+
c = compositions.dup
|
97
|
+
subclass.instance_eval{@compositions = c}
|
98
|
+
end
|
99
|
+
|
100
|
+
# Define getter and setter methods for the composition object.
|
101
|
+
def define_composition_accessor(name, opts={})
|
102
|
+
include(@composition_module ||= Module.new) unless composition_module
|
103
|
+
composer = opts[:composer]
|
104
|
+
composition_module.class_eval do
|
105
|
+
define_method(name) do
|
106
|
+
compositions.include?(name) ? compositions[name] : (compositions[name] = instance_eval(&composer))
|
107
|
+
end
|
108
|
+
define_method("#{name}=") do |v|
|
109
|
+
modified!
|
110
|
+
compositions[name] = v
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
module InstanceMethods
|
117
|
+
# Clear the cached compositions when refreshing.
|
118
|
+
def _refresh(ds)
|
119
|
+
v = super
|
120
|
+
compositions.clear
|
121
|
+
v
|
122
|
+
end
|
123
|
+
|
124
|
+
# For each composition, set the columns in the model class based
|
125
|
+
# on the composition object.
|
126
|
+
def before_save
|
127
|
+
@compositions.keys.each{|n| instance_eval(&model.compositions[n][:decomposer])} if @compositions
|
128
|
+
super
|
129
|
+
end
|
130
|
+
|
131
|
+
# Cache of composition objects for this class.
|
132
|
+
def compositions
|
133
|
+
@compositions ||= {}
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -103,8 +103,8 @@ module Sequel
|
|
103
103
|
# map for the associated object and return it if present.
|
104
104
|
def _load_associated_objects(opts)
|
105
105
|
klass = opts.associated_class
|
106
|
-
if idm = model.identity_map and
|
107
|
-
opts[:
|
106
|
+
if idm = model.identity_map and opts[:type] == :many_to_one and opts[:primary_key] == klass.primary_key and
|
107
|
+
opts[:key] and pk = send(opts[:key]) and o = idm[klass.identity_map_key(pk)]
|
108
108
|
o
|
109
109
|
else
|
110
110
|
super
|
@@ -17,7 +17,7 @@ module Sequel
|
|
17
17
|
# end
|
18
18
|
# end
|
19
19
|
module LazyAttributes
|
20
|
-
#
|
20
|
+
# Lazy attributes requires the identity map and tactical eager loading plugins
|
21
21
|
def self.apply(model, *attrs)
|
22
22
|
model.plugin :identity_map
|
23
23
|
model.plugin :tactical_eager_loading
|
@@ -91,9 +91,10 @@ module Sequel
|
|
91
91
|
send(reflection[:name]) << obj
|
92
92
|
after_save_hook{send(reflection.add_method, obj)}
|
93
93
|
else
|
94
|
+
associations[reflection[:name]] = obj
|
94
95
|
# Don't need to validate the object twice if :validate association option is not false
|
95
96
|
# and don't want to validate it at all if it is false.
|
96
|
-
before_save_hook{send(reflection.setter_method, obj.save(:validate=>false))}
|
97
|
+
send(reflection[:type] == :many_to_one ? :before_save_hook : :after_save_hook){send(reflection.setter_method, obj.save(:validate=>false))}
|
97
98
|
end
|
98
99
|
obj
|
99
100
|
end
|
@@ -103,7 +104,7 @@ module Sequel
|
|
103
104
|
def nested_attributes_find(reflection, pk)
|
104
105
|
pk = pk.to_s
|
105
106
|
unless obj = Array(associated_objects = send(reflection[:name])).find{|x| x.pk.to_s == pk}
|
106
|
-
raise(Error,
|
107
|
+
raise(Error, "no matching associated object with given primary key (association: #{reflection[:name]}, pk: #{pk})") unless reflection[:nested_attributes][:strict] == false
|
107
108
|
end
|
108
109
|
obj
|
109
110
|
end
|
@@ -0,0 +1,281 @@
|
|
1
|
+
module Sequel
|
2
|
+
module Plugins
|
3
|
+
# = Overview
|
4
|
+
#
|
5
|
+
# The rcte_tree plugin deals with tree structured data stored
|
6
|
+
# in the database using the adjacency list model (where child rows
|
7
|
+
# have a foreign key pointing to the parent rows), using recursive
|
8
|
+
# common table expressions to load all ancestors in a single query,
|
9
|
+
# all descendants in a single query, and all descendants to a given
|
10
|
+
# level (where level 1 is children, level 2 is children and grandchildren
|
11
|
+
# etc.) in a single query.
|
12
|
+
#
|
13
|
+
# = Background
|
14
|
+
#
|
15
|
+
# There are two types of common models for storing tree structured data
|
16
|
+
# in an SQL database, the adjacency list model and the nested set model.
|
17
|
+
# Before recursive common table expressions (or similar capabilities such
|
18
|
+
# as CONNECT BY for Oracle), the nested set model was the only easy way
|
19
|
+
# to retrieve all ancestors and descendants in a single query. However,
|
20
|
+
# it has significant performance corner cases.
|
21
|
+
#
|
22
|
+
# On PostgreSQL 8.4, with a significant number of rows, the nested set
|
23
|
+
# model is almost 500 times slower than using a recursive common table
|
24
|
+
# expression with the adjacency list model to get all descendants, and
|
25
|
+
# almost 24,000 times slower to get all descendants to a given level.
|
26
|
+
#
|
27
|
+
# Considering that the nested set model requires more difficult management
|
28
|
+
# than the adjacency list model, it's almost always better to use the
|
29
|
+
# adjacency list model if your database supports common table expressions.
|
30
|
+
# See http://explainextended.com/2009/09/24/adjacency-list-vs-nested-sets-postgresql/
|
31
|
+
# for detailed analysis.
|
32
|
+
#
|
33
|
+
# = Usage
|
34
|
+
#
|
35
|
+
# The rcte_tree plugin is unlike most plugins in that it doesn't add any class,
|
36
|
+
# instance, or dataset modules. It only has a single apply method, which
|
37
|
+
# adds four associations to the model: parent, children, ancestors, and
|
38
|
+
# descendants. Both the parent and children are fairly standard many_to_one
|
39
|
+
# and one_to_many associations, respectively. However, the ancestors and
|
40
|
+
# descendants associations are special. Both the ancestors and descendants
|
41
|
+
# associations will automatically set the parent and children associations,
|
42
|
+
# respectively, for current object and all of the ancestor or descendant
|
43
|
+
# objects, whenever they are loaded (either eagerly or lazily). Additionally,
|
44
|
+
# the descendants association can take a level argument when called eagerly,
|
45
|
+
# which limits the returned objects to only that many levels in the tree (see
|
46
|
+
# the Overview).
|
47
|
+
#
|
48
|
+
# Model.plugin :rcte_tree
|
49
|
+
#
|
50
|
+
# # Lazy loading
|
51
|
+
# model = Model.first
|
52
|
+
# model.parent
|
53
|
+
# model.children
|
54
|
+
# model.ancestors # Populates :parent association for all ancestors
|
55
|
+
# model.descendants # Populates :children association for all descendants
|
56
|
+
#
|
57
|
+
# # Eager loading - also populates the :parent and children associations
|
58
|
+
# # for all ancestors and descendants
|
59
|
+
# Model.filter(:id=>[1, 2]).eager(:ancestors, :descendants).all
|
60
|
+
#
|
61
|
+
# # Eager loading children and grand children
|
62
|
+
# Model.filter(:id=>[1, 2]).eager(:descendants=>2).all
|
63
|
+
# # Eager loading children, grand children, and great grand children
|
64
|
+
# Model.filter(:id=>[1, 2]).eager(:descendants=>3).all
|
65
|
+
#
|
66
|
+
# = Options
|
67
|
+
#
|
68
|
+
# You can override the options for any specific association by making
|
69
|
+
# sure the plugin options contain one of the following keys:
|
70
|
+
#
|
71
|
+
# * :parent - hash of options for the parent association
|
72
|
+
# * :children - hash of options for the children association
|
73
|
+
# * :ancestors - hash of options for the ancestors association
|
74
|
+
# * :descendants - hash of options for the descendants association
|
75
|
+
#
|
76
|
+
# Note that you can change the name of the above associations by specifying
|
77
|
+
# a :name key in the appropriate hash of options above. For example:
|
78
|
+
#
|
79
|
+
# Model.plugin :rcte_tree, :parent=>{:name=>:mother},
|
80
|
+
# :children=>{:name=>:daughters}, :descendants=>{:name=>:offspring}
|
81
|
+
#
|
82
|
+
# Any other keys in the main options hash are treated as options shared by
|
83
|
+
# all of the associations. Here's a few options that affect the plugin:
|
84
|
+
#
|
85
|
+
# * :key - The foreign key in the table that points to the primary key
|
86
|
+
# of the parent (default: :parent_id)
|
87
|
+
# * :primary_key - The primary key to use (default: the model's primary key)
|
88
|
+
# * :key_alias - The symbol identifier to use for aliasing when eager
|
89
|
+
# loading (default: :x_root_x)
|
90
|
+
# * :cte_name - The symbol identifier to use for the common table expression
|
91
|
+
# (default: :t)
|
92
|
+
# * :level_alias - The symbol identifier to use when eagerly loading descendants
|
93
|
+
# up to a given level (default: :x_level_x)
|
94
|
+
module RcteTree
|
95
|
+
# Create the appropriate parent, children, ancestors, and descendants
|
96
|
+
# associations for the model.
|
97
|
+
def self.apply(model, opts={})
|
98
|
+
opts = opts.dup
|
99
|
+
opts[:class] = model
|
100
|
+
|
101
|
+
key = opts[:key] ||= :parent_id
|
102
|
+
prkey = opts[:primary_key] ||= model.primary_key
|
103
|
+
|
104
|
+
par = opts.merge(opts.fetch(:parent, {}))
|
105
|
+
parent = par.fetch(:name, :parent)
|
106
|
+
model.many_to_one parent, par
|
107
|
+
|
108
|
+
chi = opts.merge(opts.fetch(:children, {}))
|
109
|
+
childrena = chi.fetch(:name, :children)
|
110
|
+
model.one_to_many childrena, chi
|
111
|
+
|
112
|
+
ka = opts[:key_alias] ||= :x_root_x
|
113
|
+
t = opts[:cte_name] ||= :t
|
114
|
+
opts[:reciprocal] = nil
|
115
|
+
c_all = SQL::ColumnAll.new(model.table_name)
|
116
|
+
|
117
|
+
a = opts.merge(opts.fetch(:ancestors, {}))
|
118
|
+
ancestors = a.fetch(:name, :ancestors)
|
119
|
+
a[:read_only] = true unless a.has_key?(:read_only)
|
120
|
+
a[:eager_loader_key] = key
|
121
|
+
a[:dataset] ||= proc do
|
122
|
+
model.from(t).
|
123
|
+
with_recursive(t, model.filter(prkey=>send(key)),
|
124
|
+
model.join(t, key=>prkey).
|
125
|
+
select(c_all))
|
126
|
+
end
|
127
|
+
aal = Array(a[:after_load])
|
128
|
+
aal << proc do |m, ancs|
|
129
|
+
unless m.associations.has_key?(parent)
|
130
|
+
parent_map = {m[prkey]=>m}
|
131
|
+
child_map = {}
|
132
|
+
child_map[m[key]] = m if m[key]
|
133
|
+
m.associations[parent] = nil
|
134
|
+
ancs.each do |obj|
|
135
|
+
obj.associations[parent] = nil
|
136
|
+
parent_map[obj[prkey]] = obj
|
137
|
+
if ok = obj[key]
|
138
|
+
child_map[ok] = obj
|
139
|
+
end
|
140
|
+
end
|
141
|
+
parent_map.each do |parent_id, obj|
|
142
|
+
if child = child_map[parent_id]
|
143
|
+
child.associations[parent] = obj
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
a[:after_load] ||= aal
|
149
|
+
a[:eager_loader] ||= proc do |key_hash, objects, associations|
|
150
|
+
id_map = key_hash[key]
|
151
|
+
parent_map = {}
|
152
|
+
children_map = {}
|
153
|
+
objects.each do |obj|
|
154
|
+
parent_map[obj[prkey]] = obj
|
155
|
+
(children_map[obj[key]] ||= []) << obj
|
156
|
+
obj.associations[ancestors] = []
|
157
|
+
obj.associations[parent] = nil
|
158
|
+
end
|
159
|
+
r = model.association_reflection(ancestors)
|
160
|
+
model.eager_loading_dataset(r,
|
161
|
+
model.from(t).
|
162
|
+
with_recursive(t, model.filter(prkey=>id_map.keys).
|
163
|
+
select(SQL::AliasedExpression.new(prkey, ka), c_all),
|
164
|
+
model.join(t, key=>prkey).
|
165
|
+
select(SQL::QualifiedIdentifier.new(t, ka), c_all)),
|
166
|
+
r.select,
|
167
|
+
associations).all do |obj|
|
168
|
+
opk = obj[prkey]
|
169
|
+
if in_pm = parent_map.has_key?(opk)
|
170
|
+
if idm_obj = parent_map[opk]
|
171
|
+
idm_obj.values[ka] = obj.values[ka]
|
172
|
+
obj = idm_obj
|
173
|
+
end
|
174
|
+
else
|
175
|
+
obj.associations[parent] = nil
|
176
|
+
parent_map[opk] = obj
|
177
|
+
(children_map[obj[key]] ||= []) << obj
|
178
|
+
end
|
179
|
+
|
180
|
+
if roots = id_map[obj.values.delete(ka)]
|
181
|
+
roots.each do |root|
|
182
|
+
root.associations[ancestors] << obj
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
parent_map.each do |parent_id, obj|
|
187
|
+
if children = children_map[parent_id]
|
188
|
+
children.each do |child|
|
189
|
+
child.associations[parent] = obj
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
model.one_to_many ancestors, a
|
195
|
+
|
196
|
+
d = opts.merge(opts.fetch(:descendants, {}))
|
197
|
+
descendants = d.fetch(:name, :descendants)
|
198
|
+
d[:read_only] = true unless d.has_key?(:read_only)
|
199
|
+
la = d[:level_alias] ||= :x_level_x
|
200
|
+
d[:dataset] ||= proc do
|
201
|
+
model.from(t).
|
202
|
+
with_recursive(t, model.filter(key=>send(prkey)),
|
203
|
+
model.join(t, prkey=>key).
|
204
|
+
select(SQL::ColumnAll.new(model.table_name)))
|
205
|
+
end
|
206
|
+
dal = Array(d[:after_load])
|
207
|
+
dal << proc do |m, descs|
|
208
|
+
unless m.associations.has_key?(childrena)
|
209
|
+
parent_map = {m[prkey]=>m}
|
210
|
+
children_map = {}
|
211
|
+
m.associations[childrena] = []
|
212
|
+
descs.each do |obj|
|
213
|
+
obj.associations[childrena] = []
|
214
|
+
if opk = obj[prkey]
|
215
|
+
parent_map[opk] = obj
|
216
|
+
end
|
217
|
+
if ok = obj[key]
|
218
|
+
(children_map[ok] ||= []) << obj
|
219
|
+
end
|
220
|
+
end
|
221
|
+
children_map.each do |parent_id, objs|
|
222
|
+
parent_map[parent_id].associations[childrena] = objs
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
d[:after_load] = dal
|
227
|
+
d[:eager_loader] ||= proc do |key_hash, objects, associations|
|
228
|
+
id_map = key_hash[prkey]
|
229
|
+
parent_map = {}
|
230
|
+
children_map = {}
|
231
|
+
objects.each do |obj|
|
232
|
+
parent_map[obj[prkey]] = obj
|
233
|
+
obj.associations[descendants] = []
|
234
|
+
obj.associations[childrena] = []
|
235
|
+
end
|
236
|
+
r = model.association_reflection(descendants)
|
237
|
+
base_case = model.filter(key=>id_map.keys).
|
238
|
+
select(SQL::AliasedExpression.new(key, ka), c_all)
|
239
|
+
recursive_case = model.join(t, prkey=>key).
|
240
|
+
select(SQL::QualifiedIdentifier.new(t, ka), c_all)
|
241
|
+
if associations.is_a?(Integer)
|
242
|
+
level = associations
|
243
|
+
no_cache_level = level - 1
|
244
|
+
associations = {}
|
245
|
+
base_case = base_case.select_more(SQL::AliasedExpression.new(0, la))
|
246
|
+
recursive_case = recursive_case.select_more(SQL::AliasedExpression.new(SQL::QualifiedIdentifier.new(t, la) + 1, la)).filter(SQL::QualifiedIdentifier.new(t, la) < level - 1)
|
247
|
+
end
|
248
|
+
model.eager_loading_dataset(r,
|
249
|
+
model.from(t).with_recursive(t, base_case, recursive_case),
|
250
|
+
r.select,
|
251
|
+
associations).all do |obj|
|
252
|
+
if level
|
253
|
+
no_cache = no_cache_level == obj.values.delete(la)
|
254
|
+
end
|
255
|
+
|
256
|
+
opk = obj[prkey]
|
257
|
+
if in_pm = parent_map.has_key?(opk)
|
258
|
+
if idm_obj = parent_map[opk]
|
259
|
+
idm_obj.values[ka] = obj.values[ka]
|
260
|
+
obj = idm_obj
|
261
|
+
end
|
262
|
+
else
|
263
|
+
obj.associations[childrena] = [] unless no_cache
|
264
|
+
parent_map[opk] = obj
|
265
|
+
end
|
266
|
+
|
267
|
+
if root = id_map[obj.values.delete(ka)].first
|
268
|
+
root.associations[descendants] << obj
|
269
|
+
end
|
270
|
+
|
271
|
+
(children_map[obj[key]] ||= []) << obj
|
272
|
+
end
|
273
|
+
children_map.each do |parent_id, objs|
|
274
|
+
parent_map[parent_id].associations[childrena] = objs.uniq
|
275
|
+
end
|
276
|
+
end
|
277
|
+
model.one_to_many descendants, d
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|