activerecord-oracle_enhanced-adapter 1.2.4 → 1.3.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/.gitignore +0 -1
- data/History.txt +20 -0
- data/README.rdoc +7 -3
- data/Rakefile +1 -2
- data/VERSION +1 -1
- data/activerecord-oracle_enhanced-adapter.gemspec +96 -0
- data/lib/active_record/connection_adapters/oracle_enhanced.rake +11 -8
- data/lib/active_record/connection_adapters/oracle_enhanced_activerecord_patches.rb +37 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +317 -180
- data/lib/active_record/connection_adapters/oracle_enhanced_context_index.rb +282 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb +3 -2
- data/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +1 -1
- data/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb +3 -3
- data/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb +6 -1
- data/lib/active_record/connection_adapters/oracle_enhanced_procedures.rb +143 -52
- data/lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb +2 -1
- data/lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb +39 -20
- data/lib/active_record/connection_adapters/oracle_enhanced_tasks.rb +2 -1
- data/lib/active_record/connection_adapters/oracle_enhanced_version.rb +1 -1
- data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +70 -11
- data/spec/active_record/connection_adapters/oracle_enhanced_adapter_structure_dumper_spec.rb +27 -20
- data/spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb +334 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb +28 -22
- data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +24 -28
- data/spec/active_record/connection_adapters/oracle_enhanced_dbms_output_spec.rb +13 -11
- data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +1 -1
- data/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb +72 -69
- data/spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb +112 -6
- data/spec/active_record/connection_adapters/oracle_enhanced_schema_spec.rb +49 -1
- data/spec/spec_helper.rb +97 -19
- metadata +33 -22
- data/Manifest.txt +0 -32
- data/lib/active_record/connection_adapters/oracle_enhanced_reserved_words.rb +0 -126
@@ -0,0 +1,282 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module OracleEnhancedContextIndex
|
4
|
+
|
5
|
+
# Define full text index with Oracle specific CONTEXT index type
|
6
|
+
#
|
7
|
+
# Oracle CONTEXT index by default supports full text indexing of one column.
|
8
|
+
# This method allows full text index creation also on several columns
|
9
|
+
# as well as indexing related table columns by generating stored procedure
|
10
|
+
# that concatenates all columns for indexing as well as generating trigger
|
11
|
+
# that will update main index column to trigger reindexing of record.
|
12
|
+
#
|
13
|
+
# Use +contains+ ActiveRecord model instance method to add CONTAINS where condition
|
14
|
+
# and order by score of matched results.
|
15
|
+
#
|
16
|
+
# ===== Examples
|
17
|
+
#
|
18
|
+
# ====== Creating single column index
|
19
|
+
# add_context_index :posts, :title
|
20
|
+
# search with
|
21
|
+
# Post.contains(:title, 'word')
|
22
|
+
#
|
23
|
+
# ====== Creating index on several columns
|
24
|
+
# add_context_index :posts, [:title, :body]
|
25
|
+
# search with (use first column as argument for contains method but it will search in all index columns)
|
26
|
+
# Post.contains(:title, 'word')
|
27
|
+
#
|
28
|
+
# ====== Creating index on several columns with dummy index column and commit option
|
29
|
+
# add_context_index :posts, [:title, :body], :index_column => :all_text, :sync => 'ON COMMIT'
|
30
|
+
# search with
|
31
|
+
# Post.contains(:all_text, 'word')
|
32
|
+
#
|
33
|
+
# ====== Creating index with trigger option (will reindex when specified columns are updated)
|
34
|
+
# add_context_index :posts, [:title, :body], :index_column => :all_text, :sync => 'ON COMMIT',
|
35
|
+
# :index_column_trigger_on => [:created_at, :updated_at]
|
36
|
+
# search with
|
37
|
+
# Post.contains(:all_text, 'word')
|
38
|
+
#
|
39
|
+
# ====== Creating index on multiple tables
|
40
|
+
# add_context_index :posts, [:title, :body], :index_column => :all_text, :sync => 'ON COMMIT',
|
41
|
+
# :index_column_trigger_on => [:created_at, :updated_at]
|
42
|
+
# add_context_index :posts,
|
43
|
+
# [:title, :body,
|
44
|
+
# # specify aliases always with AS keyword
|
45
|
+
# "SELECT comments.author AS comment_author, comments.body AS comment_body FROM comments WHERE comments.post_id = :id"
|
46
|
+
# ],
|
47
|
+
# :name => 'post_and_comments_index',
|
48
|
+
# :index_column => :all_text, :index_column_trigger_on => [:updated_at, :comments_count],
|
49
|
+
# :sync => 'ON COMMIT'
|
50
|
+
# search in any table columns
|
51
|
+
# Post.contains(:all_text, 'word')
|
52
|
+
# search in specified column
|
53
|
+
# Post.contains(:all_text, "aaa within title")
|
54
|
+
# Post.contains(:all_text, "bbb within comment_author")
|
55
|
+
#
|
56
|
+
def add_context_index(table_name, column_name, options = {})
|
57
|
+
self.all_schema_indexes = nil
|
58
|
+
column_names = Array(column_name)
|
59
|
+
index_name = options[:name] || index_name(table_name, :column => options[:index_column] || column_names,
|
60
|
+
# CONEXT index name max length is 25
|
61
|
+
:identifier_max_length => 25)
|
62
|
+
|
63
|
+
quoted_column_name = quote_column_name(options[:index_column] || column_names.first)
|
64
|
+
if options[:index_column_trigger_on]
|
65
|
+
raise ArgumentError, "Option :index_column should be specified together with :index_column_cource option" \
|
66
|
+
unless options[:index_column]
|
67
|
+
create_index_column_trigger(table_name, index_name, options[:index_column], options[:index_column_trigger_on])
|
68
|
+
end
|
69
|
+
|
70
|
+
sql = "CREATE INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}"
|
71
|
+
sql << " (#{quoted_column_name})"
|
72
|
+
sql << " INDEXTYPE IS CTXSYS.CONTEXT"
|
73
|
+
parameters = []
|
74
|
+
if column_names.size > 1
|
75
|
+
procedure_name = default_datastore_procedure(index_name)
|
76
|
+
datastore_name = default_datastore_name(index_name)
|
77
|
+
create_datastore_procedure(table_name, procedure_name, column_names, options)
|
78
|
+
create_datastore_preference(datastore_name, procedure_name)
|
79
|
+
parameters << "DATASTORE #{datastore_name} SECTION GROUP CTXSYS.AUTO_SECTION_GROUP"
|
80
|
+
end
|
81
|
+
if options[:tablespace]
|
82
|
+
storage_name = default_storage_name(index_name)
|
83
|
+
create_storage_preference(storage_name, options[:tablespace])
|
84
|
+
parameters << "STORAGE #{storage_name}"
|
85
|
+
end
|
86
|
+
if options[:sync]
|
87
|
+
parameters << "SYNC(#{options[:sync]})"
|
88
|
+
end
|
89
|
+
unless parameters.empty?
|
90
|
+
sql << " PARAMETERS ('#{parameters.join(' ')}')"
|
91
|
+
end
|
92
|
+
execute sql
|
93
|
+
end
|
94
|
+
|
95
|
+
# Drop full text index with Oracle specific CONTEXT index type
|
96
|
+
def remove_context_index(table_name, options = {})
|
97
|
+
self.all_schema_indexes = nil
|
98
|
+
unless Hash === options # if column names passed as argument
|
99
|
+
options = {:column => Array(options)}
|
100
|
+
end
|
101
|
+
index_name = options[:name] || index_name(table_name,
|
102
|
+
:column => options[:index_column] || options[:column], :identifier_max_length => 25)
|
103
|
+
execute "DROP INDEX #{index_name}"
|
104
|
+
drop_ctx_preference(default_datastore_name(index_name))
|
105
|
+
drop_ctx_preference(default_storage_name(index_name))
|
106
|
+
procedure_name = default_datastore_procedure(index_name)
|
107
|
+
execute "DROP PROCEDURE #{quote_table_name(procedure_name)}" rescue nil
|
108
|
+
drop_index_column_trigger(index_name)
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def create_datastore_procedure(table_name, procedure_name, column_names, options)
|
114
|
+
quoted_table_name = quote_table_name(table_name)
|
115
|
+
select_queries = column_names.select{|c| c.to_s =~ /^SELECT /i}
|
116
|
+
column_names = column_names - select_queries
|
117
|
+
keys, selected_columns = parse_select_queries(select_queries)
|
118
|
+
quoted_column_names = (column_names+keys).map{|col| quote_column_name(col)}
|
119
|
+
execute compress_lines(<<-SQL)
|
120
|
+
CREATE OR REPLACE PROCEDURE #{quote_table_name(procedure_name)}
|
121
|
+
(p_rowid IN ROWID,
|
122
|
+
p_clob IN OUT NOCOPY CLOB) IS
|
123
|
+
-- add_context_index_parameters #{(column_names+select_queries).inspect}#{!options.empty? ? ', ' << options.inspect[1..-2] : ''}
|
124
|
+
#{
|
125
|
+
selected_columns.map do |cols|
|
126
|
+
cols.map do |col|
|
127
|
+
raise ArgumentError, "Alias #{col} too large, should be 28 or less characters long" unless col.length <= 28
|
128
|
+
"l_#{col} VARCHAR2(32767);\n"
|
129
|
+
end.join
|
130
|
+
end.join
|
131
|
+
} BEGIN
|
132
|
+
FOR r1 IN (
|
133
|
+
SELECT #{quoted_column_names.join(', ')}
|
134
|
+
FROM #{quoted_table_name}
|
135
|
+
WHERE #{quoted_table_name}.ROWID = p_rowid
|
136
|
+
) LOOP
|
137
|
+
#{
|
138
|
+
(column_names.map do |col|
|
139
|
+
col = col.to_s
|
140
|
+
"DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+2}, '<#{col}>');\n" <<
|
141
|
+
"DBMS_LOB.WRITEAPPEND(p_clob, LENGTH(r1.#{col}), r1.#{col});\n" <<
|
142
|
+
"DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+3}, '</#{col}>');\n"
|
143
|
+
end.join) <<
|
144
|
+
(selected_columns.zip(select_queries).map do |cols, query|
|
145
|
+
(cols.map do |col|
|
146
|
+
"l_#{col} := '';\n"
|
147
|
+
end.join) <<
|
148
|
+
"FOR r2 IN (\n" <<
|
149
|
+
query.gsub(/:(\w+)/,"r1.\\1") << "\n) LOOP\n" <<
|
150
|
+
(cols.map do |col|
|
151
|
+
"l_#{col} := l_#{col} || r2.#{col} || CHR(10);\n"
|
152
|
+
end.join) <<
|
153
|
+
"END LOOP;\n" <<
|
154
|
+
(cols.map do |col|
|
155
|
+
col = col.to_s
|
156
|
+
"DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+2}, '<#{col}>');\n" <<
|
157
|
+
"IF LENGTH(l_#{col}) > 0 THEN\n" <<
|
158
|
+
"DBMS_LOB.WRITEAPPEND(p_clob, LENGTH(l_#{col}), l_#{col});\n" <<
|
159
|
+
"END IF;\n" <<
|
160
|
+
"DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+3}, '</#{col}>');\n"
|
161
|
+
end.join)
|
162
|
+
end.join)
|
163
|
+
}
|
164
|
+
END LOOP;
|
165
|
+
END;
|
166
|
+
SQL
|
167
|
+
end
|
168
|
+
|
169
|
+
def parse_select_queries(select_queries)
|
170
|
+
keys = []
|
171
|
+
selected_columns = []
|
172
|
+
select_queries.each do |query|
|
173
|
+
# get primary or foreign keys like :id or :something_id
|
174
|
+
keys << (query.scan(/:\w+/).map{|k| k[1..-1].downcase.to_sym})
|
175
|
+
select_part = query.scan(/^select\s.*\sfrom/i).first
|
176
|
+
selected_columns << select_part.scan(/\sas\s+(\w+)/i).map{|c| c.first}
|
177
|
+
end
|
178
|
+
[keys.flatten.uniq, selected_columns]
|
179
|
+
end
|
180
|
+
|
181
|
+
def create_datastore_preference(datastore_name, procedure_name)
|
182
|
+
drop_ctx_preference(datastore_name)
|
183
|
+
execute <<-SQL
|
184
|
+
BEGIN
|
185
|
+
CTX_DDL.CREATE_PREFERENCE('#{datastore_name}', 'USER_DATASTORE');
|
186
|
+
CTX_DDL.SET_ATTRIBUTE('#{datastore_name}', 'PROCEDURE', '#{procedure_name}');
|
187
|
+
END;
|
188
|
+
SQL
|
189
|
+
end
|
190
|
+
|
191
|
+
def create_storage_preference(storage_name, tablespace)
|
192
|
+
drop_ctx_preference(storage_name)
|
193
|
+
sql = "BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{storage_name}', 'BASIC_STORAGE');\n"
|
194
|
+
['I_TABLE_CLAUSE', 'K_TABLE_CLAUSE', 'R_TABLE_CLAUSE',
|
195
|
+
'N_TABLE_CLAUSE', 'I_INDEX_CLAUSE', 'P_TABLE_CLAUSE'].each do |clause|
|
196
|
+
default_clause = case clause
|
197
|
+
when 'R_TABLE_CLAUSE'; 'LOB(DATA) STORE AS (CACHE) '
|
198
|
+
when 'I_INDEX_CLAUSE'; 'COMPRESS 2 '
|
199
|
+
else ''
|
200
|
+
end
|
201
|
+
sql << "CTX_DDL.SET_ATTRIBUTE('#{storage_name}', '#{clause}', '#{default_clause}TABLESPACE #{tablespace}');\n"
|
202
|
+
end
|
203
|
+
sql << "END;\n"
|
204
|
+
execute sql
|
205
|
+
end
|
206
|
+
|
207
|
+
def drop_ctx_preference(preference_name)
|
208
|
+
execute "BEGIN CTX_DDL.DROP_PREFERENCE('#{preference_name}'); END;" rescue nil
|
209
|
+
end
|
210
|
+
|
211
|
+
def create_index_column_trigger(table_name, index_name, index_column, index_column_source)
|
212
|
+
trigger_name = default_index_column_trigger_name(index_name)
|
213
|
+
columns = Array(index_column_source)
|
214
|
+
quoted_column_names = columns.map{|col| quote_column_name(col)}.join(', ')
|
215
|
+
execute compress_lines(<<-SQL)
|
216
|
+
CREATE OR REPLACE TRIGGER #{quote_table_name(trigger_name)}
|
217
|
+
BEFORE UPDATE OF #{quoted_column_names} ON #{quote_table_name(table_name)} FOR EACH ROW
|
218
|
+
BEGIN
|
219
|
+
:new.#{quote_column_name(index_column)} := '1';
|
220
|
+
END;
|
221
|
+
SQL
|
222
|
+
end
|
223
|
+
|
224
|
+
def drop_index_column_trigger(index_name)
|
225
|
+
trigger_name = default_index_column_trigger_name(index_name)
|
226
|
+
execute "DROP TRIGGER #{quote_table_name(trigger_name)}" rescue nil
|
227
|
+
end
|
228
|
+
|
229
|
+
def default_datastore_procedure(index_name)
|
230
|
+
"#{index_name}_prc"
|
231
|
+
end
|
232
|
+
|
233
|
+
def default_datastore_name(index_name)
|
234
|
+
"#{index_name}_dst"
|
235
|
+
end
|
236
|
+
|
237
|
+
def default_storage_name(index_name)
|
238
|
+
"#{index_name}_sto"
|
239
|
+
end
|
240
|
+
|
241
|
+
def default_index_column_trigger_name(index_name)
|
242
|
+
"#{index_name}_trg"
|
243
|
+
end
|
244
|
+
|
245
|
+
module BaseClassMethods
|
246
|
+
# Declare that model table has context index defined.
|
247
|
+
# As a result <tt>contains</tt> class scope method is defined.
|
248
|
+
def has_context_index
|
249
|
+
extend ContextIndexClassMethods
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
module ContextIndexClassMethods
|
254
|
+
# Add context index condition.
|
255
|
+
case ::ActiveRecord::VERSION::MAJOR
|
256
|
+
when 3
|
257
|
+
def contains(column, query, options ={})
|
258
|
+
score_label = options[:label].to_i || 1
|
259
|
+
where("CONTAINS(#{connection.quote_column_name(column)}, ?, #{score_label}) > 0", query).
|
260
|
+
order("SCORE(#{score_label}) DESC")
|
261
|
+
end
|
262
|
+
when 2
|
263
|
+
def contains(column, query, options ={})
|
264
|
+
score_label = options[:label].to_i || 1
|
265
|
+
scoped(:conditions => ["CONTAINS(#{connection.quote_column_name(column)}, ?, #{score_label}) > 0", query],
|
266
|
+
:order => "SCORE(#{score_label}) DESC")
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
end
|
272
|
+
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do
|
277
|
+
include ActiveRecord::ConnectionAdapters::OracleEnhancedContextIndex
|
278
|
+
end
|
279
|
+
|
280
|
+
ActiveRecord::Base.class_eval do
|
281
|
+
extend ActiveRecord::ConnectionAdapters::OracleEnhancedContextIndex::BaseClassMethods
|
282
|
+
end
|
@@ -26,7 +26,6 @@ end
|
|
26
26
|
# Add Unicode aware String#upcase and String#downcase methods when mb_chars method is called
|
27
27
|
if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'ruby' && RUBY_VERSION >= '1.9'
|
28
28
|
begin
|
29
|
-
gem "unicode_utils", ">=1.0.0"
|
30
29
|
require "unicode_utils/upcase"
|
31
30
|
require "unicode_utils/downcase"
|
32
31
|
|
@@ -53,7 +52,9 @@ if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'ruby' && RUBY_VERSION >= '1.9'
|
|
53
52
|
|
54
53
|
rescue LoadError
|
55
54
|
warning_message = "WARNING: Please install unicode_utils gem to support Unicode aware upcase and downcase for String#mb_chars"
|
56
|
-
if defined?(
|
55
|
+
if defined?(Rails.logger) && Rails.logger
|
56
|
+
Rails.logger.warn warning_message
|
57
|
+
elsif defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER
|
57
58
|
RAILS_DEFAULT_LOGGER.warn warning_message
|
58
59
|
else
|
59
60
|
STDERR.puts warning_message
|
@@ -32,7 +32,7 @@ module ActiveRecord #:nodoc:
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
-
if ActiveRecord::Base.
|
35
|
+
if ActiveRecord::Base.method_defined?(:changed?)
|
36
36
|
ActiveRecord::Base.class_eval do
|
37
37
|
include ActiveRecord::ConnectionAdapters::OracleEnhancedDirty::InstanceMethods
|
38
38
|
end
|
@@ -154,7 +154,7 @@ module ActiveRecord
|
|
154
154
|
@active = true
|
155
155
|
rescue NativeException => e
|
156
156
|
@active = false
|
157
|
-
if e.message =~ /^java\.sql\.
|
157
|
+
if e.message =~ /^java\.sql\.SQL(Recoverable)?Exception/
|
158
158
|
raise OracleEnhancedConnectionException, e.message
|
159
159
|
else
|
160
160
|
raise
|
@@ -169,7 +169,7 @@ module ActiveRecord
|
|
169
169
|
@active = true
|
170
170
|
rescue NativeException => e
|
171
171
|
@active = false
|
172
|
-
if e.message =~ /^java\.sql\.
|
172
|
+
if e.message =~ /^java\.sql\.SQL(Recoverable)?Exception/
|
173
173
|
raise OracleEnhancedConnectionException, e.message
|
174
174
|
else
|
175
175
|
raise
|
@@ -183,7 +183,7 @@ module ActiveRecord
|
|
183
183
|
begin
|
184
184
|
yield if block_given?
|
185
185
|
rescue NativeException => e
|
186
|
-
raise unless e.message =~ /^java\.sql\.
|
186
|
+
raise unless e.message =~ /^java\.sql\.SQL(Recoverable)?Exception: (Closed Connection|Io exception:|No more data to read from socket)/
|
187
187
|
@active = false
|
188
188
|
raise unless should_retry
|
189
189
|
should_retry = false
|
@@ -51,68 +51,106 @@ module ActiveRecord #:nodoc:
|
|
51
51
|
include_with_custom_methods
|
52
52
|
self.custom_delete_method = block
|
53
53
|
end
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
:create_without_timestamps
|
58
|
-
|
59
|
-
:create_without_callbacks
|
60
|
-
|
61
|
-
|
54
|
+
|
55
|
+
if ActiveRecord::VERSION::MAJOR < 3
|
56
|
+
def create_method_name_before_custom_methods #:nodoc:
|
57
|
+
if private_method_defined?(:create_without_timestamps) && defined?(ActiveRecord::VERSION) && ActiveRecord::VERSION::STRING.to_f >= 2.3
|
58
|
+
:create_without_timestamps
|
59
|
+
elsif private_method_defined?(:create_without_callbacks)
|
60
|
+
:create_without_callbacks
|
61
|
+
else
|
62
|
+
:create
|
63
|
+
end
|
62
64
|
end
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
:
|
68
|
-
|
69
|
-
:
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
65
|
+
|
66
|
+
def update_method_name_before_custom_methods #:nodoc:
|
67
|
+
if private_method_defined?(:update_without_dirty)
|
68
|
+
:update_without_dirty
|
69
|
+
elsif private_method_defined?(:update_without_timestamps) && defined?(ActiveRecord::VERSION) && ActiveRecord::VERSION::STRING.to_f >= 2.3
|
70
|
+
:update_without_timestamps
|
71
|
+
elsif private_method_defined?(:update_without_callbacks)
|
72
|
+
:update_without_callbacks
|
73
|
+
else
|
74
|
+
:update
|
75
|
+
end
|
74
76
|
end
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
77
|
+
|
78
|
+
def destroy_method_name_before_custom_methods #:nodoc:
|
79
|
+
if public_method_defined?(:destroy_without_callbacks)
|
80
|
+
:destroy_without_callbacks
|
81
|
+
else
|
82
|
+
:destroy
|
83
|
+
end
|
82
84
|
end
|
83
85
|
end
|
84
|
-
|
86
|
+
|
85
87
|
private
|
88
|
+
|
86
89
|
def include_with_custom_methods
|
87
90
|
unless included_modules.include? InstanceMethods
|
88
91
|
include InstanceMethods
|
89
92
|
end
|
90
93
|
end
|
91
94
|
end
|
92
|
-
|
95
|
+
|
93
96
|
module InstanceMethods #:nodoc:
|
94
97
|
def self.included(base)
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
98
|
+
# alias methods just for ActiveRecord 2.x
|
99
|
+
# for ActiveRecord 3.0 will just redefine create, update, delete methods which call super
|
100
|
+
if ActiveRecord::VERSION::MAJOR < 3
|
101
|
+
base.instance_eval do
|
102
|
+
alias_method :create_without_custom_method, create_method_name_before_custom_methods
|
103
|
+
alias_method create_method_name_before_custom_methods, :create_with_custom_method
|
104
|
+
alias_method :update_without_custom_method, update_method_name_before_custom_methods
|
105
|
+
alias_method update_method_name_before_custom_methods, :update_with_custom_method
|
106
|
+
alias_method :destroy_without_custom_method, destroy_method_name_before_custom_methods
|
107
|
+
alias_method destroy_method_name_before_custom_methods, :destroy_with_custom_method
|
108
|
+
private :create, :update
|
109
|
+
public :destroy
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
if ActiveRecord::VERSION::MAJOR >= 3
|
115
|
+
def destroy #:nodoc:
|
116
|
+
# check if class has custom delete method
|
117
|
+
if self.class.custom_delete_method
|
118
|
+
# wrap destroy in transaction
|
119
|
+
with_transaction_returning_status do
|
120
|
+
# run before/after callbacks defined in model
|
121
|
+
_run_destroy_callbacks { destroy_using_custom_method }
|
122
|
+
end
|
123
|
+
else
|
124
|
+
super
|
125
|
+
end
|
104
126
|
end
|
105
127
|
end
|
106
|
-
|
128
|
+
|
107
129
|
private
|
108
|
-
|
130
|
+
|
109
131
|
# Creates a record with custom create method
|
110
132
|
# and returns its id.
|
111
|
-
|
112
|
-
|
113
|
-
|
133
|
+
if ActiveRecord::VERSION::MAJOR < 3
|
134
|
+
def create_with_custom_method
|
135
|
+
# check if class has custom create method
|
136
|
+
self.class.custom_create_method ? create_using_custom_method : create_without_custom_method
|
137
|
+
end
|
138
|
+
else # ActiveRecord 3.x
|
139
|
+
def create
|
140
|
+
# check if class has custom create method
|
141
|
+
if self.class.custom_create_method
|
142
|
+
set_timestamps_before_custom_create_method
|
143
|
+
# run before/after callbacks defined in model
|
144
|
+
_run_create_callbacks { create_using_custom_method }
|
145
|
+
else
|
146
|
+
super
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def create_using_custom_method
|
114
152
|
self.class.connection.log_custom_method("custom create method", "#{self.class.name} Create") do
|
115
|
-
self.id = self.class.custom_create_method
|
153
|
+
self.id = instance_eval(&self.class.custom_create_method)
|
116
154
|
end
|
117
155
|
@new_record = false
|
118
156
|
id
|
@@ -120,12 +158,37 @@ module ActiveRecord #:nodoc:
|
|
120
158
|
|
121
159
|
# Updates the associated record with custom update method
|
122
160
|
# Returns the number of affected rows.
|
123
|
-
|
124
|
-
|
125
|
-
|
161
|
+
if ActiveRecord::VERSION::MAJOR < 3
|
162
|
+
def update_with_custom_method(attribute_names = @attributes.keys)
|
163
|
+
# check if class has custom create method
|
164
|
+
self.class.custom_update_method ? update_using_custom_method(attribute_names) : update_without_custom_method(attribute_names)
|
165
|
+
end
|
166
|
+
else # ActiveRecord 3.x
|
167
|
+
def update(attribute_names = @attributes.keys)
|
168
|
+
# check if class has custom update method
|
169
|
+
if self.class.custom_update_method
|
170
|
+
set_timestamps_before_custom_update_method
|
171
|
+
# run before/after callbacks defined in model
|
172
|
+
_run_update_callbacks do
|
173
|
+
# update just dirty attributes
|
174
|
+
if partial_updates?
|
175
|
+
# Serialized attributes should always be written in case they've been
|
176
|
+
# changed in place.
|
177
|
+
update_using_custom_method(changed | (attributes.keys & self.class.serialized_attributes.keys))
|
178
|
+
else
|
179
|
+
update_using_custom_method(attribute_names)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
else
|
183
|
+
super
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def update_using_custom_method(attribute_names)
|
126
189
|
return 0 if attribute_names.empty?
|
127
190
|
self.class.connection.log_custom_method("custom update method with #{self.class.primary_key}=#{self.id}", "#{self.class.name} Update") do
|
128
|
-
self.class.custom_update_method
|
191
|
+
instance_eval(&self.class.custom_update_method)
|
129
192
|
end
|
130
193
|
1
|
131
194
|
end
|
@@ -133,12 +196,17 @@ module ActiveRecord #:nodoc:
|
|
133
196
|
# Deletes the record in the database with custom delete method
|
134
197
|
# and freezes this instance to reflect that no changes should
|
135
198
|
# be made (since they can't be persisted).
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
199
|
+
if ActiveRecord::VERSION::MAJOR < 3
|
200
|
+
def destroy_with_custom_method
|
201
|
+
# check if class has custom delete method
|
202
|
+
self.class.custom_delete_method ? destroy_using_custom_method : destroy_without_custom_method
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def destroy_using_custom_method
|
207
|
+
unless new_record? || @destroyed
|
140
208
|
self.class.connection.log_custom_method("custom delete method with #{self.class.primary_key}=#{self.id}", "#{self.class.name} Destroy") do
|
141
|
-
self.class.custom_delete_method
|
209
|
+
instance_eval(&self.class.custom_delete_method)
|
142
210
|
end
|
143
211
|
end
|
144
212
|
|
@@ -146,6 +214,29 @@ module ActiveRecord #:nodoc:
|
|
146
214
|
freeze
|
147
215
|
end
|
148
216
|
|
217
|
+
if ActiveRecord::VERSION::MAJOR >= 3
|
218
|
+
def set_timestamps_before_custom_create_method
|
219
|
+
if record_timestamps
|
220
|
+
current_time = current_time_from_proper_timezone
|
221
|
+
|
222
|
+
write_attribute('created_at', current_time) if respond_to?(:created_at) && created_at.nil?
|
223
|
+
write_attribute('created_on', current_time) if respond_to?(:created_on) && created_on.nil?
|
224
|
+
|
225
|
+
write_attribute('updated_at', current_time) if respond_to?(:updated_at) && updated_at.nil?
|
226
|
+
write_attribute('updated_on', current_time) if respond_to?(:updated_on) && updated_on.nil?
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def set_timestamps_before_custom_update_method
|
231
|
+
if record_timestamps && (!partial_updates? || changed?)
|
232
|
+
current_time = current_time_from_proper_timezone
|
233
|
+
|
234
|
+
write_attribute('updated_at', current_time) if respond_to?(:updated_at)
|
235
|
+
write_attribute('updated_on', current_time) if respond_to?(:updated_on)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
149
240
|
end
|
150
241
|
|
151
242
|
end
|