bigrecord 0.0.5
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/MIT-LICENSE +20 -0
- data/README.rdoc +44 -0
- data/Rakefile +17 -0
- data/VERSION +1 -0
- data/doc/bigrecord_specs.rdoc +36 -0
- data/doc/getting_started.rdoc +157 -0
- data/examples/bigrecord.yml +25 -0
- data/generators/bigrecord/bigrecord_generator.rb +17 -0
- data/generators/bigrecord/templates/bigrecord.rake +47 -0
- data/generators/bigrecord_migration/bigrecord_migration_generator.rb +13 -0
- data/generators/bigrecord_migration/templates/migration.rb +9 -0
- data/generators/bigrecord_model/bigrecord_model_generator.rb +28 -0
- data/generators/bigrecord_model/templates/migration.rb +13 -0
- data/generators/bigrecord_model/templates/model.rb +7 -0
- data/generators/bigrecord_model/templates/model_spec.rb +12 -0
- data/init.rb +9 -0
- data/install.rb +22 -0
- data/lib/big_record/abstract_base.rb +1088 -0
- data/lib/big_record/action_view_extensions.rb +266 -0
- data/lib/big_record/ar_associations/association_collection.rb +194 -0
- data/lib/big_record/ar_associations/association_proxy.rb +158 -0
- data/lib/big_record/ar_associations/belongs_to_association.rb +57 -0
- data/lib/big_record/ar_associations/belongs_to_many_association.rb +57 -0
- data/lib/big_record/ar_associations/has_and_belongs_to_many_association.rb +164 -0
- data/lib/big_record/ar_associations/has_many_association.rb +191 -0
- data/lib/big_record/ar_associations/has_one_association.rb +80 -0
- data/lib/big_record/ar_associations.rb +1608 -0
- data/lib/big_record/ar_reflection.rb +223 -0
- data/lib/big_record/attribute_methods.rb +75 -0
- data/lib/big_record/base.rb +618 -0
- data/lib/big_record/br_associations/association_collection.rb +194 -0
- data/lib/big_record/br_associations/association_proxy.rb +153 -0
- data/lib/big_record/br_associations/belongs_to_association.rb +52 -0
- data/lib/big_record/br_associations/belongs_to_many_association.rb +293 -0
- data/lib/big_record/br_associations/cached_item_proxy.rb +194 -0
- data/lib/big_record/br_associations/cached_item_proxy_factory.rb +62 -0
- data/lib/big_record/br_associations/has_and_belongs_to_many_association.rb +168 -0
- data/lib/big_record/br_associations/has_one_association.rb +80 -0
- data/lib/big_record/br_associations.rb +978 -0
- data/lib/big_record/br_reflection.rb +151 -0
- data/lib/big_record/callbacks.rb +367 -0
- data/lib/big_record/connection_adapters/abstract/connection_specification.rb +279 -0
- data/lib/big_record/connection_adapters/abstract/database_statements.rb +175 -0
- data/lib/big_record/connection_adapters/abstract/quoting.rb +58 -0
- data/lib/big_record/connection_adapters/abstract_adapter.rb +190 -0
- data/lib/big_record/connection_adapters/column.rb +491 -0
- data/lib/big_record/connection_adapters/hbase_adapter.rb +432 -0
- data/lib/big_record/connection_adapters/view.rb +27 -0
- data/lib/big_record/connection_adapters.rb +10 -0
- data/lib/big_record/deletion.rb +73 -0
- data/lib/big_record/dynamic_schema.rb +92 -0
- data/lib/big_record/embedded.rb +71 -0
- data/lib/big_record/embedded_associations/association_proxy.rb +148 -0
- data/lib/big_record/family_span_columns.rb +89 -0
- data/lib/big_record/fixtures.rb +1025 -0
- data/lib/big_record/migration.rb +380 -0
- data/lib/big_record/routing_ext.rb +65 -0
- data/lib/big_record/timestamp.rb +51 -0
- data/lib/big_record/validations.rb +830 -0
- data/lib/big_record.rb +125 -0
- data/lib/bigrecord.rb +1 -0
- data/rails/init.rb +9 -0
- data/spec/connections/bigrecord.yml +13 -0
- data/spec/connections/cassandra/connection.rb +2 -0
- data/spec/connections/hbase/connection.rb +2 -0
- data/spec/debug.log +281 -0
- data/spec/integration/br_associations_spec.rb +80 -0
- data/spec/lib/animal.rb +12 -0
- data/spec/lib/book.rb +10 -0
- data/spec/lib/broken_migrations/duplicate_name/20090706182535_add_animals_table.rb +14 -0
- data/spec/lib/broken_migrations/duplicate_name/20090706193019_add_animals_table.rb +9 -0
- data/spec/lib/broken_migrations/duplicate_version/20090706190623_add_books_table.rb +9 -0
- data/spec/lib/broken_migrations/duplicate_version/20090706190623_add_companies_table.rb +9 -0
- data/spec/lib/company.rb +14 -0
- data/spec/lib/embedded/web_link.rb +12 -0
- data/spec/lib/employee.rb +33 -0
- data/spec/lib/migrations/20090706182535_add_animals_table.rb +13 -0
- data/spec/lib/migrations/20090706190623_add_books_table.rb +15 -0
- data/spec/lib/migrations/20090706193019_add_companies_table.rb +14 -0
- data/spec/lib/migrations/20090706194512_add_employees_table.rb +13 -0
- data/spec/lib/migrations/20090706195741_add_zoos_table.rb +13 -0
- data/spec/lib/novel.rb +5 -0
- data/spec/lib/zoo.rb +17 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +55 -0
- data/spec/unit/abstract_base_spec.rb +287 -0
- data/spec/unit/adapters/abstract_adapter_spec.rb +56 -0
- data/spec/unit/adapters/adapter_shared_spec.rb +51 -0
- data/spec/unit/adapters/hbase_adapter_spec.rb +15 -0
- data/spec/unit/ar_associations_spec.rb +8 -0
- data/spec/unit/base_spec.rb +6 -0
- data/spec/unit/br_associations_spec.rb +58 -0
- data/spec/unit/embedded_spec.rb +43 -0
- data/spec/unit/find_spec.rb +34 -0
- data/spec/unit/hash_helper_spec.rb +44 -0
- data/spec/unit/migration_spec.rb +144 -0
- data/spec/unit/model_spec.rb +315 -0
- data/spec/unit/validations_spec.rb +182 -0
- data/tasks/bigrecord_tasks.rake +47 -0
- data/tasks/data_store.rb +46 -0
- data/tasks/gem.rb +22 -0
- data/tasks/rdoc.rb +8 -0
- data/tasks/spec.rb +34 -0
- metadata +189 -0
|
@@ -0,0 +1,1088 @@
|
|
|
1
|
+
## The sub-classes of this class must implement the abstract method 'column_names' of this class.
|
|
2
|
+
module BigRecord
|
|
3
|
+
class BigRecordError < StandardError #:nodoc:
|
|
4
|
+
end
|
|
5
|
+
class SubclassNotFound < BigRecordError #:nodoc:
|
|
6
|
+
end
|
|
7
|
+
class AssociationTypeMismatch < BigRecordError #:nodoc:
|
|
8
|
+
end
|
|
9
|
+
class WrongAttributeDataType < BigRecordError #:nodoc:
|
|
10
|
+
end
|
|
11
|
+
class AttributeMissing < BigRecordError #:nodoc:
|
|
12
|
+
end
|
|
13
|
+
class UnknownAttribute < BigRecordError #:nodoc:
|
|
14
|
+
end
|
|
15
|
+
class AdapterNotSpecified < BigRecordError # :nodoc:
|
|
16
|
+
end
|
|
17
|
+
class AdapterNotFound < BigRecordError # :nodoc:
|
|
18
|
+
end
|
|
19
|
+
class ConnectionNotEstablished < BigRecordError #:nodoc:
|
|
20
|
+
end
|
|
21
|
+
class ConnectionFailed < BigRecordError #:nodoc:
|
|
22
|
+
end
|
|
23
|
+
class RecordNotFound < BigRecordError #:nodoc:
|
|
24
|
+
end
|
|
25
|
+
class RecordNotSaved < BigRecordError #:nodoc:
|
|
26
|
+
end
|
|
27
|
+
class StatementInvalid < BigRecordError #:nodoc:
|
|
28
|
+
end
|
|
29
|
+
class PreparedStatementInvalid < BigRecordError #:nodoc:
|
|
30
|
+
end
|
|
31
|
+
class StaleObjectError < BigRecordError #:nodoc:
|
|
32
|
+
end
|
|
33
|
+
class ConfigurationError < StandardError #:nodoc:
|
|
34
|
+
end
|
|
35
|
+
class ReadOnlyRecord < StandardError #:nodoc:
|
|
36
|
+
end
|
|
37
|
+
class NotImplemented < BigRecordError #:nodoc:
|
|
38
|
+
end
|
|
39
|
+
class ColumnNotFound < BigRecordError #:nodoc:
|
|
40
|
+
end
|
|
41
|
+
class AttributeAssignmentError < BigRecordError #:nodoc:
|
|
42
|
+
attr_reader :exception, :attribute
|
|
43
|
+
def initialize(message, exception, attribute)
|
|
44
|
+
@exception = exception
|
|
45
|
+
@attribute = attribute
|
|
46
|
+
@message = message
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
class MultiparameterAssignmentErrors < BigRecordError #:nodoc:
|
|
50
|
+
attr_reader :errors
|
|
51
|
+
def initialize(errors)
|
|
52
|
+
@errors = errors
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class AbstractBase
|
|
57
|
+
require 'rubygems'
|
|
58
|
+
require 'uuidtools'
|
|
59
|
+
|
|
60
|
+
# Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class, which is then passed
|
|
61
|
+
# on to any new database connections made and which can be retrieved on both a class and instance level by calling +logger+.
|
|
62
|
+
cattr_accessor :logger, :instance_writer => false
|
|
63
|
+
|
|
64
|
+
# Constants for special characters in generated IDs. An ID might then look
|
|
65
|
+
# like this: 'United_States-Hawaii-Oahu-Honolulu-b9cef848-a4e0-11dc-a7ba-0018f3137ea8'
|
|
66
|
+
ID_FIELD_SEPARATOR = '-'
|
|
67
|
+
ID_WHITE_SPACE_CHAR = '_'
|
|
68
|
+
|
|
69
|
+
def self.inherited(child) #:nodoc:
|
|
70
|
+
@@subclasses[self] ||= []
|
|
71
|
+
@@subclasses[self] << child
|
|
72
|
+
child.set_table_name child.name.tableize if child.superclass == BigRecord::Base
|
|
73
|
+
super
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.reset_subclasses #:nodoc:
|
|
77
|
+
nonreloadables = []
|
|
78
|
+
subclasses.each do |klass|
|
|
79
|
+
unless Dependencies.autoloaded? klass
|
|
80
|
+
nonreloadables << klass
|
|
81
|
+
next
|
|
82
|
+
end
|
|
83
|
+
klass.instance_variables.each { |var| klass.send(:remove_instance_variable, var) }
|
|
84
|
+
klass.instance_methods(false).each { |m| klass.send :undef_method, m }
|
|
85
|
+
end
|
|
86
|
+
@@subclasses = {}
|
|
87
|
+
nonreloadables.each { |klass| (@@subclasses[klass.superclass] ||= []) << klass }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
@@subclasses = {}
|
|
91
|
+
|
|
92
|
+
def self.store_primary_key?
|
|
93
|
+
false
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
cattr_accessor :configurations, :instance_writer => false
|
|
97
|
+
@@configurations = {}
|
|
98
|
+
|
|
99
|
+
# Accessor for the name of the prefix string to prepend to every table name. So if set to "basecamp_", all
|
|
100
|
+
# table names will be named like "basecamp_projects", "basecamp_people", etc. This is a convenient way of creating a namespace
|
|
101
|
+
# for tables in a shared database. By default, the prefix is the empty string.
|
|
102
|
+
cattr_accessor :table_name_prefix, :instance_writer => false
|
|
103
|
+
@@table_name_prefix = ""
|
|
104
|
+
|
|
105
|
+
# Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp",
|
|
106
|
+
# "people_basecamp"). By default, the suffix is the empty string.
|
|
107
|
+
cattr_accessor :table_name_suffix, :instance_writer => false
|
|
108
|
+
@@table_name_suffix = ""
|
|
109
|
+
|
|
110
|
+
# Indicates whether table names should be the pluralized versions of the corresponding class names.
|
|
111
|
+
# If true, the default table name for a +Product+ class will be +products+. If false, it would just be +product+.
|
|
112
|
+
# See table_name for the full rules on table/class naming. This is true, by default.
|
|
113
|
+
cattr_accessor :pluralize_table_names, :instance_writer => false
|
|
114
|
+
@@pluralize_table_names = true
|
|
115
|
+
|
|
116
|
+
# Determines whether or not to use ANSI codes to colorize the logging statements committed by the connection adapter. These colors
|
|
117
|
+
# make it much easier to overview things during debugging (when used through a reader like +tail+ and on a black background), but
|
|
118
|
+
# may complicate matters if you use software like syslog. This is true, by default.
|
|
119
|
+
cattr_accessor :colorize_logging, :instance_writer => false
|
|
120
|
+
@@colorize_logging = true
|
|
121
|
+
|
|
122
|
+
# Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling dates and times from the database.
|
|
123
|
+
# This is set to :local by default.
|
|
124
|
+
cattr_accessor :default_timezone, :instance_writer => false
|
|
125
|
+
@@default_timezone = :local
|
|
126
|
+
|
|
127
|
+
# Determines whether to speed up access by generating optimized reader
|
|
128
|
+
# methods to avoid expensive calls to method_missing when accessing
|
|
129
|
+
# attributes by name. You might want to set this to false in development
|
|
130
|
+
# mode, because the methods would be regenerated on each request.
|
|
131
|
+
cattr_accessor :generate_read_methods, :instance_writer => false
|
|
132
|
+
@@generate_read_methods = false
|
|
133
|
+
|
|
134
|
+
# Determines whether or not to use a connection for each thread, or a single shared connection for all threads.
|
|
135
|
+
# Defaults to false. Set to true if you're writing a threaded application.
|
|
136
|
+
cattr_accessor :allow_concurrency, :instance_writer => false
|
|
137
|
+
@@allow_concurrency = false
|
|
138
|
+
|
|
139
|
+
# New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
|
|
140
|
+
# attributes but not yet saved (pass a hash with key names matching the associated table column names).
|
|
141
|
+
# In both instances, valid attribute keys are determined by the column names of the associated table --
|
|
142
|
+
# hence you can't have attributes that aren't part of the table columns.
|
|
143
|
+
def initialize(attrs = nil)
|
|
144
|
+
preinitialize(attrs)
|
|
145
|
+
@attributes = attributes_from_column_definition
|
|
146
|
+
self.attributes = attrs unless attrs.nil?
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Callback method meant to be overriden by subclasses if they need to preload some
|
|
150
|
+
# attributes before initializing the record. (usefull when using a meta model where
|
|
151
|
+
# the list of columns depends on the value of an attribute)
|
|
152
|
+
def preinitialize(attrs = nil)
|
|
153
|
+
@attributes = {}
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def deserialize(attrs = nil)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Safe version of attributes= so that objects can be instantiated even
|
|
160
|
+
# if columns are removed.
|
|
161
|
+
def safe_attributes=(new_attributes, guard_protected_attributes = true)
|
|
162
|
+
return if new_attributes.nil?
|
|
163
|
+
attributes = new_attributes.dup
|
|
164
|
+
attributes.stringify_keys!
|
|
165
|
+
|
|
166
|
+
multi_parameter_attributes = []
|
|
167
|
+
attributes = remove_attributes_protected_from_mass_assignment(attributes) if guard_protected_attributes
|
|
168
|
+
|
|
169
|
+
attributes.each do |k, v|
|
|
170
|
+
begin
|
|
171
|
+
k.include?("(") ? multi_parameter_attributes << [ k, v ] : send(k + "=", v)
|
|
172
|
+
rescue
|
|
173
|
+
logger.debug "#{__FILE__}:#{__LINE__} Warning! Ignoring attribute '#{k}' because it doesn't exist anymore"
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
begin
|
|
178
|
+
assign_multiparameter_attributes(multi_parameter_attributes)
|
|
179
|
+
rescue
|
|
180
|
+
logger.debug "#{__FILE__}:#{__LINE__} Warning! Ignoring multiparameter attributes because some don't exist anymore"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# A model instance's primary key is always available as model.id
|
|
185
|
+
# whether you name it the default 'id' or set it to something else.
|
|
186
|
+
def id
|
|
187
|
+
attr_name = self.class.primary_key
|
|
188
|
+
c = column_for_attribute(attr_name)
|
|
189
|
+
define_read_method(:id, attr_name, c) if self.class.generate_read_methods
|
|
190
|
+
read_attribute(attr_name)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def to_s
|
|
194
|
+
id
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def attributes()
|
|
198
|
+
@attributes.dup
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Reloads the attributes of this object from the database.
|
|
202
|
+
# The optional options argument is passed to find when reloading so you
|
|
203
|
+
# may do e.g. record.reload(:lock => true) to reload the same record with
|
|
204
|
+
# an exclusive row lock.
|
|
205
|
+
def reload(options = nil)
|
|
206
|
+
@attributes.update(self.class.find(self.id, options).instance_variable_get('@attributes'))
|
|
207
|
+
self
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
|
|
211
|
+
# "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
|
|
212
|
+
# (Alias for the protected read_attribute method).
|
|
213
|
+
def [](attr_name)
|
|
214
|
+
read_attribute(attr_name)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
|
|
218
|
+
# (Alias for the protected write_attribute method).
|
|
219
|
+
def []=(attr_name, value)
|
|
220
|
+
write_attribute(attr_name, value)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Allows you to set all the attributes at once by passing in a hash with keys
|
|
224
|
+
# matching the attribute names (which again matches the column names). Sensitive attributes can be protected
|
|
225
|
+
# from this form of mass-assignment by using the +attr_protected+ macro. Or you can alternatively
|
|
226
|
+
# specify which attributes *can* be accessed in with the +attr_accessible+ macro. Then all the
|
|
227
|
+
# attributes not included in that won't be allowed to be mass-assigned.
|
|
228
|
+
def attributes=(new_attributes, guard_protected_attributes = true)
|
|
229
|
+
return if new_attributes.nil?
|
|
230
|
+
attributes = new_attributes.dup
|
|
231
|
+
attributes.stringify_keys!
|
|
232
|
+
|
|
233
|
+
multi_parameter_attributes = []
|
|
234
|
+
attributes = remove_attributes_protected_from_mass_assignment(attributes) if guard_protected_attributes
|
|
235
|
+
|
|
236
|
+
attributes.each do |k, v|
|
|
237
|
+
k.include?("(") ? multi_parameter_attributes << [ k, v ] : send(k + "=", v)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
assign_multiparameter_attributes(multi_parameter_attributes)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def all_attributes_loaded=(loaded)
|
|
244
|
+
@all_attributes_loaded = loaded
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def all_attributes_loaded?
|
|
248
|
+
@all_attributes_loaded
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Format attributes nicely for inspect.
|
|
252
|
+
def attribute_for_inspect(attr_name)
|
|
253
|
+
value = read_attribute(attr_name)
|
|
254
|
+
|
|
255
|
+
if value.is_a?(String) && value.length > 50
|
|
256
|
+
"#{value[0..50]}...".inspect
|
|
257
|
+
elsif value.is_a?(Date) || value.is_a?(Time)
|
|
258
|
+
%("#{value.to_s(:db)}")
|
|
259
|
+
else
|
|
260
|
+
value.inspect
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Returns true if the specified +attribute+ has been set by the user or by a database load and is neither
|
|
265
|
+
# nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings).
|
|
266
|
+
def attribute_present?(attribute)
|
|
267
|
+
value = read_attribute(attribute)
|
|
268
|
+
!value.blank? or value == 0
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Returns true if the given attribute is in the attributes hash
|
|
272
|
+
def has_attribute?(attr_name)
|
|
273
|
+
@attributes.has_key?(attr_name.to_s)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Returns an array of names for the attributes available on this object sorted alphabetically.
|
|
277
|
+
def attribute_names
|
|
278
|
+
@attributes.keys.sort
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def human_attribute_name(attribute_key)
|
|
282
|
+
self.class.human_attribute_name(attribute_key)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Overridden by FamilySpanColumns
|
|
286
|
+
# Returns the column object for the named attribute.
|
|
287
|
+
def column_for_attribute(name)
|
|
288
|
+
self.class.columns_hash[name.to_s]
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Returns true if the +comparison_object+ is the same object, or is of the same type and has the same id.
|
|
292
|
+
def ==(comparison_object)
|
|
293
|
+
comparison_object.equal?(self) ||
|
|
294
|
+
(comparison_object.instance_of?(self.class) &&
|
|
295
|
+
comparison_object.id == id &&
|
|
296
|
+
!comparison_object.new_record?)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Delegates to ==
|
|
300
|
+
def eql?(comparison_object)
|
|
301
|
+
self == (comparison_object)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Delegates to id in order to allow two records of the same type and id to work with something like:
|
|
305
|
+
# [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
|
|
306
|
+
def hash
|
|
307
|
+
id.hash
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# For checking respond_to? without searching the attributes (which is faster).
|
|
311
|
+
alias_method :respond_to_without_attributes?, :respond_to?
|
|
312
|
+
|
|
313
|
+
# A Person object with a name attribute can ask person.respond_to?("name"), person.respond_to?("name="), and
|
|
314
|
+
# person.respond_to?("name?") which will all return true.
|
|
315
|
+
def respond_to?(method, include_priv = false)
|
|
316
|
+
if @attributes.nil?
|
|
317
|
+
return super
|
|
318
|
+
elsif attr_name = self.class.column_methods_hash[method.to_sym]
|
|
319
|
+
return true if @attributes.include?(attr_name) || attr_name == self.class.primary_key
|
|
320
|
+
return false if self.class.read_methods.include?(attr_name)
|
|
321
|
+
elsif @attributes.include?(method.to_s)
|
|
322
|
+
return true
|
|
323
|
+
elsif md = self.class.match_attribute_method?(method.to_s)
|
|
324
|
+
return true if @attributes.include?(md.pre_match)
|
|
325
|
+
end
|
|
326
|
+
# super must be called at the end of the method, because the inherited respond_to?
|
|
327
|
+
# would return true for generated readers, even if the attribute wasn't present
|
|
328
|
+
super
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Just freeze the attributes hash, such that associations are still accessible even on destroyed records.
|
|
332
|
+
def freeze
|
|
333
|
+
@attributes.freeze; self
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def frozen?
|
|
337
|
+
@attributes.frozen?
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def quoted_id #:nodoc:
|
|
341
|
+
quote_value(id, column_for_attribute(self.class.primary_key))
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Sets the primary ID.
|
|
345
|
+
def id=(value)
|
|
346
|
+
write_attribute(self.class.primary_key, value)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet.
|
|
350
|
+
def new_record?
|
|
351
|
+
false
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# * No record exists: Creates a new record with values matching those of the object attributes.
|
|
355
|
+
# * A record does exist: Updates the record with values matching those of the object attributes.
|
|
356
|
+
def save
|
|
357
|
+
raise NotImplemented
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Attempts to save the record, but instead of just returning false if it couldn't happen, it raises a
|
|
361
|
+
# RecordNotSaved exception
|
|
362
|
+
def save!
|
|
363
|
+
raise NotImplemented
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Deletes the record in the database and freezes this instance to reflect that no changes should
|
|
367
|
+
# be made (since they can't be persisted).
|
|
368
|
+
def destroy
|
|
369
|
+
raise NotImplemented
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Updates a single attribute and saves the record. This is especially useful for boolean flags on existing records.
|
|
373
|
+
# Note: This method is overwritten by the Validation module that'll make sure that updates made with this method
|
|
374
|
+
# doesn't get subjected to validation checks. Hence, attributes can be updated even if the full object isn't valid.
|
|
375
|
+
def update_attribute(name, value)
|
|
376
|
+
raise NotImplemented
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Updates all the attributes from the passed-in Hash and saves the record. If the object is invalid, the saving will
|
|
380
|
+
# fail and false will be returned.
|
|
381
|
+
def update_attributes(attributes)
|
|
382
|
+
raise NotImplemented
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# Updates an object just like Base.update_attributes but calls save! instead of save so an exception is raised if the record is invalid.
|
|
386
|
+
def update_attributes!(attributes)
|
|
387
|
+
raise NotImplemented
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def connection
|
|
391
|
+
self.class.connection
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Records loaded through joins with piggy-back attributes will be marked as read only as they cannot be saved and return true to this query.
|
|
395
|
+
def readonly?
|
|
396
|
+
@readonly == true
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def readonly! #:nodoc:
|
|
400
|
+
@readonly = true
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Returns the contents of the record as a nicely formatted string.
|
|
404
|
+
def inspect
|
|
405
|
+
attributes_as_nice_string = self.class.column_names.collect { |name|
|
|
406
|
+
if has_attribute?(name) || new_record?
|
|
407
|
+
"#{name}: #{attribute_for_inspect(name)}"
|
|
408
|
+
end
|
|
409
|
+
}.compact.join(", ")
|
|
410
|
+
"#<#{self.class} #{attributes_as_nice_string}>"
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
protected
|
|
414
|
+
def clone_in_persistence_format
|
|
415
|
+
validate_attributes_schema
|
|
416
|
+
|
|
417
|
+
data = {}
|
|
418
|
+
|
|
419
|
+
# normalized attributes without the id
|
|
420
|
+
@attributes.keys.each do |key|
|
|
421
|
+
next if !self.class.store_primary_key? and (key == self.class.primary_key)
|
|
422
|
+
value = read_attribute(key)
|
|
423
|
+
if value.kind_of?(Embedded)
|
|
424
|
+
data[key] = value.clone_in_persistence_format
|
|
425
|
+
elsif value.is_a?(Array)
|
|
426
|
+
data[key] = value.collect do |e|
|
|
427
|
+
if e.kind_of?(Embedded)
|
|
428
|
+
e.clone_in_persistence_format
|
|
429
|
+
else
|
|
430
|
+
e
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
else
|
|
434
|
+
data[key] = value
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
data
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Validate the type of the values in the attributes hash
|
|
441
|
+
def validate_attributes_schema
|
|
442
|
+
@attributes.keys.each do |key|
|
|
443
|
+
value = read_attribute(key)
|
|
444
|
+
next unless value
|
|
445
|
+
# type validation
|
|
446
|
+
if (column = column_for_attribute(key)) and !key.ends_with?(":")
|
|
447
|
+
if column.collection?
|
|
448
|
+
unless value.is_a?(Array)
|
|
449
|
+
raise WrongAttributeDataType, "#{human_attribute_name(column.name)} has the wrong type. Expected collection of #{column.klass}. Record is #{value.class}"
|
|
450
|
+
end
|
|
451
|
+
value.each do |v|
|
|
452
|
+
validate_attribute_type(v, column)
|
|
453
|
+
end
|
|
454
|
+
else
|
|
455
|
+
validate_attribute_type(value, column)
|
|
456
|
+
end
|
|
457
|
+
else
|
|
458
|
+
# Don't save attributes set in a previous schema version
|
|
459
|
+
@attributes.delete(key)
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def validate_attribute_type(value, column)
|
|
465
|
+
unless (value == nil) or value.kind_of?(column.klass)
|
|
466
|
+
raise WrongAttributeDataType, "#{human_attribute_name(column.name)} has the wrong type. Expected #{column.klass}. Record is #{value.class}"
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# Generate a new id. Override this to use custom ids.
|
|
471
|
+
def generate_new_id
|
|
472
|
+
UUIDTools::UUID.random_create.to_s
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def self.random_id #not necessarily unique! -- this is strictly for 'stumbling', not for assigning to new entities
|
|
476
|
+
[8,4,4,4,12].map{|l| "%0#{l}x" % rand(1 << l*4) }.join('-')
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
# Initializes the attributes array with keys matching the columns from the linked table and
|
|
480
|
+
# the values matching the corresponding default value of that column, so
|
|
481
|
+
# that a new instance, or one populated from a passed-in Hash, still has all the attributes
|
|
482
|
+
# that instances loaded from the database would.
|
|
483
|
+
def attributes_from_column_definition
|
|
484
|
+
self.class.columns.inject({}) do |attributes, column|
|
|
485
|
+
unless column.name == self.class.primary_key
|
|
486
|
+
attributes[column.name] = column.default
|
|
487
|
+
end
|
|
488
|
+
attributes
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
|
|
493
|
+
# by calling new on the column type or aggregation type (through composed_of) object with these parameters.
|
|
494
|
+
# So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
|
|
495
|
+
# written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
|
|
496
|
+
# parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, f for Float,
|
|
497
|
+
# s for String, and a for Array. If all the values for a given attribute are empty, the attribute will be set to nil.
|
|
498
|
+
def assign_multiparameter_attributes(pairs)
|
|
499
|
+
execute_callstack_for_multiparameter_attributes(
|
|
500
|
+
extract_callstack_for_multiparameter_attributes(pairs)
|
|
501
|
+
)
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# Includes an ugly hack for Time.local instead of Time.new because the latter is reserved by Time itself.
|
|
505
|
+
def execute_callstack_for_multiparameter_attributes(callstack)
|
|
506
|
+
errors = []
|
|
507
|
+
callstack.each do |name, values|
|
|
508
|
+
# TODO: handle aggregation reflections
|
|
509
|
+
# klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
|
|
510
|
+
column = column_for_attribute(name)
|
|
511
|
+
if column
|
|
512
|
+
klass = column.klass
|
|
513
|
+
|
|
514
|
+
# Ugly fix for time selectors so that when any value is invalid the value is considered invalid, hence nil
|
|
515
|
+
if values.empty? or (column.type == :time and !values[-2..-1].all?) or ([:date, :datetime].include?(column.type) and !values.all?)
|
|
516
|
+
send(name + "=", nil)
|
|
517
|
+
else
|
|
518
|
+
# End of the ugly time fix...
|
|
519
|
+
values = [2000, 1, 1, values[-2], values[-1]] if column.type == :time and !values[0..2].all?
|
|
520
|
+
begin
|
|
521
|
+
send(name + "=", Time == klass ? (@@default_timezone == :utc ? klass.utc(*values) : klass.local(*values)) : klass.new(*values))
|
|
522
|
+
rescue => ex
|
|
523
|
+
errors << AttributeAssignmentError.new("error on assignment #{values.inspect} to #{name}", ex, name)
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
unless errors.empty?
|
|
529
|
+
raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes"
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def extract_callstack_for_multiparameter_attributes(pairs)
|
|
534
|
+
attributes = { }
|
|
535
|
+
|
|
536
|
+
for pair in pairs
|
|
537
|
+
multiparameter_name, value = pair
|
|
538
|
+
attribute_name = multiparameter_name.split("(").first
|
|
539
|
+
attributes[attribute_name] = [] unless attributes.include?(attribute_name)
|
|
540
|
+
|
|
541
|
+
position = find_parameter_position(multiparameter_name)
|
|
542
|
+
value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
|
|
543
|
+
attributes[attribute_name] << [position, value]
|
|
544
|
+
end
|
|
545
|
+
attributes.each { |name, values| attributes[name] = values.sort_by{ |v| v.first }.collect { |v| v.last } }
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def type_cast_attribute_value(multiparameter_name, value)
|
|
549
|
+
multiparameter_name =~ /\([0-9]*([a-z])\)/ ? value.send("to_" + $1) : value
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def find_parameter_position(multiparameter_name)
|
|
553
|
+
multiparameter_name.scan(/\(([0-9]*).*\)/).first.first
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
# Quote strings appropriately for SQL statements.
|
|
557
|
+
def quote_value(value, column = nil)
|
|
558
|
+
self.class.connection.quote(value, column)
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def create_or_update
|
|
562
|
+
raise NotImplemented
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# Creates a record with values matching those of the instance attributes
|
|
566
|
+
# and returns its id. Generate a UUID as the row key.
|
|
567
|
+
def create
|
|
568
|
+
raise NotImplemented
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
# Updates the associated record with values matching those of the instance attributes.
|
|
572
|
+
# Returns the number of affected rows.
|
|
573
|
+
def update
|
|
574
|
+
raise NotImplemented
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
# Update this record in hbase. Cannot be directly in the method 'update' because it would trigger callbacks and
|
|
578
|
+
# therefore weird behaviors.
|
|
579
|
+
def update_bigrecord
|
|
580
|
+
timestamp = self.respond_to?(:updated_at) ? self.updated_at.to_bigrecord_timestamp : Time.now.to_bigrecord_timestamp
|
|
581
|
+
connection.update(self.class.table_name, id, clone_in_persistence_format, timestamp)
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# Allows access to the object attributes, which are held in the @attributes hash, as were
|
|
585
|
+
# they first-class methods. So a Person class with a name attribute can use Person#name and
|
|
586
|
+
# Person#name= and never directly use the attributes hash -- except for multiple assigns with
|
|
587
|
+
# ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that
|
|
588
|
+
# the completed attribute is not nil or 0.
|
|
589
|
+
#
|
|
590
|
+
# It's also possible to instantiate related objects, so a Client class belonging to the clients
|
|
591
|
+
# table with a master_id foreign key can instantiate master through Client#master.
|
|
592
|
+
def method_missing(method_id, *args, &block)
|
|
593
|
+
method_name = method_id.to_s
|
|
594
|
+
if column_for_attribute(method_name) or
|
|
595
|
+
((md = /\?$/.match(method_name)) and
|
|
596
|
+
column_for_attribute(query_method_name = md.pre_match) and
|
|
597
|
+
method_name = query_method_name)
|
|
598
|
+
define_read_methods if self.class.read_methods.empty? && self.class.generate_read_methods
|
|
599
|
+
md ? query_attribute(method_name) : read_attribute(method_name)
|
|
600
|
+
elsif self.class.primary_key.to_s == method_name
|
|
601
|
+
id
|
|
602
|
+
elsif (md = self.class.match_attribute_method?(method_name))
|
|
603
|
+
attribute_name, method_type = md.pre_match, md.to_s
|
|
604
|
+
if column_for_attribute(attribute_name)
|
|
605
|
+
__send__("attribute#{method_type}", attribute_name, *args, &block)
|
|
606
|
+
else
|
|
607
|
+
super
|
|
608
|
+
end
|
|
609
|
+
else
|
|
610
|
+
super
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def remove_attributes_protected_from_mass_assignment(attributes)
|
|
615
|
+
safe_attributes =
|
|
616
|
+
if self.class.accessible_attributes.nil? && self.class.protected_attributes.nil?
|
|
617
|
+
attributes.reject { |key, value| attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
|
|
618
|
+
elsif self.class.protected_attributes.nil?
|
|
619
|
+
attributes.reject { |key, value| !self.class.accessible_attributes.include?(key.gsub(/\(.+/, "")) || attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
|
|
620
|
+
elsif self.class.accessible_attributes.nil?
|
|
621
|
+
attributes.reject { |key, value| self.class.protected_attributes.include?(key.gsub(/\(.+/,"")) || attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
|
|
622
|
+
else
|
|
623
|
+
raise "Declare either attr_protected or attr_accessible for #{self.class}, but not both."
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
if !self.new_record? && !self.class.create_accessible_attributes.nil?
|
|
627
|
+
safe_attributes = safe_attributes.delete_if{ |key, value| self.class.create_accessible_attributes.include?(key.gsub(/\(.+/,"")) }
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
removed_attributes = attributes.keys - safe_attributes.keys
|
|
631
|
+
|
|
632
|
+
if removed_attributes.any?
|
|
633
|
+
log_protected_attribute_removal(removed_attributes)
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
safe_attributes
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
# Removes attributes which have been marked as readonly.
|
|
640
|
+
def remove_readonly_attributes(attributes)
|
|
641
|
+
unless self.class.readonly_attributes.nil?
|
|
642
|
+
attributes.delete_if { |key, value| self.class.readonly_attributes.include?(key.gsub(/\(.+/,"")) }
|
|
643
|
+
else
|
|
644
|
+
attributes
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def log_protected_attribute_removal(*attributes)
|
|
649
|
+
logger.debug "WARNING: Can't mass-assign these protected attributes: #{attributes.join(', ')}"
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
# The primary key and inheritance column can never be set by mass-assignment for security reasons.
|
|
653
|
+
def attributes_protected_by_default
|
|
654
|
+
# default = [ self.class.primary_key, self.class.inheritance_column ]
|
|
655
|
+
# default << 'id' unless self.class.primary_key.eql? 'id'
|
|
656
|
+
# default
|
|
657
|
+
[]
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
protected
|
|
661
|
+
# Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
|
|
662
|
+
# "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
|
|
663
|
+
def read_attribute(attr_name)
|
|
664
|
+
attr_name = attr_name.to_s
|
|
665
|
+
if !(value = @attributes[attr_name]).nil?
|
|
666
|
+
if column = column_for_attribute(attr_name)
|
|
667
|
+
write_attribute(attr_name, column.type_cast(value))
|
|
668
|
+
else
|
|
669
|
+
value
|
|
670
|
+
end
|
|
671
|
+
else
|
|
672
|
+
nil
|
|
673
|
+
end
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
def read_attribute_before_type_cast(attr_name)
|
|
677
|
+
@attributes[attr_name.to_s]
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
private
|
|
681
|
+
# Called on first read access to any given column and generates reader
|
|
682
|
+
# methods for all columns in the columns_hash if
|
|
683
|
+
# ActiveRecord::Base.generate_read_methods is set to true.
|
|
684
|
+
def define_read_methods
|
|
685
|
+
self.class.columns_hash.each do |name, column|
|
|
686
|
+
unless respond_to_without_attributes?(name)
|
|
687
|
+
define_read_method(name.to_sym, name, column)
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
unless respond_to_without_attributes?("#{name}?")
|
|
691
|
+
define_question_method(name)
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
# Define an attribute reader method. Cope with nil column.
|
|
697
|
+
def define_read_method(symbol, attr_name, column)
|
|
698
|
+
cast_code = column.type_cast_code('v') if column
|
|
699
|
+
access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
|
|
700
|
+
|
|
701
|
+
unless attr_name.to_s == self.class.primary_key.to_s
|
|
702
|
+
access_code = access_code.insert(0, "raise NoMethodError, 'missing attribute: #{attr_name}', caller unless @attributes.has_key?('#{attr_name}'); ")
|
|
703
|
+
self.class.read_methods << attr_name
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
evaluate_read_method attr_name, "def #{symbol}; #{access_code}; end"
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
# Define an attribute ? method.
|
|
710
|
+
def define_question_method(attr_name)
|
|
711
|
+
unless attr_name.to_s == self.class.primary_key.to_s
|
|
712
|
+
self.class.read_methods << "#{attr_name}?"
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
evaluate_read_method attr_name, "def #{attr_name}?; query_attribute('#{attr_name}'); end"
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
# Evaluate the definition for an attribute reader or ? method
|
|
719
|
+
def evaluate_read_method(attr_name, method_definition)
|
|
720
|
+
begin
|
|
721
|
+
self.class.class_eval(method_definition)
|
|
722
|
+
rescue SyntaxError => err
|
|
723
|
+
self.class.read_methods.delete(attr_name)
|
|
724
|
+
if logger
|
|
725
|
+
logger.warn "Exception occurred during reader method compilation."
|
|
726
|
+
logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?"
|
|
727
|
+
logger.warn "#{err.message}"
|
|
728
|
+
end
|
|
729
|
+
end
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
# Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float
|
|
733
|
+
# columns are turned into nil.
|
|
734
|
+
def write_attribute(attr_name, value)
|
|
735
|
+
attr_name = attr_name.to_s
|
|
736
|
+
column = column_for_attribute(attr_name)
|
|
737
|
+
|
|
738
|
+
raise "Invalid column for this bigrecord object (e.g., you tried to set a predicate value for an entity that is out of the predicate scope)" if column == nil
|
|
739
|
+
|
|
740
|
+
if column.number?
|
|
741
|
+
@attributes[attr_name] = convert_number_column_value(value)
|
|
742
|
+
else
|
|
743
|
+
@attributes[attr_name] = value
|
|
744
|
+
end
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
def convert_number_column_value(value)
|
|
748
|
+
case value
|
|
749
|
+
when FalseClass then 0
|
|
750
|
+
when TrueClass then 1
|
|
751
|
+
when '' then nil
|
|
752
|
+
else value
|
|
753
|
+
end
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
public
|
|
757
|
+
class << self
|
|
758
|
+
|
|
759
|
+
# Evaluate the name of the column of the primary key only once
|
|
760
|
+
def primary_key
|
|
761
|
+
raise NotImplemented
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
# # Returns a string like 'Post id:integer, title:string, body:text'
|
|
765
|
+
# def inspect
|
|
766
|
+
# if self == Base
|
|
767
|
+
# super
|
|
768
|
+
# elsif abstract_class?
|
|
769
|
+
# "#{super}(abstract)"
|
|
770
|
+
# elsif table_exists?
|
|
771
|
+
# attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', '
|
|
772
|
+
# "#{super}(#{attr_list})"
|
|
773
|
+
# else
|
|
774
|
+
# "#{super}(Table doesn't exist)"
|
|
775
|
+
# end
|
|
776
|
+
# end
|
|
777
|
+
|
|
778
|
+
# Log and benchmark multiple statements in a single block. Example:
|
|
779
|
+
#
|
|
780
|
+
# Project.benchmark("Creating project") do
|
|
781
|
+
# project = Project.create("name" => "stuff")
|
|
782
|
+
# project.create_manager("name" => "David")
|
|
783
|
+
# project.milestones << Milestone.find(:all)
|
|
784
|
+
# end
|
|
785
|
+
#
|
|
786
|
+
# The benchmark is only recorded if the current level of the logger matches the <tt>log_level</tt>, which makes it
|
|
787
|
+
# easy to include benchmarking statements in production software that will remain inexpensive because the benchmark
|
|
788
|
+
# will only be conducted if the log level is low enough.
|
|
789
|
+
#
|
|
790
|
+
# The logging of the multiple statements is turned off unless <tt>use_silence</tt> is set to false.
|
|
791
|
+
def benchmark(title, log_level = Logger::DEBUG, use_silence = true)
|
|
792
|
+
if logger && logger.level == log_level
|
|
793
|
+
result = nil
|
|
794
|
+
seconds = Benchmark.realtime { result = use_silence ? silence { yield } : yield }
|
|
795
|
+
logger.add(log_level, "#{title} (#{'%.5f' % seconds})")
|
|
796
|
+
result
|
|
797
|
+
else
|
|
798
|
+
yield
|
|
799
|
+
end
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
# Silences the logger for the duration of the block.
|
|
803
|
+
def silence
|
|
804
|
+
old_logger_level, logger.level = logger.level, Logger::ERROR if logger
|
|
805
|
+
yield
|
|
806
|
+
ensure
|
|
807
|
+
logger.level = old_logger_level if logger
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
# Overwrite the default class equality method to provide support for association proxies.
|
|
811
|
+
def ===(object)
|
|
812
|
+
object.is_a?(self)
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
def base_class
|
|
816
|
+
raise NotImplemented
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
# Override this method in the subclasses to add new columns. This is different from ActiveRecord because
|
|
820
|
+
# the number of columns in a Hbase table is variable.
|
|
821
|
+
def columns
|
|
822
|
+
@columns = columns_hash.values
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
# Returns a hash of column objects for the table associated with this class.
|
|
826
|
+
def columns_hash
|
|
827
|
+
unless @all_columns_hash
|
|
828
|
+
# add default hbase columns
|
|
829
|
+
@all_columns_hash =
|
|
830
|
+
if self == base_class
|
|
831
|
+
if @columns_hash
|
|
832
|
+
default_columns.merge(@columns_hash)
|
|
833
|
+
else
|
|
834
|
+
@columns_hash = default_columns
|
|
835
|
+
end
|
|
836
|
+
else
|
|
837
|
+
if @columns_hash
|
|
838
|
+
superclass.columns_hash.merge(@columns_hash)
|
|
839
|
+
else
|
|
840
|
+
superclass.columns_hash
|
|
841
|
+
end
|
|
842
|
+
end
|
|
843
|
+
end
|
|
844
|
+
@all_columns_hash
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
# Returns an array of column names as strings.
|
|
848
|
+
def column_names
|
|
849
|
+
@column_names = columns_hash.keys
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
# Returns an array of column objects where the primary id, all columns ending in "_id" or "_count",
|
|
853
|
+
# and columns used for single table inheritance have been removed.
|
|
854
|
+
def content_columns
|
|
855
|
+
@content_columns ||= columns.reject{|c| c.primary || "id"}
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
# Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key
|
|
859
|
+
# and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute
|
|
860
|
+
# is available.
|
|
861
|
+
def column_methods_hash #:nodoc:
|
|
862
|
+
@dynamic_methods_hash ||= column_names.inject(Hash.new(false)) do |methods, attr|
|
|
863
|
+
attr_name = attr.to_s
|
|
864
|
+
methods[attr.to_sym] = attr_name
|
|
865
|
+
methods["#{attr}=".to_sym] = attr_name
|
|
866
|
+
methods["#{attr}?".to_sym] = attr_name
|
|
867
|
+
methods["#{attr}_before_type_cast".to_sym] = attr_name
|
|
868
|
+
methods
|
|
869
|
+
end
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
def column(name, type, options={})
|
|
873
|
+
name = name.to_s
|
|
874
|
+
|
|
875
|
+
@columns_hash = default_columns unless @columns_hash
|
|
876
|
+
|
|
877
|
+
# The other variables that are cached and depend on @columns_hash need to be reloaded
|
|
878
|
+
invalidate_columns
|
|
879
|
+
|
|
880
|
+
c = create_column(name, type, options)
|
|
881
|
+
@columns_hash[c.name] = c
|
|
882
|
+
|
|
883
|
+
alias_attribute c.alias, c.name if c.alias
|
|
884
|
+
|
|
885
|
+
c
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
def create_column(name, type, options)
|
|
889
|
+
ConnectionAdapters::Column.new(name, type, options)
|
|
890
|
+
end
|
|
891
|
+
|
|
892
|
+
# Define aliases to the fully qualified attributes
|
|
893
|
+
def alias_attribute(alias_name, fully_qualified_name)
|
|
894
|
+
self.class_eval <<-EOF
|
|
895
|
+
def #{alias_name}
|
|
896
|
+
read_attribute("#{fully_qualified_name}")
|
|
897
|
+
end
|
|
898
|
+
def #{alias_name}=(value)
|
|
899
|
+
write_attribute("#{fully_qualified_name}", value)
|
|
900
|
+
end
|
|
901
|
+
EOF
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
# Contains the names of the generated reader methods.
|
|
905
|
+
def read_methods #:nodoc:
|
|
906
|
+
@read_methods ||= Set.new
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
# Transforms attribute key names into a more humane format, such as "First name" instead of "first_name". Example:
|
|
910
|
+
# Person.human_attribute_name("first_name") # => "First name"
|
|
911
|
+
# Deprecated in favor of just calling "first_name".humanize
|
|
912
|
+
def human_attribute_name(attribute_key_name) #:nodoc:
|
|
913
|
+
attribute_key_name.humanize
|
|
914
|
+
end
|
|
915
|
+
|
|
916
|
+
def quote_value(value, c = nil) #:nodoc:
|
|
917
|
+
connection.quote(value,c)
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
# Finder methods must instantiate through this method.
|
|
921
|
+
def instantiate(raw_record)
|
|
922
|
+
record = self.allocate
|
|
923
|
+
record.deserialize(raw_record)
|
|
924
|
+
record.preinitialize(raw_record)
|
|
925
|
+
record.instance_variable_set(:@new_record, false)
|
|
926
|
+
record.send("safe_attributes=", raw_record, false)
|
|
927
|
+
record
|
|
928
|
+
end
|
|
929
|
+
|
|
930
|
+
# Attributes named in this macro are protected from mass-assignment,
|
|
931
|
+
# such as <tt>new(attributes)</tt>,
|
|
932
|
+
# <tt>update_attributes(attributes)</tt>, or
|
|
933
|
+
# <tt>attributes=(attributes)</tt>.
|
|
934
|
+
#
|
|
935
|
+
# Mass-assignment to these attributes will simply be ignored, to assign
|
|
936
|
+
# to them you can use direct writer methods. This is meant to protect
|
|
937
|
+
# sensitive attributes from being overwritten by malicious users
|
|
938
|
+
# tampering with URLs or forms.
|
|
939
|
+
#
|
|
940
|
+
# class Customer < ActiveRecord::Base
|
|
941
|
+
# attr_protected :credit_rating
|
|
942
|
+
# end
|
|
943
|
+
#
|
|
944
|
+
# customer = Customer.new("name" => David, "credit_rating" => "Excellent")
|
|
945
|
+
# customer.credit_rating # => nil
|
|
946
|
+
# customer.attributes = { "description" => "Jolly fellow", "credit_rating" => "Superb" }
|
|
947
|
+
# customer.credit_rating # => nil
|
|
948
|
+
#
|
|
949
|
+
# customer.credit_rating = "Average"
|
|
950
|
+
# customer.credit_rating # => "Average"
|
|
951
|
+
#
|
|
952
|
+
# To start from an all-closed default and enable attributes as needed,
|
|
953
|
+
# have a look at +attr_accessible+.
|
|
954
|
+
def attr_protected(*attributes)
|
|
955
|
+
write_inheritable_attribute(:attr_protected, Set.new(attributes.map {|a| a.to_s}) + (protected_attributes || []))
|
|
956
|
+
end
|
|
957
|
+
|
|
958
|
+
# Returns an array of all the attributes that have been protected from mass-assignment.
|
|
959
|
+
def protected_attributes # :nodoc:
|
|
960
|
+
read_inheritable_attribute(:attr_protected)
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
# Specifies a white list of model attributes that can be set via
|
|
964
|
+
# mass-assignment, such as <tt>new(attributes)</tt>,
|
|
965
|
+
# <tt>update_attributes(attributes)</tt>, or
|
|
966
|
+
# <tt>attributes=(attributes)</tt>
|
|
967
|
+
#
|
|
968
|
+
# This is the opposite of the +attr_protected+ macro: Mass-assignment
|
|
969
|
+
# will only set attributes in this list, to assign to the rest of
|
|
970
|
+
# attributes you can use direct writer methods. This is meant to protect
|
|
971
|
+
# sensitive attributes from being overwritten by malicious users
|
|
972
|
+
# tampering with URLs or forms. If you'd rather start from an all-open
|
|
973
|
+
# default and restrict attributes as needed, have a look at
|
|
974
|
+
# +attr_protected+.
|
|
975
|
+
#
|
|
976
|
+
# class Customer < ActiveRecord::Base
|
|
977
|
+
# attr_accessible :name, :nickname
|
|
978
|
+
# end
|
|
979
|
+
#
|
|
980
|
+
# customer = Customer.new(:name => "David", :nickname => "Dave", :credit_rating => "Excellent")
|
|
981
|
+
# customer.credit_rating # => nil
|
|
982
|
+
# customer.attributes = { :name => "Jolly fellow", :credit_rating => "Superb" }
|
|
983
|
+
# customer.credit_rating # => nil
|
|
984
|
+
#
|
|
985
|
+
# customer.credit_rating = "Average"
|
|
986
|
+
# customer.credit_rating # => "Average"
|
|
987
|
+
def attr_accessible(*attributes)
|
|
988
|
+
write_inheritable_attribute(:attr_accessible, Set.new(attributes.map(&:to_s)) + (accessible_attributes || []))
|
|
989
|
+
end
|
|
990
|
+
|
|
991
|
+
# Returns an array of all the attributes that have been made accessible to mass-assignment.
|
|
992
|
+
def accessible_attributes # :nodoc:
|
|
993
|
+
read_inheritable_attribute(:attr_accessible)
|
|
994
|
+
end
|
|
995
|
+
|
|
996
|
+
# Attributes listed as readonly can be set for a new record, but will be ignored in database updates afterwards.
|
|
997
|
+
def attr_readonly(*attributes)
|
|
998
|
+
write_inheritable_attribute(:attr_readonly, Set.new(attributes.map(&:to_s)) + (readonly_attributes || []))
|
|
999
|
+
end
|
|
1000
|
+
|
|
1001
|
+
# Returns an array of all the attributes that have been specified as readonly.
|
|
1002
|
+
def readonly_attributes
|
|
1003
|
+
read_inheritable_attribute(:attr_readonly)
|
|
1004
|
+
end
|
|
1005
|
+
|
|
1006
|
+
# Attributes listed as create_accessible work with mass assignment ONLY on creation. After that, any updates
|
|
1007
|
+
# to that attribute will be protected from mass assignment. This differs from attr_readonly since that macro
|
|
1008
|
+
# prevents attributes from ever being changed (even with the explicit setters) after the record is created.
|
|
1009
|
+
#
|
|
1010
|
+
# class Customer < BigRecord::Base
|
|
1011
|
+
# attr_create_accessible :name
|
|
1012
|
+
# end
|
|
1013
|
+
#
|
|
1014
|
+
# customer = Customer.new(:name => "Greg")
|
|
1015
|
+
# customer.name # => "Greg"
|
|
1016
|
+
# customer.save # => true
|
|
1017
|
+
#
|
|
1018
|
+
# customer.attributes = { :name => "Nerd" }
|
|
1019
|
+
# customer.name # => "Greg"
|
|
1020
|
+
# customer.name = "Nerd"
|
|
1021
|
+
# customer.name # => "Nerd"
|
|
1022
|
+
#
|
|
1023
|
+
def attr_create_accessible(*attributes)
|
|
1024
|
+
write_inheritable_attribute(:attr_create_accessible, Set.new(attributes.map(&:to_s)) + (create_accessible_attributes || []))
|
|
1025
|
+
end
|
|
1026
|
+
|
|
1027
|
+
# Returns an array of all the attributes that have been specified as create_accessible.
|
|
1028
|
+
def create_accessible_attributes
|
|
1029
|
+
read_inheritable_attribute(:attr_create_accessible)
|
|
1030
|
+
end
|
|
1031
|
+
|
|
1032
|
+
protected
|
|
1033
|
+
def invalidate_views
|
|
1034
|
+
@views = nil
|
|
1035
|
+
@view_names = nil
|
|
1036
|
+
end
|
|
1037
|
+
|
|
1038
|
+
def invalidate_columns
|
|
1039
|
+
@columns = nil
|
|
1040
|
+
@column_names = nil
|
|
1041
|
+
@content_columns = nil
|
|
1042
|
+
end
|
|
1043
|
+
|
|
1044
|
+
def default_columns
|
|
1045
|
+
raise NotImplemented
|
|
1046
|
+
end
|
|
1047
|
+
|
|
1048
|
+
def default_views
|
|
1049
|
+
{:all=>ConnectionAdapters::View.new('all', nil, self), :default=>ConnectionAdapters::View.new('default', nil, self)}
|
|
1050
|
+
end
|
|
1051
|
+
|
|
1052
|
+
def subclasses #:nodoc:
|
|
1053
|
+
@@subclasses[self] ||= []
|
|
1054
|
+
@@subclasses[self] + @@subclasses[self].inject([]) {|list, subclass| list + subclass.subclasses }
|
|
1055
|
+
end
|
|
1056
|
+
|
|
1057
|
+
# Returns the class type of the record using the current module as a prefix. So descendents of
|
|
1058
|
+
# MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
|
|
1059
|
+
def compute_type(type_name)
|
|
1060
|
+
type_name.constantize
|
|
1061
|
+
end
|
|
1062
|
+
|
|
1063
|
+
public
|
|
1064
|
+
# Guesses the table name, but does not decorate it with prefix and suffix information.
|
|
1065
|
+
def undecorated_table_name(class_name = base_class.name)
|
|
1066
|
+
table_name = Inflector.underscore(Inflector.demodulize(class_name))
|
|
1067
|
+
table_name = Inflector.pluralize(table_name) if pluralize_table_names
|
|
1068
|
+
table_name
|
|
1069
|
+
end
|
|
1070
|
+
end
|
|
1071
|
+
|
|
1072
|
+
protected
|
|
1073
|
+
# Handle *? for method_missing.
|
|
1074
|
+
def attribute?(attribute_name)
|
|
1075
|
+
query_attribute(attribute_name)
|
|
1076
|
+
end
|
|
1077
|
+
|
|
1078
|
+
# Handle *= for method_missing.
|
|
1079
|
+
def attribute=(attribute_name, value)
|
|
1080
|
+
write_attribute(attribute_name, value)
|
|
1081
|
+
end
|
|
1082
|
+
|
|
1083
|
+
# Handle *_before_type_cast for method_missing.
|
|
1084
|
+
def attribute_before_type_cast(attribute_name)
|
|
1085
|
+
read_attribute_before_type_cast(attribute_name)
|
|
1086
|
+
end
|
|
1087
|
+
end
|
|
1088
|
+
end
|