-
Notifications
You must be signed in to change notification settings - Fork 0
/
active_record_proxy.rb
213 lines (178 loc) · 7.63 KB
/
active_record_proxy.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# frozen_string_literal: true
module Petra
module Proxies
#
# A specialized version of the ObjectProxy class to handle ActiveRecord instances.
# The main difference is, that changes to an object are only persisted if the
# records were saved, updated or destroyed using the corresponding
# methods (save, update_attributes, destroy) as we do not have to track e.g.
# new records (#new actions) or records which could not be saved due to
# validation errors etc.
#
class ActiveRecordProxy < Petra::Proxies::ObjectProxy
CLASS_NAMES = %w[ActiveRecord::Base].freeze
#
# When using ActiveRecord objects, we have a pretty good
# way of checking whether an object already existing outside the transaction
# or not by simply checking whether it already has an ID or not.
#
def __new?
!proxied_object.persisted?
end
#----------------------------------------------------------------
# CUD
#----------------------------------------------------------------
def update_attributes(attributes)
instance_method!
# TODO: nested parameters...
attributes.each do |k, v|
__set_attribute(k, v)
end
# As attribute changes are done separately, we can safely give `save` as
# method to apply to the generated log entry instead of the actual method (update_attributes)
save
end
def save(*)
instance_method!
transaction.log_object_persistence(self, method: 'save')
# TODO: Validations?
true
end
# Still Creepy!
def new(attributes = {})
super().tap do |o|
transaction.log_object_initialization(o, method: 'new')
# TODO: nested parameters...
attributes.each do |k, v|
o.__set_attribute(k, v)
end
end
end
def create(attributes = {})
class_method!
new(attributes).tap do |o|
# Set the called method to #save as we will use #new -> #save in the commit phase
transaction.log_object_persistence(o, method: 'save')
end
end
def destroy
instance_method!
transaction.log_object_destruction(self, method: 'destroy')
self
end
#----------------------------------------------------------------
# Finding Records
#----------------------------------------------------------------
#
# Ugly wrapper around AR's #find method which allows
# searching for records which were created during a transaction.
#
def find(*ids)
class_method!
# Extract non-AR IDs. Currently, the only way to detect them is to
# search for the pattern "new_DIGITS" which may conflict with custom primary keys,
# e.g. the `friendly_id` gem
new_ids = ids.select { |id| id =~ /^new_\d+$/ }
# Try to look up objects which were created during this transaction and match
# the given IDs. This will automatically raise ActiveRecord::RecordNotFound errors
# if an object isn't found.
new_records = new_records_from_ids(new_ids)
# Fetch the records which already existed outside the transaction and
# add the temporary objects to the result
result = __handlers.handle_missing_method('find', ids - new_ids) + new_records
# Fail if there are any destroyed objects in the returned collection
if (destroyed_record = result.find(&:destroyed?))
fail ::ActiveRecord::RecordNotFound,
"Couldn't find #{name} with '#{primary_key}'=#{destroyed_record.__object_id}"
end
# Make sure that everything is a petra proxy from this point on
result = result.map { |r| r.petra(inherited: true, configuration_args: ['find']) }
# To emulate AR's behaviour, return the first result if we only got one.
result.size == 1 ? result.first : result
end
#----------------------------------------------------------------
# Persistence Flags
#----------------------------------------------------------------
#
# @return [Boolean] +true+ if the proxied object was initialized during the transaction
# and hasn't been object persisted yet
#
def new_record?
instance_method!
!__existing? && !__created?
end
#
# @return [Boolean] +true+ if the proxied object either already existed (persisted) when the
# transaction started or was object persisted during its execution.
#
def persisted?
instance_method!
__existing? || __created?
end
def destroyed?
instance_method!
__destroyed?
end
#----------------------------------------------------------------
# Rails' Internal Helpers
#----------------------------------------------------------------
#
# Instead of forwarding #to_model to the proxied object, we have to
# return the proxy to ensure that Rails' internal methods (e.g. url_for)
# get the correct data
#
def to_model(*)
self
end
#
# If the record existed before the transaction started, we may simply return its ID.
# Otherwise... well, we return our internal ID which isn't the best solution, but it at
# least allows us to work with __new? records mostly like we would with __existing?
#
def to_param
__existing? ? proxied_object.to_param : __object_id
end
private
#
# For ActiveRecord instances, getter and setter methods can usually be derived
# from the database based attributes.
# Therefore, this proxy will first check whether the given method name
# matches one of the instance's attributes before checking for manually defined
# getter methods.
# This way, developers don't have to specify each database attribute manually.
#
def __attribute_reader?(method_name)
# If we don't have access to the available attributes, we have to
# to fall back to normal getter detection.
return super(method_name) unless proxied_object.respond_to?(:attributes)
# Setters are no getters. TODO: is super() necessary here?
return false if method_name =~ /=$/
# Check whether the given method name is part
proxied_object.attributes.keys.include?(method_name.to_s) || super(method_name)
end
#
# @see #__attribute_reader?
#
def __attribute_writer?(method_name)
# Attribute writers have to end with a = (for now)
return false unless method_name =~ /=$/
# Association setters... not going to be as easy as this
# return true if !class_proxy? && proxied_object.class.reflect_on_association(method_name[0..-2])
# If the method name ended with a =, we simply have to check if there is
# a corresponding getter (= an attribute with the given method name)
__attribute_reader?(method_name[0..-2]) || super(method_name)
end
#
# @return [Array<Petra::Proxies::ObjectProxy>] records which were created during this transaction.
# The cannot be found using AR's finder methods as they are not yet persisted in the database.
#
def new_records_from_ids(ids)
ids.map do |new_id|
transaction.objects.created(proxied_object).find { |o| o.__object_id == new_id }.tap do |object|
fail ::ActiveRecord::RecordNotFound, "Couldn't find #{name} with '#{primary_key}'=#{new_id}" unless object
end
end
end
end
end
end