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