duck_record 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +59 -0
- data/Rakefile +29 -0
- data/lib/duck_record/attribute/user_provided_default.rb +30 -0
- data/lib/duck_record/attribute.rb +221 -0
- data/lib/duck_record/attribute_assignment.rb +91 -0
- data/lib/duck_record/attribute_methods/before_type_cast.rb +76 -0
- data/lib/duck_record/attribute_methods/dirty.rb +124 -0
- data/lib/duck_record/attribute_methods/read.rb +78 -0
- data/lib/duck_record/attribute_methods/write.rb +65 -0
- data/lib/duck_record/attribute_methods.rb +332 -0
- data/lib/duck_record/attribute_mutation_tracker.rb +113 -0
- data/lib/duck_record/attribute_set/builder.rb +124 -0
- data/lib/duck_record/attribute_set/yaml_encoder.rb +41 -0
- data/lib/duck_record/attribute_set.rb +99 -0
- data/lib/duck_record/attributes.rb +262 -0
- data/lib/duck_record/base.rb +296 -0
- data/lib/duck_record/callbacks.rb +324 -0
- data/lib/duck_record/core.rb +253 -0
- data/lib/duck_record/define_callbacks.rb +23 -0
- data/lib/duck_record/errors.rb +44 -0
- data/lib/duck_record/inheritance.rb +130 -0
- data/lib/duck_record/locale/en.yml +48 -0
- data/lib/duck_record/model_schema.rb +64 -0
- data/lib/duck_record/serialization.rb +19 -0
- data/lib/duck_record/translation.rb +22 -0
- data/lib/duck_record/type/array.rb +36 -0
- data/lib/duck_record/type/decimal_without_scale.rb +13 -0
- data/lib/duck_record/type/internal/abstract_json.rb +33 -0
- data/lib/duck_record/type/json.rb +6 -0
- data/lib/duck_record/type/registry.rb +97 -0
- data/lib/duck_record/type/serialized.rb +63 -0
- data/lib/duck_record/type/text.rb +9 -0
- data/lib/duck_record/type/unsigned_integer.rb +15 -0
- data/lib/duck_record/type.rb +66 -0
- data/lib/duck_record/validations.rb +40 -0
- data/lib/duck_record/version.rb +3 -0
- data/lib/duck_record.rb +47 -0
- data/lib/tasks/acts_as_record_tasks.rake +4 -0
- metadata +126 -0
@@ -0,0 +1,296 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'active_support/benchmarkable'
|
3
|
+
require 'active_support/dependencies'
|
4
|
+
require 'active_support/descendants_tracker'
|
5
|
+
require 'active_support/time'
|
6
|
+
require 'active_support/core_ext/module/attribute_accessors'
|
7
|
+
require 'active_support/core_ext/array/extract_options'
|
8
|
+
require 'active_support/core_ext/hash/deep_merge'
|
9
|
+
require 'active_support/core_ext/hash/slice'
|
10
|
+
require 'active_support/core_ext/hash/transform_values'
|
11
|
+
require 'active_support/core_ext/string/behavior'
|
12
|
+
require 'active_support/core_ext/kernel/singleton_class'
|
13
|
+
require 'active_support/core_ext/module/introspection'
|
14
|
+
require 'active_support/core_ext/object/duplicable'
|
15
|
+
require 'active_support/core_ext/class/subclasses'
|
16
|
+
require 'duck_record/define_callbacks'
|
17
|
+
require 'duck_record/errors'
|
18
|
+
require 'duck_record/attributes'
|
19
|
+
|
20
|
+
module DuckRecord #:nodoc:
|
21
|
+
# = Active Record
|
22
|
+
#
|
23
|
+
# Active Record objects don't specify their attributes directly, but rather infer them from
|
24
|
+
# the table definition with which they're linked. Adding, removing, and changing attributes
|
25
|
+
# and their type is done directly in the database. Any change is instantly reflected in the
|
26
|
+
# Active Record objects. The mapping that binds a given Active Record class to a certain
|
27
|
+
# database table will happen automatically in most common cases, but can be overwritten for the uncommon ones.
|
28
|
+
#
|
29
|
+
# See the mapping rules in table_name and the full example in link:files/activerecord/README_rdoc.html for more insight.
|
30
|
+
#
|
31
|
+
# == Creation
|
32
|
+
#
|
33
|
+
# Active Records accept constructor parameters either in a hash or as a block. The hash
|
34
|
+
# method is especially useful when you're receiving the data from somewhere else, like an
|
35
|
+
# HTTP request. It works like this:
|
36
|
+
#
|
37
|
+
# user = User.new(name: 'David', occupation: 'Code Artist')
|
38
|
+
# user.name # => 'David'
|
39
|
+
#
|
40
|
+
# You can also use block initialization:
|
41
|
+
#
|
42
|
+
# user = User.new do |u|
|
43
|
+
# u.name = 'David'
|
44
|
+
# u.occupation = 'Code Artist'
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# And of course you can just create a bare object and specify the attributes after the fact:
|
48
|
+
#
|
49
|
+
# user = User.new
|
50
|
+
# user.name = 'David'
|
51
|
+
# user.occupation = 'Code Artist'
|
52
|
+
#
|
53
|
+
# == Conditions
|
54
|
+
#
|
55
|
+
# Conditions can either be specified as a string, array, or hash representing the WHERE-part of an SQL statement.
|
56
|
+
# The array form is to be used when the condition input is tainted and requires sanitization. The string form can
|
57
|
+
# be used for statements that don't involve tainted data. The hash form works much like the array form, except
|
58
|
+
# only equality and range is possible. Examples:
|
59
|
+
#
|
60
|
+
# class User < DuckRecord::Base
|
61
|
+
# def self.authenticate_unsafely(user_name, password)
|
62
|
+
# where('user_name = '#{user_name}' AND password = '#{password}'').first
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# def self.authenticate_safely(user_name, password)
|
66
|
+
# where('user_name = ? AND password = ?', user_name, password).first
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# def self.authenticate_safely_simply(user_name, password)
|
70
|
+
# where(user_name: user_name, password: password).first
|
71
|
+
# end
|
72
|
+
# end
|
73
|
+
#
|
74
|
+
# The <tt>authenticate_unsafely</tt> method inserts the parameters directly into the query
|
75
|
+
# and is thus susceptible to SQL-injection attacks if the <tt>user_name</tt> and +password+
|
76
|
+
# parameters come directly from an HTTP request. The <tt>authenticate_safely</tt> and
|
77
|
+
# <tt>authenticate_safely_simply</tt> both will sanitize the <tt>user_name</tt> and +password+
|
78
|
+
# before inserting them in the query, which will ensure that an attacker can't escape the
|
79
|
+
# query and fake the login (or worse).
|
80
|
+
#
|
81
|
+
# When using multiple parameters in the conditions, it can easily become hard to read exactly
|
82
|
+
# what the fourth or fifth question mark is supposed to represent. In those cases, you can
|
83
|
+
# resort to named bind variables instead. That's done by replacing the question marks with
|
84
|
+
# symbols and supplying a hash with values for the matching symbol keys:
|
85
|
+
#
|
86
|
+
# Company.where(
|
87
|
+
# 'id = :id AND name = :name AND division = :division AND created_at > :accounting_date',
|
88
|
+
# { id: 3, name: '37signals', division: 'First', accounting_date: '2005-01-01' }
|
89
|
+
# ).first
|
90
|
+
#
|
91
|
+
# Similarly, a simple hash without a statement will generate conditions based on equality with the SQL AND
|
92
|
+
# operator. For instance:
|
93
|
+
#
|
94
|
+
# Student.where(first_name: 'Harvey', status: 1)
|
95
|
+
# Student.where(params[:student])
|
96
|
+
#
|
97
|
+
# A range may be used in the hash to use the SQL BETWEEN operator:
|
98
|
+
#
|
99
|
+
# Student.where(grade: 9..12)
|
100
|
+
#
|
101
|
+
# An array may be used in the hash to use the SQL IN operator:
|
102
|
+
#
|
103
|
+
# Student.where(grade: [9,11,12])
|
104
|
+
#
|
105
|
+
# When joining tables, nested hashes or keys written in the form 'table_name.column_name'
|
106
|
+
# can be used to qualify the table name of a particular condition. For instance:
|
107
|
+
#
|
108
|
+
# Student.joins(:schools).where(schools: { category: 'public' })
|
109
|
+
# Student.joins(:schools).where('schools.category' => 'public' )
|
110
|
+
#
|
111
|
+
# == Overwriting default accessors
|
112
|
+
#
|
113
|
+
# All column values are automatically available through basic accessors on the Active Record
|
114
|
+
# object, but sometimes you want to specialize this behavior. This can be done by overwriting
|
115
|
+
# the default accessors (using the same name as the attribute) and calling
|
116
|
+
# +super+ to actually change things.
|
117
|
+
#
|
118
|
+
# class Song < DuckRecord::Base
|
119
|
+
# # Uses an integer of seconds to hold the length of the song
|
120
|
+
#
|
121
|
+
# def length=(minutes)
|
122
|
+
# super(minutes.to_i * 60)
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
# def length
|
126
|
+
# super / 60
|
127
|
+
# end
|
128
|
+
# end
|
129
|
+
#
|
130
|
+
# == Attribute query methods
|
131
|
+
#
|
132
|
+
# In addition to the basic accessors, query methods are also automatically available on the Active Record object.
|
133
|
+
# Query methods allow you to test whether an attribute value is present.
|
134
|
+
# Additionally, when dealing with numeric values, a query method will return false if the value is zero.
|
135
|
+
#
|
136
|
+
# For example, an Active Record User with the <tt>name</tt> attribute has a <tt>name?</tt> method that you can call
|
137
|
+
# to determine whether the user has a name:
|
138
|
+
#
|
139
|
+
# user = User.new(name: 'David')
|
140
|
+
# user.name? # => true
|
141
|
+
#
|
142
|
+
# anonymous = User.new(name: '')
|
143
|
+
# anonymous.name? # => false
|
144
|
+
#
|
145
|
+
# == Accessing attributes before they have been typecasted
|
146
|
+
#
|
147
|
+
# Sometimes you want to be able to read the raw attribute data without having the column-determined
|
148
|
+
# typecast run its course first. That can be done by using the <tt><attribute>_before_type_cast</tt>
|
149
|
+
# accessors that all attributes have. For example, if your Account model has a <tt>balance</tt> attribute,
|
150
|
+
# you can call <tt>account.balance_before_type_cast</tt> or <tt>account.id_before_type_cast</tt>.
|
151
|
+
#
|
152
|
+
# This is especially useful in validation situations where the user might supply a string for an
|
153
|
+
# integer field and you want to display the original string back in an error message. Accessing the
|
154
|
+
# attribute normally would typecast the string to 0, which isn't what you want.
|
155
|
+
#
|
156
|
+
# == Dynamic attribute-based finders
|
157
|
+
#
|
158
|
+
# Dynamic attribute-based finders are a mildly deprecated way of getting (and/or creating) objects
|
159
|
+
# by simple queries without turning to SQL. They work by appending the name of an attribute
|
160
|
+
# to <tt>find_by_</tt> like <tt>Person.find_by_user_name</tt>.
|
161
|
+
# Instead of writing <tt>Person.find_by(user_name: user_name)</tt>, you can use
|
162
|
+
# <tt>Person.find_by_user_name(user_name)</tt>.
|
163
|
+
#
|
164
|
+
# It's possible to add an exclamation point (!) on the end of the dynamic finders to get them to raise an
|
165
|
+
# DuckRecord::RecordNotFound error if they do not return any records,
|
166
|
+
# like <tt>Person.find_by_last_name!</tt>.
|
167
|
+
#
|
168
|
+
# It's also possible to use multiple attributes in the same <tt>find_by_</tt> by separating them with
|
169
|
+
# '_and_'.
|
170
|
+
#
|
171
|
+
# Person.find_by(user_name: user_name, password: password)
|
172
|
+
# Person.find_by_user_name_and_password(user_name, password) # with dynamic finder
|
173
|
+
#
|
174
|
+
# It's even possible to call these dynamic finder methods on relations and named scopes.
|
175
|
+
#
|
176
|
+
# Payment.order('created_on').find_by_amount(50)
|
177
|
+
#
|
178
|
+
# == Saving arrays, hashes, and other non-mappable objects in text columns
|
179
|
+
#
|
180
|
+
# Active Record can serialize any object in text columns using YAML. To do so, you must
|
181
|
+
# specify this with a call to the class method
|
182
|
+
# {serialize}[rdoc-ref:AttributeMethods::Serialization::ClassMethods#serialize].
|
183
|
+
# This makes it possible to store arrays, hashes, and other non-mappable objects without doing
|
184
|
+
# any additional work.
|
185
|
+
#
|
186
|
+
# class User < DuckRecord::Base
|
187
|
+
# serialize :preferences
|
188
|
+
# end
|
189
|
+
#
|
190
|
+
# user = User.create(preferences: { 'background' => 'black', 'display' => large })
|
191
|
+
# User.find(user.id).preferences # => { 'background' => 'black', 'display' => large }
|
192
|
+
#
|
193
|
+
# You can also specify a class option as the second parameter that'll raise an exception
|
194
|
+
# if a serialized object is retrieved as a descendant of a class not in the hierarchy.
|
195
|
+
#
|
196
|
+
# class User < DuckRecord::Base
|
197
|
+
# serialize :preferences, Hash
|
198
|
+
# end
|
199
|
+
#
|
200
|
+
# user = User.create(preferences: %w( one two three ))
|
201
|
+
# User.find(user.id).preferences # raises SerializationTypeMismatch
|
202
|
+
#
|
203
|
+
# When you specify a class option, the default value for that attribute will be a new
|
204
|
+
# instance of that class.
|
205
|
+
#
|
206
|
+
# class User < DuckRecord::Base
|
207
|
+
# serialize :preferences, OpenStruct
|
208
|
+
# end
|
209
|
+
#
|
210
|
+
# user = User.new
|
211
|
+
# user.preferences.theme_color = 'red'
|
212
|
+
#
|
213
|
+
#
|
214
|
+
# == Single table inheritance
|
215
|
+
#
|
216
|
+
# Active Record allows inheritance by storing the name of the class in a
|
217
|
+
# column that is named 'type' by default. See DuckRecord::Inheritance for
|
218
|
+
# more details.
|
219
|
+
#
|
220
|
+
# == Connection to multiple databases in different models
|
221
|
+
#
|
222
|
+
# Connections are usually created through
|
223
|
+
# {DuckRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection] and retrieved
|
224
|
+
# by DuckRecord::Base.connection. All classes inheriting from DuckRecord::Base will use this
|
225
|
+
# connection. But you can also set a class-specific connection. For example, if Course is an
|
226
|
+
# DuckRecord::Base, but resides in a different database, you can just say <tt>Course.establish_connection</tt>
|
227
|
+
# and Course and all of its subclasses will use this connection instead.
|
228
|
+
#
|
229
|
+
# This feature is implemented by keeping a connection pool in DuckRecord::Base that is
|
230
|
+
# a hash indexed by the class. If a connection is requested, the
|
231
|
+
# {DuckRecord::Base.retrieve_connection}[rdoc-ref:ConnectionHandling#retrieve_connection] method
|
232
|
+
# will go up the class-hierarchy until a connection is found in the connection pool.
|
233
|
+
#
|
234
|
+
# == Exceptions
|
235
|
+
#
|
236
|
+
# * DuckRecordError - Generic error class and superclass of all other errors raised by Active Record.
|
237
|
+
# * AdapterNotSpecified - The configuration hash used in
|
238
|
+
# {DuckRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection]
|
239
|
+
# didn't include an <tt>:adapter</tt> key.
|
240
|
+
# * AdapterNotFound - The <tt>:adapter</tt> key used in
|
241
|
+
# {DuckRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection]
|
242
|
+
# specified a non-existent adapter
|
243
|
+
# (or a bad spelling of an existing one).
|
244
|
+
# * AssociationTypeMismatch - The object assigned to the association wasn't of the type
|
245
|
+
# specified in the association definition.
|
246
|
+
# * AttributeAssignmentError - An error occurred while doing a mass assignment through the
|
247
|
+
# {DuckRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method.
|
248
|
+
# You can inspect the +attribute+ property of the exception object to determine which attribute
|
249
|
+
# triggered the error.
|
250
|
+
# * ConnectionNotEstablished - No connection has been established.
|
251
|
+
# Use {DuckRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection] before querying.
|
252
|
+
# * MultiparameterAssignmentErrors - Collection of errors that occurred during a mass assignment using the
|
253
|
+
# {DuckRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method.
|
254
|
+
# The +errors+ property of this exception contains an array of
|
255
|
+
# AttributeAssignmentError
|
256
|
+
# objects that should be inspected to determine which attributes triggered the errors.
|
257
|
+
# * RecordInvalid - raised by {DuckRecord::Base#save!}[rdoc-ref:Persistence#save!] and
|
258
|
+
# {DuckRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!]
|
259
|
+
# when the record is invalid.
|
260
|
+
# * RecordNotFound - No record responded to the {DuckRecord::Base.find}[rdoc-ref:FinderMethods#find] method.
|
261
|
+
# Either the row with the given ID doesn't exist or the row didn't meet the additional restrictions.
|
262
|
+
# Some {DuckRecord::Base.find}[rdoc-ref:FinderMethods#find] calls do not raise this exception to signal
|
263
|
+
# nothing was found, please check its documentation for further details.
|
264
|
+
# * SerializationTypeMismatch - The serialized object wasn't of the class specified as the second parameter.
|
265
|
+
# * StatementInvalid - The database server rejected the SQL statement. The precise error is added in the message.
|
266
|
+
#
|
267
|
+
# *Note*: The attributes listed are class-level attributes (accessible from both the class and instance level).
|
268
|
+
# So it's possible to assign a logger to the class through <tt>Base.logger=</tt> which will then be used by all
|
269
|
+
# instances in the current object space.
|
270
|
+
class Base
|
271
|
+
extend ActiveModel::Naming
|
272
|
+
|
273
|
+
extend ActiveSupport::Benchmarkable
|
274
|
+
extend ActiveSupport::DescendantsTracker
|
275
|
+
|
276
|
+
extend Translation
|
277
|
+
|
278
|
+
include Core
|
279
|
+
include ModelSchema
|
280
|
+
include Inheritance
|
281
|
+
include AttributeAssignment
|
282
|
+
include ActiveModel::Conversion
|
283
|
+
include Validations
|
284
|
+
include Attributes
|
285
|
+
include DefineCallbacks
|
286
|
+
include AttributeMethods
|
287
|
+
include Callbacks
|
288
|
+
include Serialization
|
289
|
+
|
290
|
+
def persisted?
|
291
|
+
false
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
ActiveSupport.run_load_hooks(:duck_record, Base)
|
296
|
+
end
|
@@ -0,0 +1,324 @@
|
|
1
|
+
module DuckRecord
|
2
|
+
# = Active Record \Callbacks
|
3
|
+
#
|
4
|
+
# \Callbacks are hooks into the life cycle of an Active Record object that allow you to trigger logic
|
5
|
+
# before or after an alteration of the object state. This can be used to make sure that associated and
|
6
|
+
# dependent objects are deleted when {DuckRecord::Base#destroy}[rdoc-ref:Persistence#destroy] is called (by overwriting +before_destroy+) or
|
7
|
+
# to massage attributes before they're validated (by overwriting +before_validation+).
|
8
|
+
# As an example of the callbacks initiated, consider the {DuckRecord::Base#save}[rdoc-ref:Persistence#save] call for a new record:
|
9
|
+
#
|
10
|
+
# * (-) <tt>save</tt>
|
11
|
+
# * (-) <tt>valid</tt>
|
12
|
+
# * (1) <tt>before_validation</tt>
|
13
|
+
# * (-) <tt>validate</tt>
|
14
|
+
# * (2) <tt>after_validation</tt>
|
15
|
+
# * (3) <tt>before_save</tt>
|
16
|
+
# * (4) <tt>before_create</tt>
|
17
|
+
# * (-) <tt>create</tt>
|
18
|
+
# * (5) <tt>after_create</tt>
|
19
|
+
# * (6) <tt>after_save</tt>
|
20
|
+
# * (7) <tt>after_commit</tt>
|
21
|
+
#
|
22
|
+
# Also, an <tt>after_rollback</tt> callback can be configured to be triggered whenever a rollback is issued.
|
23
|
+
# Check out DuckRecord::Transactions for more details about <tt>after_commit</tt> and
|
24
|
+
# <tt>after_rollback</tt>.
|
25
|
+
#
|
26
|
+
# Additionally, an <tt>after_touch</tt> callback is triggered whenever an
|
27
|
+
# object is touched.
|
28
|
+
#
|
29
|
+
# Lastly an <tt>after_find</tt> and <tt>after_initialize</tt> callback is triggered for each object that
|
30
|
+
# is found and instantiated by a finder, with <tt>after_initialize</tt> being triggered after new objects
|
31
|
+
# are instantiated as well.
|
32
|
+
#
|
33
|
+
# There are nineteen callbacks in total, which give you immense power to react and prepare for each state in the
|
34
|
+
# Active Record life cycle. The sequence for calling {DuckRecord::Base#save}[rdoc-ref:Persistence#save] for an existing record is similar,
|
35
|
+
# except that each <tt>_create</tt> callback is replaced by the corresponding <tt>_update</tt> callback.
|
36
|
+
#
|
37
|
+
# Examples:
|
38
|
+
# class CreditCard < DuckRecord::Base
|
39
|
+
# # Strip everything but digits, so the user can specify "555 234 34" or
|
40
|
+
# # "5552-3434" and both will mean "55523434"
|
41
|
+
# before_validation(on: :create) do
|
42
|
+
# self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number")
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# class Subscription < DuckRecord::Base
|
47
|
+
# before_create :record_signup
|
48
|
+
#
|
49
|
+
# private
|
50
|
+
# def record_signup
|
51
|
+
# self.signed_up_on = Date.today
|
52
|
+
# end
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# class Firm < DuckRecord::Base
|
56
|
+
# # Disables access to the system, for associated clients and people when the firm is destroyed
|
57
|
+
# before_destroy { |record| Person.where(firm_id: record.id).update_all(access: 'disabled') }
|
58
|
+
# before_destroy { |record| Client.where(client_of: record.id).update_all(access: 'disabled') }
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# == Inheritable callback queues
|
62
|
+
#
|
63
|
+
# Besides the overwritable callback methods, it's also possible to register callbacks through the
|
64
|
+
# use of the callback macros. Their main advantage is that the macros add behavior into a callback
|
65
|
+
# queue that is kept intact down through an inheritance hierarchy.
|
66
|
+
#
|
67
|
+
# class Topic < DuckRecord::Base
|
68
|
+
# before_destroy :destroy_author
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# class Reply < Topic
|
72
|
+
# before_destroy :destroy_readers
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# Now, when <tt>Topic#destroy</tt> is run only +destroy_author+ is called. When <tt>Reply#destroy</tt> is
|
76
|
+
# run, both +destroy_author+ and +destroy_readers+ are called. Contrast this to the following situation
|
77
|
+
# where the +before_destroy+ method is overridden:
|
78
|
+
#
|
79
|
+
# class Topic < DuckRecord::Base
|
80
|
+
# def before_destroy() destroy_author end
|
81
|
+
# end
|
82
|
+
#
|
83
|
+
# class Reply < Topic
|
84
|
+
# def before_destroy() destroy_readers end
|
85
|
+
# end
|
86
|
+
#
|
87
|
+
# In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+.
|
88
|
+
# So, use the callback macros when you want to ensure that a certain callback is called for the entire
|
89
|
+
# hierarchy, and use the regular overwritable methods when you want to leave it up to each descendant
|
90
|
+
# to decide whether they want to call +super+ and trigger the inherited callbacks.
|
91
|
+
#
|
92
|
+
# *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the
|
93
|
+
# callbacks before specifying the associations. Otherwise, you might trigger the loading of a
|
94
|
+
# child before the parent has registered the callbacks and they won't be inherited.
|
95
|
+
#
|
96
|
+
# == Types of callbacks
|
97
|
+
#
|
98
|
+
# There are four types of callbacks accepted by the callback macros: Method references (symbol), callback objects,
|
99
|
+
# inline methods (using a proc), and inline eval methods (using a string). Method references and callback objects
|
100
|
+
# are the recommended approaches, inline methods using a proc are sometimes appropriate (such as for
|
101
|
+
# creating mix-ins), and inline eval methods are deprecated.
|
102
|
+
#
|
103
|
+
# The method reference callbacks work by specifying a protected or private method available in the object, like this:
|
104
|
+
#
|
105
|
+
# class Topic < DuckRecord::Base
|
106
|
+
# before_destroy :delete_parents
|
107
|
+
#
|
108
|
+
# private
|
109
|
+
# def delete_parents
|
110
|
+
# self.class.delete_all "parent_id = #{id}"
|
111
|
+
# end
|
112
|
+
# end
|
113
|
+
#
|
114
|
+
# The callback objects have methods named after the callback called with the record as the only parameter, such as:
|
115
|
+
#
|
116
|
+
# class BankAccount < DuckRecord::Base
|
117
|
+
# before_save EncryptionWrapper.new
|
118
|
+
# after_save EncryptionWrapper.new
|
119
|
+
# after_initialize EncryptionWrapper.new
|
120
|
+
# end
|
121
|
+
#
|
122
|
+
# class EncryptionWrapper
|
123
|
+
# def before_save(record)
|
124
|
+
# record.credit_card_number = encrypt(record.credit_card_number)
|
125
|
+
# end
|
126
|
+
#
|
127
|
+
# def after_save(record)
|
128
|
+
# record.credit_card_number = decrypt(record.credit_card_number)
|
129
|
+
# end
|
130
|
+
#
|
131
|
+
# alias_method :after_initialize, :after_save
|
132
|
+
#
|
133
|
+
# private
|
134
|
+
# def encrypt(value)
|
135
|
+
# # Secrecy is committed
|
136
|
+
# end
|
137
|
+
#
|
138
|
+
# def decrypt(value)
|
139
|
+
# # Secrecy is unveiled
|
140
|
+
# end
|
141
|
+
# end
|
142
|
+
#
|
143
|
+
# So you specify the object you want messaged on a given callback. When that callback is triggered, the object has
|
144
|
+
# a method by the name of the callback messaged. You can make these callbacks more flexible by passing in other
|
145
|
+
# initialization data such as the name of the attribute to work with:
|
146
|
+
#
|
147
|
+
# class BankAccount < DuckRecord::Base
|
148
|
+
# before_save EncryptionWrapper.new("credit_card_number")
|
149
|
+
# after_save EncryptionWrapper.new("credit_card_number")
|
150
|
+
# after_initialize EncryptionWrapper.new("credit_card_number")
|
151
|
+
# end
|
152
|
+
#
|
153
|
+
# class EncryptionWrapper
|
154
|
+
# def initialize(attribute)
|
155
|
+
# @attribute = attribute
|
156
|
+
# end
|
157
|
+
#
|
158
|
+
# def before_save(record)
|
159
|
+
# record.send("#{@attribute}=", encrypt(record.send("#{@attribute}")))
|
160
|
+
# end
|
161
|
+
#
|
162
|
+
# def after_save(record)
|
163
|
+
# record.send("#{@attribute}=", decrypt(record.send("#{@attribute}")))
|
164
|
+
# end
|
165
|
+
#
|
166
|
+
# alias_method :after_initialize, :after_save
|
167
|
+
#
|
168
|
+
# private
|
169
|
+
# def encrypt(value)
|
170
|
+
# # Secrecy is committed
|
171
|
+
# end
|
172
|
+
#
|
173
|
+
# def decrypt(value)
|
174
|
+
# # Secrecy is unveiled
|
175
|
+
# end
|
176
|
+
# end
|
177
|
+
#
|
178
|
+
# == <tt>before_validation*</tt> returning statements
|
179
|
+
#
|
180
|
+
# If the +before_validation+ callback throws +:abort+, the process will be
|
181
|
+
# aborted and {DuckRecord::Base#save}[rdoc-ref:Persistence#save] will return +false+.
|
182
|
+
# If {DuckRecord::Base#save!}[rdoc-ref:Persistence#save!] is called it will raise an DuckRecord::RecordInvalid exception.
|
183
|
+
# Nothing will be appended to the errors object.
|
184
|
+
#
|
185
|
+
# == Canceling callbacks
|
186
|
+
#
|
187
|
+
# If a <tt>before_*</tt> callback throws +:abort+, all the later callbacks and
|
188
|
+
# the associated action are cancelled.
|
189
|
+
# Callbacks are generally run in the order they are defined, with the exception of callbacks defined as
|
190
|
+
# methods on the model, which are called last.
|
191
|
+
#
|
192
|
+
# == Ordering callbacks
|
193
|
+
#
|
194
|
+
# Sometimes the code needs that the callbacks execute in a specific order. For example, a +before_destroy+
|
195
|
+
# callback (+log_children+ in this case) should be executed before the children get destroyed by the
|
196
|
+
# <tt>dependent: :destroy</tt> option.
|
197
|
+
#
|
198
|
+
# Let's look at the code below:
|
199
|
+
#
|
200
|
+
# class Topic < DuckRecord::Base
|
201
|
+
# has_many :children, dependent: :destroy
|
202
|
+
#
|
203
|
+
# before_destroy :log_children
|
204
|
+
#
|
205
|
+
# private
|
206
|
+
# def log_children
|
207
|
+
# # Child processing
|
208
|
+
# end
|
209
|
+
# end
|
210
|
+
#
|
211
|
+
# In this case, the problem is that when the +before_destroy+ callback is executed, the children are not available
|
212
|
+
# because the {DuckRecord::Base#destroy}[rdoc-ref:Persistence#destroy] callback gets executed first.
|
213
|
+
# You can use the +prepend+ option on the +before_destroy+ callback to avoid this.
|
214
|
+
#
|
215
|
+
# class Topic < DuckRecord::Base
|
216
|
+
# has_many :children, dependent: :destroy
|
217
|
+
#
|
218
|
+
# before_destroy :log_children, prepend: true
|
219
|
+
#
|
220
|
+
# private
|
221
|
+
# def log_children
|
222
|
+
# # Child processing
|
223
|
+
# end
|
224
|
+
# end
|
225
|
+
#
|
226
|
+
# This way, the +before_destroy+ gets executed before the <tt>dependent: :destroy</tt> is called, and the data is still available.
|
227
|
+
#
|
228
|
+
# Also, there are cases when you want several callbacks of the same type to
|
229
|
+
# be executed in order.
|
230
|
+
#
|
231
|
+
# For example:
|
232
|
+
#
|
233
|
+
# class Topic
|
234
|
+
# has_many :children
|
235
|
+
#
|
236
|
+
# after_save :log_children
|
237
|
+
# after_save :do_something_else
|
238
|
+
#
|
239
|
+
# private
|
240
|
+
#
|
241
|
+
# def log_chidren
|
242
|
+
# # Child processing
|
243
|
+
# end
|
244
|
+
#
|
245
|
+
# def do_something_else
|
246
|
+
# # Something else
|
247
|
+
# end
|
248
|
+
# end
|
249
|
+
#
|
250
|
+
# In this case the +log_children+ gets executed before +do_something_else+.
|
251
|
+
# The same applies to all non-transactional callbacks.
|
252
|
+
#
|
253
|
+
# In case there are multiple transactional callbacks as seen below, the order
|
254
|
+
# is reversed.
|
255
|
+
#
|
256
|
+
# For example:
|
257
|
+
#
|
258
|
+
# class Topic
|
259
|
+
# has_many :children
|
260
|
+
#
|
261
|
+
# after_commit :log_children
|
262
|
+
# after_commit :do_something_else
|
263
|
+
#
|
264
|
+
# private
|
265
|
+
#
|
266
|
+
# def log_chidren
|
267
|
+
# # Child processing
|
268
|
+
# end
|
269
|
+
#
|
270
|
+
# def do_something_else
|
271
|
+
# # Something else
|
272
|
+
# end
|
273
|
+
# end
|
274
|
+
#
|
275
|
+
# In this case the +do_something_else+ gets executed before +log_children+.
|
276
|
+
#
|
277
|
+
# == \Transactions
|
278
|
+
#
|
279
|
+
# The entire callback chain of a {#save}[rdoc-ref:Persistence#save], {#save!}[rdoc-ref:Persistence#save!],
|
280
|
+
# or {#destroy}[rdoc-ref:Persistence#destroy] call runs within a transaction. That includes <tt>after_*</tt> hooks.
|
281
|
+
# If everything goes fine a COMMIT is executed once the chain has been completed.
|
282
|
+
#
|
283
|
+
# If a <tt>before_*</tt> callback cancels the action a ROLLBACK is issued. You
|
284
|
+
# can also trigger a ROLLBACK raising an exception in any of the callbacks,
|
285
|
+
# including <tt>after_*</tt> hooks. Note, however, that in that case the client
|
286
|
+
# needs to be aware of it because an ordinary {#save}[rdoc-ref:Persistence#save] will raise such exception
|
287
|
+
# instead of quietly returning +false+.
|
288
|
+
#
|
289
|
+
# == Debugging callbacks
|
290
|
+
#
|
291
|
+
# The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. Active Model \Callbacks support
|
292
|
+
# <tt>:before</tt>, <tt>:after</tt> and <tt>:around</tt> as values for the <tt>kind</tt> property. The <tt>kind</tt> property
|
293
|
+
# defines what part of the chain the callback runs in.
|
294
|
+
#
|
295
|
+
# To find all callbacks in the before_save callback chain:
|
296
|
+
#
|
297
|
+
# Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }
|
298
|
+
#
|
299
|
+
# Returns an array of callback objects that form the before_save chain.
|
300
|
+
#
|
301
|
+
# To further check if the before_save chain contains a proc defined as <tt>rest_when_dead</tt> use the <tt>filter</tt> property of the callback object:
|
302
|
+
#
|
303
|
+
# Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }.collect(&:filter).include?(:rest_when_dead)
|
304
|
+
#
|
305
|
+
# Returns true or false depending on whether the proc is contained in the before_save callback chain on a Topic model.
|
306
|
+
#
|
307
|
+
module Callbacks
|
308
|
+
extend ActiveSupport::Concern
|
309
|
+
|
310
|
+
CALLBACKS = [
|
311
|
+
:after_initialize, :before_validation, :after_validation
|
312
|
+
]
|
313
|
+
|
314
|
+
module ClassMethods # :nodoc:
|
315
|
+
include ActiveModel::Callbacks
|
316
|
+
end
|
317
|
+
|
318
|
+
included do
|
319
|
+
include ActiveModel::Validations::Callbacks
|
320
|
+
|
321
|
+
define_model_callbacks :initialize, :only => :after
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|