viking-sequel 3.10.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 +3134 -0
- data/COPYING +19 -0
- data/README.rdoc +723 -0
- data/Rakefile +193 -0
- data/bin/sequel +196 -0
- data/doc/advanced_associations.rdoc +644 -0
- data/doc/cheat_sheet.rdoc +218 -0
- data/doc/dataset_basics.rdoc +106 -0
- data/doc/dataset_filtering.rdoc +158 -0
- data/doc/opening_databases.rdoc +296 -0
- data/doc/prepared_statements.rdoc +104 -0
- data/doc/reflection.rdoc +84 -0
- data/doc/release_notes/1.0.txt +38 -0
- data/doc/release_notes/1.1.txt +143 -0
- data/doc/release_notes/1.3.txt +101 -0
- data/doc/release_notes/1.4.0.txt +53 -0
- data/doc/release_notes/1.5.0.txt +155 -0
- data/doc/release_notes/2.0.0.txt +298 -0
- data/doc/release_notes/2.1.0.txt +271 -0
- data/doc/release_notes/2.10.0.txt +328 -0
- data/doc/release_notes/2.11.0.txt +215 -0
- data/doc/release_notes/2.12.0.txt +534 -0
- data/doc/release_notes/2.2.0.txt +253 -0
- data/doc/release_notes/2.3.0.txt +88 -0
- data/doc/release_notes/2.4.0.txt +106 -0
- data/doc/release_notes/2.5.0.txt +137 -0
- data/doc/release_notes/2.6.0.txt +157 -0
- data/doc/release_notes/2.7.0.txt +166 -0
- data/doc/release_notes/2.8.0.txt +171 -0
- data/doc/release_notes/2.9.0.txt +97 -0
- data/doc/release_notes/3.0.0.txt +221 -0
- data/doc/release_notes/3.1.0.txt +406 -0
- data/doc/release_notes/3.10.0.txt +286 -0
- data/doc/release_notes/3.2.0.txt +268 -0
- data/doc/release_notes/3.3.0.txt +192 -0
- data/doc/release_notes/3.4.0.txt +325 -0
- data/doc/release_notes/3.5.0.txt +510 -0
- data/doc/release_notes/3.6.0.txt +366 -0
- data/doc/release_notes/3.7.0.txt +179 -0
- data/doc/release_notes/3.8.0.txt +151 -0
- data/doc/release_notes/3.9.0.txt +233 -0
- data/doc/schema.rdoc +36 -0
- data/doc/sharding.rdoc +113 -0
- data/doc/virtual_rows.rdoc +205 -0
- data/lib/sequel.rb +1 -0
- data/lib/sequel/adapters/ado.rb +90 -0
- data/lib/sequel/adapters/ado/mssql.rb +30 -0
- data/lib/sequel/adapters/amalgalite.rb +176 -0
- data/lib/sequel/adapters/db2.rb +139 -0
- data/lib/sequel/adapters/dbi.rb +113 -0
- data/lib/sequel/adapters/do.rb +188 -0
- data/lib/sequel/adapters/do/mysql.rb +49 -0
- data/lib/sequel/adapters/do/postgres.rb +91 -0
- data/lib/sequel/adapters/do/sqlite.rb +40 -0
- data/lib/sequel/adapters/firebird.rb +283 -0
- data/lib/sequel/adapters/informix.rb +77 -0
- data/lib/sequel/adapters/jdbc.rb +587 -0
- data/lib/sequel/adapters/jdbc/as400.rb +58 -0
- data/lib/sequel/adapters/jdbc/h2.rb +133 -0
- data/lib/sequel/adapters/jdbc/mssql.rb +57 -0
- data/lib/sequel/adapters/jdbc/mysql.rb +78 -0
- data/lib/sequel/adapters/jdbc/oracle.rb +50 -0
- data/lib/sequel/adapters/jdbc/postgresql.rb +108 -0
- data/lib/sequel/adapters/jdbc/sqlite.rb +55 -0
- data/lib/sequel/adapters/mysql.rb +421 -0
- data/lib/sequel/adapters/odbc.rb +143 -0
- data/lib/sequel/adapters/odbc/mssql.rb +42 -0
- data/lib/sequel/adapters/openbase.rb +64 -0
- data/lib/sequel/adapters/oracle.rb +131 -0
- data/lib/sequel/adapters/postgres.rb +504 -0
- data/lib/sequel/adapters/shared/mssql.rb +490 -0
- data/lib/sequel/adapters/shared/mysql.rb +498 -0
- data/lib/sequel/adapters/shared/oracle.rb +195 -0
- data/lib/sequel/adapters/shared/postgres.rb +830 -0
- data/lib/sequel/adapters/shared/progress.rb +44 -0
- data/lib/sequel/adapters/shared/sqlite.rb +389 -0
- data/lib/sequel/adapters/sqlite.rb +224 -0
- data/lib/sequel/adapters/utils/stored_procedures.rb +84 -0
- data/lib/sequel/connection_pool.rb +99 -0
- data/lib/sequel/connection_pool/sharded_single.rb +84 -0
- data/lib/sequel/connection_pool/sharded_threaded.rb +211 -0
- data/lib/sequel/connection_pool/single.rb +29 -0
- data/lib/sequel/connection_pool/threaded.rb +150 -0
- data/lib/sequel/core.rb +293 -0
- data/lib/sequel/core_sql.rb +241 -0
- data/lib/sequel/database.rb +1079 -0
- data/lib/sequel/database/schema_generator.rb +327 -0
- data/lib/sequel/database/schema_methods.rb +203 -0
- data/lib/sequel/database/schema_sql.rb +320 -0
- data/lib/sequel/dataset.rb +32 -0
- data/lib/sequel/dataset/actions.rb +441 -0
- data/lib/sequel/dataset/features.rb +86 -0
- data/lib/sequel/dataset/graph.rb +254 -0
- data/lib/sequel/dataset/misc.rb +119 -0
- data/lib/sequel/dataset/mutation.rb +64 -0
- data/lib/sequel/dataset/prepared_statements.rb +227 -0
- data/lib/sequel/dataset/query.rb +709 -0
- data/lib/sequel/dataset/sql.rb +996 -0
- data/lib/sequel/exceptions.rb +51 -0
- data/lib/sequel/extensions/blank.rb +43 -0
- data/lib/sequel/extensions/inflector.rb +242 -0
- data/lib/sequel/extensions/looser_typecasting.rb +21 -0
- data/lib/sequel/extensions/migration.rb +239 -0
- data/lib/sequel/extensions/named_timezones.rb +61 -0
- data/lib/sequel/extensions/pagination.rb +100 -0
- data/lib/sequel/extensions/pretty_table.rb +82 -0
- data/lib/sequel/extensions/query.rb +52 -0
- data/lib/sequel/extensions/schema_dumper.rb +271 -0
- data/lib/sequel/extensions/sql_expr.rb +122 -0
- data/lib/sequel/extensions/string_date_time.rb +46 -0
- data/lib/sequel/extensions/thread_local_timezones.rb +48 -0
- data/lib/sequel/metaprogramming.rb +9 -0
- data/lib/sequel/model.rb +120 -0
- data/lib/sequel/model/associations.rb +1514 -0
- data/lib/sequel/model/base.rb +1069 -0
- data/lib/sequel/model/default_inflections.rb +45 -0
- data/lib/sequel/model/errors.rb +39 -0
- data/lib/sequel/model/exceptions.rb +21 -0
- data/lib/sequel/model/inflections.rb +162 -0
- data/lib/sequel/model/plugins.rb +70 -0
- data/lib/sequel/plugins/active_model.rb +59 -0
- data/lib/sequel/plugins/association_dependencies.rb +103 -0
- data/lib/sequel/plugins/association_proxies.rb +41 -0
- data/lib/sequel/plugins/boolean_readers.rb +53 -0
- data/lib/sequel/plugins/caching.rb +141 -0
- data/lib/sequel/plugins/class_table_inheritance.rb +214 -0
- data/lib/sequel/plugins/composition.rb +138 -0
- data/lib/sequel/plugins/force_encoding.rb +72 -0
- data/lib/sequel/plugins/hook_class_methods.rb +126 -0
- data/lib/sequel/plugins/identity_map.rb +116 -0
- data/lib/sequel/plugins/instance_filters.rb +98 -0
- data/lib/sequel/plugins/instance_hooks.rb +57 -0
- data/lib/sequel/plugins/lazy_attributes.rb +77 -0
- data/lib/sequel/plugins/many_through_many.rb +208 -0
- data/lib/sequel/plugins/nested_attributes.rb +206 -0
- data/lib/sequel/plugins/optimistic_locking.rb +81 -0
- data/lib/sequel/plugins/rcte_tree.rb +281 -0
- data/lib/sequel/plugins/schema.rb +66 -0
- data/lib/sequel/plugins/serialization.rb +166 -0
- data/lib/sequel/plugins/single_table_inheritance.rb +74 -0
- data/lib/sequel/plugins/subclasses.rb +45 -0
- data/lib/sequel/plugins/tactical_eager_loading.rb +61 -0
- data/lib/sequel/plugins/timestamps.rb +87 -0
- data/lib/sequel/plugins/touch.rb +118 -0
- data/lib/sequel/plugins/typecast_on_load.rb +72 -0
- data/lib/sequel/plugins/validation_class_methods.rb +405 -0
- data/lib/sequel/plugins/validation_helpers.rb +223 -0
- data/lib/sequel/sql.rb +1020 -0
- data/lib/sequel/timezones.rb +161 -0
- data/lib/sequel/version.rb +12 -0
- data/lib/sequel_core.rb +1 -0
- data/lib/sequel_model.rb +1 -0
- data/spec/adapters/firebird_spec.rb +407 -0
- data/spec/adapters/informix_spec.rb +97 -0
- data/spec/adapters/mssql_spec.rb +403 -0
- data/spec/adapters/mysql_spec.rb +1019 -0
- data/spec/adapters/oracle_spec.rb +286 -0
- data/spec/adapters/postgres_spec.rb +969 -0
- data/spec/adapters/spec_helper.rb +51 -0
- data/spec/adapters/sqlite_spec.rb +432 -0
- data/spec/core/connection_pool_spec.rb +808 -0
- data/spec/core/core_sql_spec.rb +417 -0
- data/spec/core/database_spec.rb +1662 -0
- data/spec/core/dataset_spec.rb +3827 -0
- data/spec/core/expression_filters_spec.rb +595 -0
- data/spec/core/object_graph_spec.rb +296 -0
- data/spec/core/schema_generator_spec.rb +159 -0
- data/spec/core/schema_spec.rb +830 -0
- data/spec/core/spec_helper.rb +56 -0
- data/spec/core/version_spec.rb +7 -0
- data/spec/extensions/active_model_spec.rb +76 -0
- data/spec/extensions/association_dependencies_spec.rb +127 -0
- data/spec/extensions/association_proxies_spec.rb +50 -0
- data/spec/extensions/blank_spec.rb +67 -0
- data/spec/extensions/boolean_readers_spec.rb +92 -0
- data/spec/extensions/caching_spec.rb +250 -0
- data/spec/extensions/class_table_inheritance_spec.rb +252 -0
- data/spec/extensions/composition_spec.rb +194 -0
- data/spec/extensions/force_encoding_spec.rb +117 -0
- data/spec/extensions/hook_class_methods_spec.rb +470 -0
- data/spec/extensions/identity_map_spec.rb +202 -0
- data/spec/extensions/inflector_spec.rb +181 -0
- data/spec/extensions/instance_filters_spec.rb +55 -0
- data/spec/extensions/instance_hooks_spec.rb +133 -0
- data/spec/extensions/lazy_attributes_spec.rb +153 -0
- data/spec/extensions/looser_typecasting_spec.rb +39 -0
- data/spec/extensions/many_through_many_spec.rb +884 -0
- data/spec/extensions/migration_spec.rb +332 -0
- data/spec/extensions/named_timezones_spec.rb +72 -0
- data/spec/extensions/nested_attributes_spec.rb +396 -0
- data/spec/extensions/optimistic_locking_spec.rb +100 -0
- data/spec/extensions/pagination_spec.rb +99 -0
- data/spec/extensions/pretty_table_spec.rb +91 -0
- data/spec/extensions/query_spec.rb +85 -0
- data/spec/extensions/rcte_tree_spec.rb +205 -0
- data/spec/extensions/schema_dumper_spec.rb +357 -0
- data/spec/extensions/schema_spec.rb +127 -0
- data/spec/extensions/serialization_spec.rb +209 -0
- data/spec/extensions/single_table_inheritance_spec.rb +96 -0
- data/spec/extensions/spec_helper.rb +91 -0
- data/spec/extensions/sql_expr_spec.rb +89 -0
- data/spec/extensions/string_date_time_spec.rb +93 -0
- data/spec/extensions/subclasses_spec.rb +52 -0
- data/spec/extensions/tactical_eager_loading_spec.rb +65 -0
- data/spec/extensions/thread_local_timezones_spec.rb +45 -0
- data/spec/extensions/timestamps_spec.rb +150 -0
- data/spec/extensions/touch_spec.rb +155 -0
- data/spec/extensions/typecast_on_load_spec.rb +69 -0
- data/spec/extensions/validation_class_methods_spec.rb +984 -0
- data/spec/extensions/validation_helpers_spec.rb +438 -0
- data/spec/integration/associations_test.rb +281 -0
- data/spec/integration/database_test.rb +26 -0
- data/spec/integration/dataset_test.rb +963 -0
- data/spec/integration/eager_loader_test.rb +734 -0
- data/spec/integration/model_test.rb +130 -0
- data/spec/integration/plugin_test.rb +814 -0
- data/spec/integration/prepared_statement_test.rb +213 -0
- data/spec/integration/schema_test.rb +361 -0
- data/spec/integration/spec_helper.rb +73 -0
- data/spec/integration/timezone_test.rb +55 -0
- data/spec/integration/transaction_test.rb +122 -0
- data/spec/integration/type_test.rb +96 -0
- data/spec/model/association_reflection_spec.rb +175 -0
- data/spec/model/associations_spec.rb +2633 -0
- data/spec/model/base_spec.rb +418 -0
- data/spec/model/dataset_methods_spec.rb +78 -0
- data/spec/model/eager_loading_spec.rb +1391 -0
- data/spec/model/hooks_spec.rb +240 -0
- data/spec/model/inflector_spec.rb +26 -0
- data/spec/model/model_spec.rb +593 -0
- data/spec/model/plugins_spec.rb +236 -0
- data/spec/model/record_spec.rb +1500 -0
- data/spec/model/spec_helper.rb +97 -0
- data/spec/model/validations_spec.rb +153 -0
- data/spec/rcov.opts +6 -0
- data/spec/spec_config.rb.example +10 -0
- metadata +346 -0
@@ -0,0 +1,98 @@
|
|
1
|
+
module Sequel
|
2
|
+
module Plugins
|
3
|
+
# This plugin allows you to add filters on a per object basis that
|
4
|
+
# restrict updating or deleting the object. It's designed for cases
|
5
|
+
# where you would normally have to drop down to the dataset level
|
6
|
+
# to get the necessary control, because you only want to delete or
|
7
|
+
# update the rows in certain cases based on the current status of
|
8
|
+
# the row in the database.
|
9
|
+
#
|
10
|
+
# class Item < Sequel::Model
|
11
|
+
# plugin :instance_filters
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# # These are two separate objects that represent the same
|
15
|
+
# # database row.
|
16
|
+
# i1 = Item.first(:id=>1, :delete_allowed=>false)
|
17
|
+
# i2 = Item.first(:id=>1, :delete_allowed=>false)
|
18
|
+
#
|
19
|
+
# # Add an instance filter to the object. This filter is in effect
|
20
|
+
# # until the object is successfully updated or deleted.
|
21
|
+
# i1.instance_filter(:delete_allowed=>true)
|
22
|
+
#
|
23
|
+
# # Attempting to delete the object where the filter doesn't
|
24
|
+
# # match any rows raises an error.
|
25
|
+
# i1.delete # raises Sequel::Error
|
26
|
+
#
|
27
|
+
# # The other object that represents the same row has no
|
28
|
+
# # instance filters, and can be updated normally.
|
29
|
+
# i2.update(:delete_allowed=>true)
|
30
|
+
#
|
31
|
+
# # Even though the filter is now still in effect, since the
|
32
|
+
# # database row has been updated to allow deleting,
|
33
|
+
# # delete now works.
|
34
|
+
# i1.delete
|
35
|
+
#
|
36
|
+
# This plugin sets the require_modification flag on the model,
|
37
|
+
# so if the model's dataset doesn't provide an accurate number
|
38
|
+
# of matched rows, this could result in invalid exceptions being raised.
|
39
|
+
module InstanceFilters
|
40
|
+
# Exception class raised when updating or deleting an object does
|
41
|
+
# not affect exactly one row.
|
42
|
+
Error = Sequel::NoExistingObject
|
43
|
+
|
44
|
+
# Set the require_modification flag to true for the model.
|
45
|
+
def self.configure(model)
|
46
|
+
model.require_modification = true
|
47
|
+
end
|
48
|
+
|
49
|
+
module InstanceMethods
|
50
|
+
# Clear the instance filters after successfully destroying the object.
|
51
|
+
def after_destroy
|
52
|
+
super
|
53
|
+
clear_instance_filters
|
54
|
+
end
|
55
|
+
|
56
|
+
# Clear the instance filters after successfully updating the object.
|
57
|
+
def after_update
|
58
|
+
super
|
59
|
+
clear_instance_filters
|
60
|
+
end
|
61
|
+
|
62
|
+
# Add an instance filter to the array of instance filters
|
63
|
+
# Both the arguments given and the block are passed to the
|
64
|
+
# dataset's filter method.
|
65
|
+
def instance_filter(*args, &block)
|
66
|
+
instance_filters << [args, block]
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# Lazily initialize the instance filter array.
|
72
|
+
def instance_filters
|
73
|
+
@instance_filters ||= []
|
74
|
+
end
|
75
|
+
|
76
|
+
# Apply the instance filters to the given dataset
|
77
|
+
def apply_instance_filters(ds)
|
78
|
+
instance_filters.inject(ds){|ds, i| ds.filter(*i[0], &i[1])}
|
79
|
+
end
|
80
|
+
|
81
|
+
# Clear the instance filters.
|
82
|
+
def clear_instance_filters
|
83
|
+
instance_filters.clear
|
84
|
+
end
|
85
|
+
|
86
|
+
# Apply the instance filters to the dataset returned by super.
|
87
|
+
def _delete_dataset
|
88
|
+
apply_instance_filters(super)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Apply the instance filters to the dataset returned by super.
|
92
|
+
def _update_dataset
|
93
|
+
apply_instance_filters(super)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Sequel
|
2
|
+
module Plugins
|
3
|
+
# The instance_hooks plugin allows you to add hooks to specific instances,
|
4
|
+
# by passing a block to a _hook method (e.g. before_save_hook{do_something}).
|
5
|
+
# The block executed when the hook is called (e.g. before_save).
|
6
|
+
#
|
7
|
+
# All of the standard hooks are supported, except for after_initialize.
|
8
|
+
# Instance level before hooks are executed in reverse order of addition before
|
9
|
+
# calling super. Instance level after hooks are executed in order of addition
|
10
|
+
# after calling super. If any of the instance level before hook blocks return
|
11
|
+
# false, no more instance level before hooks are called and false is returned.
|
12
|
+
#
|
13
|
+
# Instance level hooks are cleared when the object is saved successfully.
|
14
|
+
module InstanceHooks
|
15
|
+
module InstanceMethods
|
16
|
+
HOOKS = Sequel::Model::HOOKS - [:after_initialize]
|
17
|
+
HOOKS.each{|h| class_eval("def #{h}_hook(&block); add_instance_hook(:#{h}, &block) end", __FILE__, __LINE__)}
|
18
|
+
|
19
|
+
BEFORE_HOOKS, AFTER_HOOKS = HOOKS.partition{|hook| hook.to_s =~ /\Abefore_/}
|
20
|
+
BEFORE_HOOKS.each{|h| class_eval("def #{h}; run_instance_hooks(:#{h}) == false ? false : super end", __FILE__, __LINE__)}
|
21
|
+
AFTER_HOOKS.each{|h| class_eval("def #{h}; super; run_instance_hooks(:#{h}) end", __FILE__, __LINE__)}
|
22
|
+
|
23
|
+
# Clear the instance level hooks after saving the object.
|
24
|
+
def after_save
|
25
|
+
super
|
26
|
+
run_instance_hooks(:after_save)
|
27
|
+
@instance_hooks.clear if @instance_hooks
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Add the block as an instance level hook. For before hooks, add it to
|
33
|
+
# the beginning of the instance hook's array. For after hooks, add it
|
34
|
+
# to the end.
|
35
|
+
def add_instance_hook(hook, &block)
|
36
|
+
instance_hooks(hook).send(BEFORE_HOOKS.include?(hook) ? :unshift : :push, block)
|
37
|
+
end
|
38
|
+
|
39
|
+
# An array of instance level hook blocks for the given hook type.
|
40
|
+
def instance_hooks(hook)
|
41
|
+
@instance_hooks ||= {}
|
42
|
+
@instance_hooks[hook] ||= []
|
43
|
+
end
|
44
|
+
|
45
|
+
# Run all hook blocks of the given hook type. If a before hook,
|
46
|
+
# immediately return false if any hook block call returns false.
|
47
|
+
def run_instance_hooks(hook)
|
48
|
+
if BEFORE_HOOKS.include?(hook)
|
49
|
+
instance_hooks(hook).each{|b| return false if b.call == false}
|
50
|
+
else
|
51
|
+
instance_hooks(hook).each{|b| b.call}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,77 @@
|
|
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
|
+
# Lazy attributes requires the identity map and tactical eager loading plugins
|
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
|
+
# Module to store the lazy attribute getter methods, so they can
|
33
|
+
# be overridden and call super to get the lazy attribute behavior
|
34
|
+
attr_accessor :lazy_attributes_module
|
35
|
+
|
36
|
+
# Remove the given attributes from the list of columns selected by default.
|
37
|
+
# For each attribute given, create an accessor method that allows a lazy
|
38
|
+
# lookup of the attribute. Each attribute should be given as a symbol.
|
39
|
+
def lazy_attributes(*attrs)
|
40
|
+
set_dataset(dataset.select(*(columns - attrs)))
|
41
|
+
attrs.each{|a| define_lazy_attribute_getter(a)}
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# Add a lazy attribute getter method to the lazy_attributes_module
|
47
|
+
def define_lazy_attribute_getter(a)
|
48
|
+
include(self.lazy_attributes_module ||= Module.new) unless lazy_attributes_module
|
49
|
+
lazy_attributes_module.class_eval do
|
50
|
+
define_method(a) do
|
51
|
+
if !values.include?(a) && !new?
|
52
|
+
lazy_attribute_lookup(a)
|
53
|
+
else
|
54
|
+
super()
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
module InstanceMethods
|
62
|
+
private
|
63
|
+
|
64
|
+
# If the model was selected with other model objects, eagerly load the
|
65
|
+
# attribute for all of those objects. If not, query the database for
|
66
|
+
# the attribute for just the current object. Return the value of
|
67
|
+
# the attribute for the current object.
|
68
|
+
def lazy_attribute_lookup(a)
|
69
|
+
primary_key = model.primary_key
|
70
|
+
model.select(*(Array(primary_key) + [a])).filter(primary_key=>retrieved_with.map{|o| o.pk}.sql_array).all if model.identity_map && retrieved_with
|
71
|
+
values[a] = this.select(a).first[a] unless values.include?(a)
|
72
|
+
values[a]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,208 @@
|
|
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 default associated key alias(es) to use when eager loading
|
42
|
+
# associations via eager.
|
43
|
+
def default_associated_key_alias
|
44
|
+
self[:uses_left_composite_keys] ? (0...self[:through].first[:left].length).map{|i| :"x_foreign_key_#{i}_x"} : :x_foreign_key_x
|
45
|
+
end
|
46
|
+
|
47
|
+
# The list of joins to use when eager graphing
|
48
|
+
def edges
|
49
|
+
self[:edges] || calculate_edges || self[:edges]
|
50
|
+
end
|
51
|
+
|
52
|
+
# Many through many associations don't have a reciprocal
|
53
|
+
def reciprocal
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
57
|
+
# The list of joins to use when lazy loading or eager loading
|
58
|
+
def reverse_edges
|
59
|
+
self[:reverse_edges] || calculate_edges || self[:reverse_edges]
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# Make sure to use unique table aliases when lazy loading or eager loading
|
65
|
+
def calculate_reverse_edge_aliases(reverse_edges)
|
66
|
+
aliases = [associated_class.table_name]
|
67
|
+
reverse_edges.each do |e|
|
68
|
+
table_alias = e[:table]
|
69
|
+
if aliases.include?(table_alias)
|
70
|
+
i = 0
|
71
|
+
table_alias = loop do
|
72
|
+
ta = :"#{table_alias}_#{i}"
|
73
|
+
break ta unless aliases.include?(ta)
|
74
|
+
i += 1
|
75
|
+
end
|
76
|
+
end
|
77
|
+
aliases.push(e[:alias] = table_alias)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Transform the :through option into a list of edges and reverse edges to use to join tables when loading the association.
|
82
|
+
def calculate_edges
|
83
|
+
es = [{:left_table=>self[:model].table_name, :left_key=>self[:left_primary_key]}]
|
84
|
+
self[:through].each do |t|
|
85
|
+
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])
|
86
|
+
es.last[:only_conditions] = t[:only_conditions] if t.include?(:only_conditions)
|
87
|
+
es << {:left_table=>t[:table], :left_key=>t[:right]}
|
88
|
+
end
|
89
|
+
es.last.merge!(:right_key=>right_primary_key, :right_table=>associated_class.table_name)
|
90
|
+
edges = es.map do |e|
|
91
|
+
h = {:table=>e[:right_table], :left=>e[:left_key], :right=>e[:right_key], :conditions=>e[:conditions], :join_type=>e[:join_type], :block=>e[:block]}
|
92
|
+
h[:only_conditions] = e[:only_conditions] if e.include?(:only_conditions)
|
93
|
+
h
|
94
|
+
end
|
95
|
+
reverse_edges = es.reverse.map{|e| {:table=>e[:left_table], :left=>e[:left_key], :right=>e[:right_key]}}
|
96
|
+
reverse_edges.pop
|
97
|
+
calculate_reverse_edge_aliases(reverse_edges)
|
98
|
+
self[:final_edge] = edges.pop
|
99
|
+
self[:final_reverse_edge] = reverse_edges.pop
|
100
|
+
self[:edges] = edges
|
101
|
+
self[:reverse_edges] = reverse_edges
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
end
|
105
|
+
module ClassMethods
|
106
|
+
# Create a many_through_many association. Arguments:
|
107
|
+
# * name - Same as associate, the name of the association.
|
108
|
+
# * through - The tables and keys to join between the current table and the associated table.
|
109
|
+
# Must be an array, with elements that are either 3 element arrays, or hashes with keys :table, :left, and :right.
|
110
|
+
# The required entries in the array/hash are:
|
111
|
+
# * :table (first array element) - The name of the table to join.
|
112
|
+
# * :left (middle array element) - The key joining the table to the previous table. Can use an
|
113
|
+
# array of symbols for a composite key association.
|
114
|
+
# * :right (last array element) - The key joining the table to the next table. Can use an
|
115
|
+
# array of symbols for a composite key association.
|
116
|
+
# If a hash is provided, the following keys are respected when using eager_graph:
|
117
|
+
# * :block - A proc to use as the block argument to join.
|
118
|
+
# * :conditions - Extra conditions to add to the JOIN ON clause. Must be a hash or array of two pairs.
|
119
|
+
# * :join_type - The join type to use for the join, defaults to :left_outer.
|
120
|
+
# * :only_conditions - Conditions to use for the join instead of the ones specified by the keys.
|
121
|
+
# * opts - The options for the associaion. Takes the same options as associate, and supports these additional options:
|
122
|
+
# * :left_primary_key - column in current table that the first :left option in
|
123
|
+
# through points to, as a symbol. Defaults to primary key of current table. Can use an
|
124
|
+
# array of symbols for a composite key association.
|
125
|
+
# * :right_primary_key - column in associated table that the final :right option in
|
126
|
+
# through points to, as a symbol. Defaults to primary key of the associated table. Can use an
|
127
|
+
# array of symbols for a composite key association.
|
128
|
+
# * :uniq - Adds a after_load callback that makes the array of objects unique.
|
129
|
+
def many_through_many(name, through, opts={}, &block)
|
130
|
+
associate(:many_through_many, name, opts.merge(:through=>through), &block)
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
# Create the association methods and :eager_loader and :eager_grapher procs.
|
136
|
+
def def_many_through_many(opts)
|
137
|
+
name = opts[:name]
|
138
|
+
model = self
|
139
|
+
opts[:read_only] = true
|
140
|
+
opts[:after_load].unshift(:array_uniq!) if opts[:uniq]
|
141
|
+
opts[:cartesian_product_number] ||= 2
|
142
|
+
opts[:through] = opts[:through].map do |e|
|
143
|
+
case e
|
144
|
+
when Array
|
145
|
+
raise(Error, "array elements of the through option/argument for many_through_many associations must have at least three elements") unless e.length == 3
|
146
|
+
{:table=>e[0], :left=>e[1], :right=>e[2]}
|
147
|
+
when Hash
|
148
|
+
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]
|
149
|
+
e
|
150
|
+
else
|
151
|
+
raise(Error, "the through option/argument for many_through_many associations must be an enumerable of arrays or hashes")
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
left_key = opts[:left_key] = opts[:through].first[:left]
|
156
|
+
uses_lcks = opts[:uses_left_composite_keys] = left_key.is_a?(Array)
|
157
|
+
left_keys = Array(left_key)
|
158
|
+
left_pk = (opts[:left_primary_key] ||= self.primary_key)
|
159
|
+
left_pks = opts[:left_primary_keys] = Array(left_pk)
|
160
|
+
opts[:dataset] ||= lambda do
|
161
|
+
ds = opts.associated_class
|
162
|
+
opts.reverse_edges.each{|t| ds = ds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias])}
|
163
|
+
ft = opts[:final_reverse_edge]
|
164
|
+
ds.join(ft[:table], Array(ft[:left]).zip(Array(ft[:right])) + left_keys.zip(left_pks.map{|k| send(k)}), :table_alias=>ft[:alias])
|
165
|
+
end
|
166
|
+
|
167
|
+
left_key_alias = opts[:left_key_alias] ||= opts.default_associated_key_alias
|
168
|
+
opts[:eager_loader] ||= lambda do |key_hash, records, associations|
|
169
|
+
h = key_hash[left_pk]
|
170
|
+
records.each{|object| object.associations[name] = []}
|
171
|
+
ds = opts.associated_class
|
172
|
+
opts.reverse_edges.each{|t| ds = ds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias])}
|
173
|
+
ft = opts[:final_reverse_edge]
|
174
|
+
conds = uses_lcks ? [[left_keys.map{|k| SQL::QualifiedIdentifier.new(ft[:table], k)}, SQL::SQLArray.new(h.keys)]] : [[left_key, h.keys]]
|
175
|
+
ds = ds.join(ft[:table], Array(ft[:left]).zip(Array(ft[:right])) + conds, :table_alias=>ft[:alias])
|
176
|
+
model.eager_loading_dataset(opts, ds, Array(opts.select), associations).all do |assoc_record|
|
177
|
+
hash_key = if uses_lcks
|
178
|
+
left_key_alias.map{|k| assoc_record.values.delete(k)}
|
179
|
+
else
|
180
|
+
assoc_record.values.delete(left_key_alias)
|
181
|
+
end
|
182
|
+
next unless objects = h[hash_key]
|
183
|
+
objects.each{|object| object.associations[name].push(assoc_record)}
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
join_type = opts[:graph_join_type]
|
188
|
+
select = opts[:graph_select]
|
189
|
+
graph_block = opts[:graph_block]
|
190
|
+
only_conditions = opts[:graph_only_conditions]
|
191
|
+
use_only_conditions = opts.include?(:graph_only_conditions)
|
192
|
+
conditions = opts[:graph_conditions]
|
193
|
+
opts[:eager_grapher] ||= proc do |ds, assoc_alias, table_alias|
|
194
|
+
iq = table_alias
|
195
|
+
opts.edges.each do |t|
|
196
|
+
ds = ds.graph(t[:table], t.fetch(:only_conditions, (Array(t[:right]).zip(Array(t[:left])) + t[:conditions])), :select=>false, :table_alias=>ds.unused_table_alias(t[:table]), :join_type=>t[:join_type], :implicit_qualifier=>iq, &t[:block])
|
197
|
+
iq = nil
|
198
|
+
end
|
199
|
+
fe = opts[:final_edge]
|
200
|
+
ds.graph(opts.associated_class, use_only_conditions ? only_conditions : (Array(opts.right_primary_key).zip(Array(fe[:left])) + conditions), :select=>select, :table_alias=>assoc_alias, :join_type=>join_type, &graph_block)
|
201
|
+
end
|
202
|
+
|
203
|
+
def_association_dataset_methods(opts)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,206 @@
|
|
1
|
+
module Sequel
|
2
|
+
module Plugins
|
3
|
+
# The nested_attributes plugin allows you to update attributes for associated
|
4
|
+
# objects directly through the parent object, similar to ActiveRecord's
|
5
|
+
# Nested Attributes feature.
|
6
|
+
#
|
7
|
+
# Nested attributes are created using the nested_attributes method:
|
8
|
+
#
|
9
|
+
# Artist.one_to_many :albums
|
10
|
+
# Artist.nested_attributes :albums
|
11
|
+
# a = Artist.new(:name=>'YJM',
|
12
|
+
# :albums_attributes=>[{:name=>'RF'}, {:name=>'MO'}])
|
13
|
+
# # No database activity yet
|
14
|
+
#
|
15
|
+
# a.save # Saves artist and both albums
|
16
|
+
# a.albums.map{|x| x.name} # ['RF', 'MO']
|
17
|
+
module NestedAttributes
|
18
|
+
# Depend on the instance_hooks plugin.
|
19
|
+
def self.apply(model)
|
20
|
+
model.plugin(:instance_hooks)
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
# Module to store the nested_attributes setter methods, so they can
|
25
|
+
# call be overridden and call super to get the default behavior
|
26
|
+
attr_accessor :nested_attributes_module
|
27
|
+
|
28
|
+
# Allow nested attributes to be set for the given associations. Options:
|
29
|
+
# * :destroy - Allow destruction of nested records.
|
30
|
+
# * :fields - If provided, should be an Array. Restricts the fields allowed to be
|
31
|
+
# modified through the association_attributes= method to the specific fields given.
|
32
|
+
# * :limit - For *_to_many associations, a limit on the number of records
|
33
|
+
# that will be processed, to prevent denial of service attacks.
|
34
|
+
# * :remove - Allow disassociation of nested records (can remove the associated
|
35
|
+
# object from the parent object, but not destroy the associated object).
|
36
|
+
# * :strict - Set to false to not raise an error message if a primary key
|
37
|
+
# is provided in a record, but it doesn't match an existing associated
|
38
|
+
# object.
|
39
|
+
#
|
40
|
+
# If a block is provided, it is passed each nested attribute hash. If
|
41
|
+
# the hash should be ignored, the block should return anything except false or nil.
|
42
|
+
def nested_attributes(*associations, &block)
|
43
|
+
include(self.nested_attributes_module ||= Module.new) unless nested_attributes_module
|
44
|
+
opts = associations.last.is_a?(Hash) ? associations.pop : {}
|
45
|
+
reflections = associations.map{|a| association_reflection(a) || raise(Error, "no association named #{a} for #{self}")}
|
46
|
+
reflections.each do |r|
|
47
|
+
r[:nested_attributes] = opts
|
48
|
+
r[:nested_attributes][:reject_if] ||= block
|
49
|
+
def_nested_attribute_method(r)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# Add a nested attribute setter method to a module included in the
|
56
|
+
# class.
|
57
|
+
def def_nested_attribute_method(reflection)
|
58
|
+
nested_attributes_module.class_eval do
|
59
|
+
if reflection.returns_array?
|
60
|
+
define_method("#{reflection[:name]}_attributes=") do |array|
|
61
|
+
nested_attributes_list_setter(reflection, array)
|
62
|
+
end
|
63
|
+
else
|
64
|
+
define_method("#{reflection[:name]}_attributes=") do |h|
|
65
|
+
nested_attributes_setter(reflection, h)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
module InstanceMethods
|
73
|
+
private
|
74
|
+
|
75
|
+
# Check that the keys related to the association are not modified inside the block. Does
|
76
|
+
# not use an ensure block, so callers should be careful.
|
77
|
+
def nested_attributes_check_key_modifications(reflection, obj)
|
78
|
+
keys = reflection.associated_object_keys.map{|x| obj.send(x)}
|
79
|
+
yield
|
80
|
+
raise(Error, "Modifying association dependent key(s) when updating associated objects is not allowed") unless keys == reflection.associated_object_keys.map{|x| obj.send(x)}
|
81
|
+
end
|
82
|
+
|
83
|
+
# Create a new associated object with the given attributes, validate
|
84
|
+
# it when the parent is validated, and save it when the object is saved.
|
85
|
+
# Returns the object created.
|
86
|
+
def nested_attributes_create(reflection, attributes)
|
87
|
+
obj = reflection.associated_class.new
|
88
|
+
nested_attributes_set_attributes(reflection, obj, attributes)
|
89
|
+
after_validation_hook{validate_associated_object(reflection, obj)}
|
90
|
+
if reflection.returns_array?
|
91
|
+
send(reflection[:name]) << obj
|
92
|
+
after_save_hook{send(reflection.add_method, obj)}
|
93
|
+
else
|
94
|
+
associations[reflection[:name]] = obj
|
95
|
+
# Don't need to validate the object twice if :validate association option is not false
|
96
|
+
# and don't want to validate it at all if it is false.
|
97
|
+
send(reflection[:type] == :many_to_one ? :before_save_hook : :after_save_hook){send(reflection.setter_method, obj.save(:validate=>false))}
|
98
|
+
end
|
99
|
+
obj
|
100
|
+
end
|
101
|
+
|
102
|
+
# Find an associated object with the matching pk. If a matching option
|
103
|
+
# is not found and the :strict option is not false, raise an Error.
|
104
|
+
def nested_attributes_find(reflection, pk)
|
105
|
+
pk = pk.to_s
|
106
|
+
unless obj = Array(associated_objects = send(reflection[:name])).find{|x| x.pk.to_s == pk}
|
107
|
+
raise(Error, "no matching associated object with given primary key (association: #{reflection[:name]}, pk: #{pk})") unless reflection[:nested_attributes][:strict] == false
|
108
|
+
end
|
109
|
+
obj
|
110
|
+
end
|
111
|
+
|
112
|
+
# Take an array or hash of attribute hashes and set each one individually.
|
113
|
+
# If a hash is provided it, sort it by key and then use the values.
|
114
|
+
# If there is a limit on the nested attributes for this association,
|
115
|
+
# make sure the length of the attributes_list is not greater than the limit.
|
116
|
+
def nested_attributes_list_setter(reflection, attributes_list)
|
117
|
+
attributes_list = attributes_list.sort_by{|x| x.to_s}.map{|k,v| v} if attributes_list.is_a?(Hash)
|
118
|
+
if (limit = reflection[:nested_attributes][:limit]) && attributes_list.length > limit
|
119
|
+
raise(Error, "number of nested attributes (#{attributes_list.length}) exceeds the limit (#{limit})")
|
120
|
+
end
|
121
|
+
attributes_list.each{|a| nested_attributes_setter(reflection, a)}
|
122
|
+
end
|
123
|
+
|
124
|
+
# Remove the matching associated object from the current object.
|
125
|
+
# If the :destroy option is given, destroy the object after disassociating it.
|
126
|
+
# Returns the object removed, if it exists.
|
127
|
+
def nested_attributes_remove(reflection, pk, opts={})
|
128
|
+
if obj = nested_attributes_find(reflection, pk)
|
129
|
+
before_save_hook do
|
130
|
+
if reflection.returns_array?
|
131
|
+
send(reflection.remove_method, obj)
|
132
|
+
else
|
133
|
+
send(reflection.setter_method, nil)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
after_save_hook{obj.destroy} if opts[:destroy]
|
137
|
+
obj
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Set the fields in the obj based on the association, only allowing
|
142
|
+
# specific :fields if configured.
|
143
|
+
def nested_attributes_set_attributes(reflection, obj, attributes)
|
144
|
+
if fields = reflection[:nested_attributes][:fields]
|
145
|
+
obj.set_only(attributes, fields)
|
146
|
+
else
|
147
|
+
obj.set(attributes)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Modify the associated object based on the contents of the attribtues hash:
|
152
|
+
# * If a block was given to nested_attributes, call it with the attributes and return immediately if the block returns true.
|
153
|
+
# * If no primary key exists in the attributes hash, create a new object.
|
154
|
+
# * If _delete is a key in the hash and the :destroy option is used, destroy the matching associated object.
|
155
|
+
# * If _remove is a key in the hash and the :remove option is used, disassociated the matching associated object.
|
156
|
+
# * Otherwise, update the matching associated object with the contents of the hash.
|
157
|
+
def nested_attributes_setter(reflection, attributes)
|
158
|
+
return if (b = reflection[:nested_attributes][:reject_if]) && b.call(attributes)
|
159
|
+
modified!
|
160
|
+
klass = reflection.associated_class
|
161
|
+
if pk = attributes.delete(klass.primary_key) || attributes.delete(klass.primary_key.to_s)
|
162
|
+
if klass.db.send(:typecast_value_boolean, attributes[:_delete] || attributes['_delete']) && reflection[:nested_attributes][:destroy]
|
163
|
+
nested_attributes_remove(reflection, pk, :destroy=>true)
|
164
|
+
elsif klass.db.send(:typecast_value_boolean, attributes[:_remove] || attributes['_remove']) && reflection[:nested_attributes][:remove]
|
165
|
+
nested_attributes_remove(reflection, pk)
|
166
|
+
else
|
167
|
+
nested_attributes_update(reflection, pk, attributes)
|
168
|
+
end
|
169
|
+
else
|
170
|
+
nested_attributes_create(reflection, attributes)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Update the matching associated object with the attributes,
|
175
|
+
# validating it when the parent object is validated and saving it
|
176
|
+
# when the parent is saved.
|
177
|
+
# Returns the object updated, if it exists.
|
178
|
+
def nested_attributes_update(reflection, pk, attributes)
|
179
|
+
if obj = nested_attributes_find(reflection, pk)
|
180
|
+
nested_attributes_update_attributes(reflection, obj, attributes)
|
181
|
+
after_validation_hook{validate_associated_object(reflection, obj)}
|
182
|
+
# Don't need to validate the object twice if :validate association option is not false
|
183
|
+
# and don't want to validate it at all if it is false.
|
184
|
+
after_save_hook{obj.save_changes(:validate=>false)}
|
185
|
+
obj
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Update the attributes for the given object related to the current object through the association.
|
190
|
+
def nested_attributes_update_attributes(reflection, obj, attributes)
|
191
|
+
nested_attributes_check_key_modifications(reflection, obj) do
|
192
|
+
nested_attributes_set_attributes(reflection, obj, attributes)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Validate the given associated object, adding any validation error messages from the
|
197
|
+
# given object to the parent object.
|
198
|
+
def validate_associated_object(reflection, obj)
|
199
|
+
return if reflection[:validate] == false
|
200
|
+
association = reflection[:name]
|
201
|
+
obj.errors.full_messages.each{|m| errors.add(association, m)} unless obj.valid?
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|