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