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,618 @@
|
|
1
|
+
## The sub-classes of this class must implement the abstract method 'column_names' of this class.
|
2
|
+
module BigRecord
|
3
|
+
|
4
|
+
class Base < AbstractBase
|
5
|
+
|
6
|
+
attr_accessor :modified_attributes
|
7
|
+
|
8
|
+
|
9
|
+
def self.inherited(child) #:nodoc:
|
10
|
+
@@subclasses[self] ||= []
|
11
|
+
@@subclasses[self] << child
|
12
|
+
child.set_table_name child.name.tableize if child.superclass == BigRecord::Base
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
# New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
|
17
|
+
# attributes but not yet saved (pass a hash with key names matching the associated table column names).
|
18
|
+
# In both instances, valid attribute keys are determined by the column names of the associated table --
|
19
|
+
# hence you can't have attributes that aren't part of the table columns.
|
20
|
+
def initialize(attrs = nil)
|
21
|
+
@new_record = true
|
22
|
+
super
|
23
|
+
attrs.keys.each{ |k| set_loaded(k) } if attrs
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
|
27
|
+
# "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
|
28
|
+
# (Alias for the protected read_attribute method).
|
29
|
+
def [](attr_name)
|
30
|
+
if attr_name.ends_with?(":")
|
31
|
+
read_family_attributes(attr_name)
|
32
|
+
else
|
33
|
+
read_attribute(attr_name)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# protected
|
38
|
+
# Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
|
39
|
+
# "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
|
40
|
+
def read_attribute(attr_name, options={})
|
41
|
+
attr_name = attr_name.to_s
|
42
|
+
column = column_for_attribute(attr_name)
|
43
|
+
if column
|
44
|
+
# First check if the attribute is already in the attributes hash
|
45
|
+
if @attributes.has_key?(attr_name) and options.blank?
|
46
|
+
super(attr_name)
|
47
|
+
# Elsif the column exist, we try to lazy load it
|
48
|
+
elsif !(is_loaded?(attr_name)) and attr_name != self.class.primary_key and !new_record?
|
49
|
+
unless self.all_attributes_loaded? and attr_name =~ /\A#{self.class.default_family}:/
|
50
|
+
if options.blank?
|
51
|
+
# Normal behavior
|
52
|
+
|
53
|
+
# Retrieve the version of the attribute matching the current record version
|
54
|
+
options[:timestamp] = self.updated_at.to_bigrecord_timestamp if self.has_attribute?("#{self.class.default_family}:updated_at") and self.updated_at
|
55
|
+
|
56
|
+
# get the content of the cell
|
57
|
+
value = connection.get(self.class.table_name, self.id, attr_name, options)
|
58
|
+
|
59
|
+
set_loaded(attr_name)
|
60
|
+
write_attribute(attr_name, column.type_cast(value))
|
61
|
+
else
|
62
|
+
# Special request... don't keep it in the attributes hash
|
63
|
+
options[:timestamp] ||= self.updated_at.to_bigrecord_timestamp if self.has_attribute?("#{self.class.default_family}:updated_at") and self.updated_at
|
64
|
+
|
65
|
+
# get the content of the cell
|
66
|
+
value = connection.get(self.class.table_name, self.id, attr_name, options)
|
67
|
+
|
68
|
+
if options[:versions] and options[:versions] > 1
|
69
|
+
value.collect{ |v| column.type_cast(v) }
|
70
|
+
else
|
71
|
+
column.type_cast(value)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
else
|
75
|
+
write_attribute(attr_name, column.default)
|
76
|
+
end
|
77
|
+
else
|
78
|
+
write_attribute(attr_name, column.default)
|
79
|
+
end
|
80
|
+
else
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def read_family_attributes(attr_name)
|
86
|
+
attr_name = attr_name.to_s
|
87
|
+
column = column_for_attribute(attr_name)
|
88
|
+
if column
|
89
|
+
# First check if the attribute is already in the attributes hash
|
90
|
+
if @attributes.has_key?(attr_name)
|
91
|
+
if (values = @attributes[attr_name]) and values.is_a?(Hash)
|
92
|
+
values.delete(self.class.primary_key)
|
93
|
+
casted_values = {}
|
94
|
+
values.each{|k,v| casted_values[k] = column.type_cast(v)}
|
95
|
+
write_attribute(attr_name, casted_values)
|
96
|
+
else
|
97
|
+
write_attribute(attr_name, {})
|
98
|
+
end
|
99
|
+
|
100
|
+
# Elsif the column exist, we try to lazy load it
|
101
|
+
elsif !(is_loaded?(attr_name)) and attr_name != self.class.primary_key and !new_record?
|
102
|
+
unless self.all_attributes_loaded? and attr_name =~ /\A#{self.class.default_family}:/
|
103
|
+
options = {}
|
104
|
+
# Retrieve the version of the attribute matching the current record version
|
105
|
+
options[:timestamp] = self.updated_at.to_bigrecord_timestamp if self.has_attribute?("#{self.class.default_family}:updated_at") and self.updated_at
|
106
|
+
|
107
|
+
# get the content of the whole family
|
108
|
+
values = connection.get_columns(self.class.table_name, self.id, [attr_name], options)
|
109
|
+
if values
|
110
|
+
values.delete(self.class.primary_key)
|
111
|
+
casted_values = {}
|
112
|
+
values.each do |k,v|
|
113
|
+
short_name = k.split(":")[1]
|
114
|
+
casted_values[short_name] = column.type_cast(v) if short_name
|
115
|
+
set_loaded(k)
|
116
|
+
write_attribute(k, casted_values[short_name]) if short_name
|
117
|
+
end
|
118
|
+
write_attribute(attr_name, casted_values)
|
119
|
+
else
|
120
|
+
set_loaded(attr_name)
|
121
|
+
write_attribute(attr_name, {})
|
122
|
+
end
|
123
|
+
else
|
124
|
+
write_attribute(attr_name, column.default)
|
125
|
+
end
|
126
|
+
else
|
127
|
+
write_attribute(attr_name, column.default)
|
128
|
+
end
|
129
|
+
else
|
130
|
+
nil
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def set_loaded(name)
|
135
|
+
@loaded_columns ||= []
|
136
|
+
@loaded_columns << name
|
137
|
+
end
|
138
|
+
|
139
|
+
def is_loaded?(name)
|
140
|
+
@loaded_columns ||= []
|
141
|
+
@loaded_columns.include?(name)
|
142
|
+
end
|
143
|
+
|
144
|
+
public
|
145
|
+
# Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet.
|
146
|
+
def new_record?
|
147
|
+
@new_record
|
148
|
+
end
|
149
|
+
|
150
|
+
# * No record exists: Creates a new record with values matching those of the object attributes.
|
151
|
+
# * A record does exist: Updates the record with values matching those of the object attributes.
|
152
|
+
def save
|
153
|
+
create_or_update
|
154
|
+
end
|
155
|
+
|
156
|
+
# Attempts to save the record, but instead of just returning false if it couldn't happen, it raises a
|
157
|
+
# RecordNotSaved exception
|
158
|
+
def save!
|
159
|
+
create_or_update || raise(RecordNotSaved)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Deletes the record in the database and freezes this instance to reflect that no changes should
|
163
|
+
# be made (since they can't be persisted).
|
164
|
+
def destroy
|
165
|
+
unless new_record?
|
166
|
+
connection.delete(self.class.table_name, self.id)
|
167
|
+
end
|
168
|
+
|
169
|
+
# FIXME: this currently doesn't work because we write the attributes everytime we read them
|
170
|
+
# which means that we cannot read the attributes of a deleted record... it's bad
|
171
|
+
# freeze
|
172
|
+
end
|
173
|
+
|
174
|
+
# Updates a single attribute and saves the record. This is especially useful for boolean flags on existing records.
|
175
|
+
# Note: This method is overwritten by the Validation module that'll make sure that updates made with this method
|
176
|
+
# doesn't get subjected to validation checks. Hence, attributes can be updated even if the full object isn't valid.
|
177
|
+
def update_attribute(name, value)
|
178
|
+
send(name.to_s + '=', value)
|
179
|
+
save
|
180
|
+
end
|
181
|
+
|
182
|
+
# Updates all the attributes from the passed-in Hash and saves the record. If the object is invalid, the saving will
|
183
|
+
# fail and false will be returned.
|
184
|
+
def update_attributes(attributes)
|
185
|
+
self.attributes = attributes
|
186
|
+
save
|
187
|
+
end
|
188
|
+
|
189
|
+
# Updates an object just like Base.update_attributes but calls save! instead of save so an exception is raised if the record is invalid.
|
190
|
+
def update_attributes!(attributes)
|
191
|
+
self.attributes = attributes
|
192
|
+
save!
|
193
|
+
end
|
194
|
+
|
195
|
+
def connection
|
196
|
+
self.class.connection
|
197
|
+
end
|
198
|
+
|
199
|
+
protected
|
200
|
+
|
201
|
+
def create_or_update
|
202
|
+
raise ReadOnlyRecord if readonly?
|
203
|
+
result = new_record? ? create : update
|
204
|
+
result != false
|
205
|
+
end
|
206
|
+
|
207
|
+
# Creates a record with values matching those of the instance attributes
|
208
|
+
# and returns its id. Generate a UUID as the row key.
|
209
|
+
def create
|
210
|
+
self.id = generate_new_id unless self.id
|
211
|
+
@new_record = false
|
212
|
+
update_bigrecord
|
213
|
+
end
|
214
|
+
|
215
|
+
# Updates the associated record with values matching those of the instance attributes.
|
216
|
+
# Returns the number of affected rows.
|
217
|
+
def update
|
218
|
+
update_bigrecord
|
219
|
+
end
|
220
|
+
|
221
|
+
# Update this record in hbase. Cannot be directly in the method 'update' because it would trigger callbacks and
|
222
|
+
# therefore weird behaviors.
|
223
|
+
def update_bigrecord
|
224
|
+
timestamp = self.respond_to?(:updated_at) ? self.updated_at.to_bigrecord_timestamp : Time.now.to_bigrecord_timestamp
|
225
|
+
|
226
|
+
data = clone_in_persistence_format
|
227
|
+
|
228
|
+
connection.update(self.class.table_name, id, data, timestamp)
|
229
|
+
end
|
230
|
+
|
231
|
+
public
|
232
|
+
class << self
|
233
|
+
|
234
|
+
# Replaced with: class_inheritable_accessor :default_family (line 46)
|
235
|
+
# def default_family
|
236
|
+
# "attribute"
|
237
|
+
# end
|
238
|
+
|
239
|
+
def primary_key
|
240
|
+
@primary_key ||= "id"
|
241
|
+
end
|
242
|
+
|
243
|
+
# Return the list of families for this class
|
244
|
+
def families
|
245
|
+
columns.collect(&:family).uniq
|
246
|
+
end
|
247
|
+
|
248
|
+
# HBase scanner utility -- scans the table and executes code on each record
|
249
|
+
# Example:
|
250
|
+
# Entity.scan(:batch_size => 200) {|e|puts "#{e.name} is a child!" if e.parent}
|
251
|
+
#
|
252
|
+
# Parameters:
|
253
|
+
# batch_size - number of records to retrieve from database with each scan iteration.
|
254
|
+
# code - the code to execute (see example above for syntax)
|
255
|
+
#
|
256
|
+
def scan(options={}, &code)
|
257
|
+
options = options.dup
|
258
|
+
limit = options.delete(:batch_size) || 100
|
259
|
+
|
260
|
+
items_processed = 0
|
261
|
+
|
262
|
+
# add an extra record for defining the next offset without duplicating records
|
263
|
+
limit += 1
|
264
|
+
last_row_id = nil
|
265
|
+
|
266
|
+
while true
|
267
|
+
items = find(:all, options.merge({:limit => limit}))
|
268
|
+
|
269
|
+
# set the new offset as the extra record
|
270
|
+
unless items.empty?
|
271
|
+
items.delete_at(0) if items[0].id == last_row_id
|
272
|
+
|
273
|
+
break if items.empty?
|
274
|
+
|
275
|
+
last_row_id = items.last.id
|
276
|
+
options[:offset] = last_row_id
|
277
|
+
items_processed += items.size
|
278
|
+
|
279
|
+
items.each do |item|
|
280
|
+
code.call(item)
|
281
|
+
end
|
282
|
+
else
|
283
|
+
break
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
def find(*args)
|
289
|
+
options = extract_options_from_args!(args)
|
290
|
+
validate_find_options(options)
|
291
|
+
|
292
|
+
# set a default view
|
293
|
+
if options[:view]
|
294
|
+
options[:view] = options[:view].to_sym
|
295
|
+
else
|
296
|
+
options[:view] = :default
|
297
|
+
end
|
298
|
+
|
299
|
+
case args.first
|
300
|
+
when :first then find_every(options.merge({:limit => 1})).first
|
301
|
+
when :all then find_every(options)
|
302
|
+
else find_from_ids(args, options)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
# Returns true if the given +id+ represents the primary key of a record in the database, false otherwise.
|
307
|
+
def exists?(id)
|
308
|
+
!find(id).nil?
|
309
|
+
rescue BigRecord::BigRecordError
|
310
|
+
false
|
311
|
+
end
|
312
|
+
|
313
|
+
# Creates an object, instantly saves it as a record (if the validation permits it), and returns it. If the save
|
314
|
+
# fails under validations, the unsaved object is still returned.
|
315
|
+
def create(attrs = nil)
|
316
|
+
if attrs.is_a?(Array)
|
317
|
+
attrs.collect { |attr| create(attr) }
|
318
|
+
else
|
319
|
+
object = new(attrs)
|
320
|
+
object.save
|
321
|
+
object
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
# Finds the record from the passed +id+, instantly saves it with the passed +attributes+ (if the validation permits it),
|
326
|
+
# and returns it. If the save fails under validations, the unsaved object is still returned.
|
327
|
+
#
|
328
|
+
# The arguments may also be given as arrays in which case the update method is called for each pair of +id+ and
|
329
|
+
# +attributes+ and an array of objects is returned.
|
330
|
+
#
|
331
|
+
# Example of updating one record:
|
332
|
+
# Person.update(15, {:user_name => 'Samuel', :group => 'expert'})
|
333
|
+
#
|
334
|
+
# Example of updating multiple records:
|
335
|
+
# people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy"} }
|
336
|
+
# Person.update(people.keys, people.values)
|
337
|
+
def update(id, attributes)
|
338
|
+
if id.is_a?(Array)
|
339
|
+
idx = -1
|
340
|
+
id.collect { |a| idx += 1; update(a, attributes[idx]) }
|
341
|
+
else
|
342
|
+
object = find(id)
|
343
|
+
object.update_attributes(attributes)
|
344
|
+
object
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
# Deletes the record with the given +id+ without instantiating an object first. If an array of ids is provided, all of them
|
349
|
+
# are deleted.
|
350
|
+
def delete(id)
|
351
|
+
if id.is_a?(Array)
|
352
|
+
id.each { |a| connection.delete(table_name, a) }
|
353
|
+
else
|
354
|
+
connection.delete(table_name, id)
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
# Destroys the record with the given +id+ by instantiating the object and calling #destroy (all the callbacks are the triggered).
|
359
|
+
# If an array of ids is provided, all of them are destroyed.
|
360
|
+
def destroy(id)
|
361
|
+
id.is_a?(Array) ? id.each { |a| destroy(a) } : find(id).destroy
|
362
|
+
end
|
363
|
+
|
364
|
+
# Updates all records with the SET-part of an SQL update statement in +updates+ and returns an integer with the number of rows updated.
|
365
|
+
# A subset of the records can be selected by specifying +conditions+. Example:
|
366
|
+
# Billing.update_all "category = 'authorized', approved = 1", "author = 'David'"
|
367
|
+
def update_all(updates, conditions = nil)
|
368
|
+
raise NotImplemented, "update_all"
|
369
|
+
end
|
370
|
+
|
371
|
+
# Destroys the objects for all the records that match the +condition+ by instantiating each object and calling
|
372
|
+
# the destroy method. Example:
|
373
|
+
# Person.destroy_all "last_login < '2004-04-04'"
|
374
|
+
def destroy_all(conditions = nil)
|
375
|
+
find(:all, :conditions => conditions).each { |object| object.destroy }
|
376
|
+
end
|
377
|
+
|
378
|
+
# Deletes all the records that match the +condition+ without instantiating the objects first (and hence not
|
379
|
+
# calling the destroy method). Example:
|
380
|
+
# Post.delete_all "person_id = 5 AND (category = 'Something' OR category = 'Else')"
|
381
|
+
#
|
382
|
+
# TODO: take into consideration the conditions
|
383
|
+
def delete_all(conditions = nil)
|
384
|
+
connection.get_consecutive_rows(table_name, nil, nil, ["#{default_family}:"]).each do |row|
|
385
|
+
connection.delete(table_name, row["id"])
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
# Truncate the table for this model
|
390
|
+
def truncate
|
391
|
+
connection.truncate_table(table_name)
|
392
|
+
end
|
393
|
+
|
394
|
+
def table_name
|
395
|
+
(superclass == BigRecord::Base) ? @table_name : superclass.table_name
|
396
|
+
end
|
397
|
+
|
398
|
+
def set_table_name(name)
|
399
|
+
@table_name = name.to_s
|
400
|
+
end
|
401
|
+
|
402
|
+
def default_family
|
403
|
+
(superclass == BigRecord::Base) ? (@default_family ||= "attribute") : superclass.default_family
|
404
|
+
end
|
405
|
+
|
406
|
+
def set_default_family(name)
|
407
|
+
@default_family = name.to_s
|
408
|
+
end
|
409
|
+
|
410
|
+
def base_class
|
411
|
+
(superclass == BigRecord::Base) ? self : superclass.base_class
|
412
|
+
end
|
413
|
+
|
414
|
+
def view(name, columns)
|
415
|
+
name = name.to_sym
|
416
|
+
@views_hash ||= default_views
|
417
|
+
|
418
|
+
# The other variables that are cached and depend on @views_hash need to be reloaded
|
419
|
+
invalidate_views
|
420
|
+
|
421
|
+
@views_hash[name] = ConnectionAdapters::View.new(name, columns, self)
|
422
|
+
end
|
423
|
+
|
424
|
+
def views
|
425
|
+
@views ||= views_hash.values
|
426
|
+
end
|
427
|
+
|
428
|
+
def view_names
|
429
|
+
@view_names ||= views_hash.keys
|
430
|
+
end
|
431
|
+
|
432
|
+
def views_hash
|
433
|
+
unless @all_views_hash
|
434
|
+
# add default hbase columns
|
435
|
+
@all_views_hash =
|
436
|
+
if self == BigRecord::Base # stop at Base
|
437
|
+
@views_hash = default_views
|
438
|
+
else
|
439
|
+
if @views_hash
|
440
|
+
superclass.views_hash.merge(default_views).merge(@views_hash)
|
441
|
+
else
|
442
|
+
superclass.views_hash.merge(default_views)
|
443
|
+
end
|
444
|
+
end
|
445
|
+
end
|
446
|
+
@all_views_hash
|
447
|
+
end
|
448
|
+
|
449
|
+
def default_columns
|
450
|
+
{primary_key => ConnectionAdapters::Column.new(primary_key, 'string')}
|
451
|
+
end
|
452
|
+
|
453
|
+
def column(name, type, options={})
|
454
|
+
name = name.to_s
|
455
|
+
name = "#{self.default_family}:#{name}" unless (name =~ /:/)
|
456
|
+
|
457
|
+
super(name, type, options)
|
458
|
+
end
|
459
|
+
|
460
|
+
def default_views
|
461
|
+
{:all=>ConnectionAdapters::View.new('all', nil, self), :default=>ConnectionAdapters::View.new('default', nil, self)}
|
462
|
+
end
|
463
|
+
|
464
|
+
def find_all_by_id(ids, options={})
|
465
|
+
ids.inject([]) do |result, id|
|
466
|
+
begin
|
467
|
+
result << find_one(id, options)
|
468
|
+
rescue BigRecord::RecordNotFound => e
|
469
|
+
end
|
470
|
+
result
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
474
|
+
protected
|
475
|
+
def invalidate_views
|
476
|
+
@views = nil
|
477
|
+
@view_names = nil
|
478
|
+
end
|
479
|
+
|
480
|
+
def extract_options_from_args!(args) #:nodoc:
|
481
|
+
args.last.is_a?(Hash) ? args.pop : {}
|
482
|
+
end
|
483
|
+
|
484
|
+
VALID_FIND_OPTIONS = [:limit, :offset, :include, :view, :versions, :timestamp,
|
485
|
+
:include_deleted, :force_reload, :columns, :stop_row]
|
486
|
+
|
487
|
+
def validate_find_options(options) #:nodoc:
|
488
|
+
options.assert_valid_keys(VALID_FIND_OPTIONS)
|
489
|
+
end
|
490
|
+
|
491
|
+
def find_every(options)
|
492
|
+
requested_columns = columns_to_find(options)
|
493
|
+
|
494
|
+
raw_records = connection.get_consecutive_rows(table_name, options[:offset],
|
495
|
+
options[:limit], requested_columns, options[:stop_row])
|
496
|
+
|
497
|
+
raw_records.collect do |raw_record|
|
498
|
+
add_missing_cells(raw_record, requested_columns)
|
499
|
+
rec = instantiate(raw_record)
|
500
|
+
rec.all_attributes_loaded = true if options[:view] == :all
|
501
|
+
rec
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
def find_from_ids(ids, options)
|
506
|
+
expects_array = ids.first.kind_of?(Array)
|
507
|
+
return ids.first if expects_array && ids.first.empty?
|
508
|
+
|
509
|
+
ids = ids.flatten.compact.uniq
|
510
|
+
|
511
|
+
case ids.size
|
512
|
+
when 0
|
513
|
+
raise RecordNotFound, "Couldn't find #{name} without an ID"
|
514
|
+
when 1
|
515
|
+
result = find_one(ids.first, options)
|
516
|
+
expects_array ? [ result ] : result
|
517
|
+
else
|
518
|
+
ids.collect do |id|
|
519
|
+
find_one(id, options)
|
520
|
+
end
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
524
|
+
def find_one(id, options)
|
525
|
+
# allow to pass a record (e.g. Entity.find(@entity)) and not only a string (e.g. Entity.find("$-monkey-123"))
|
526
|
+
unless id.is_a?(String)
|
527
|
+
id = id.id if id and not id.is_a?(String)
|
528
|
+
end
|
529
|
+
|
530
|
+
# Allow the client to give us other objects than integers, e.g. Time and String
|
531
|
+
if options[:timestamp] && options[:timestamp].kind_of?(Time)
|
532
|
+
options[:timestamp] = options[:timestamp].to_bigrecord_timestamp
|
533
|
+
end
|
534
|
+
|
535
|
+
requested_columns = columns_to_find(options)
|
536
|
+
|
537
|
+
# TODO: this is a hack... it should be done in a single call but currently hbase doesn't allow that
|
538
|
+
raw_record =
|
539
|
+
if options[:versions] and options[:versions] > 1
|
540
|
+
timestamps = connection.get(table_name, id, "#{default_family}:updated_at", options)
|
541
|
+
timestamps.collect{|timestamp| connection.get_columns(table_name, id, requested_columns, :timestamp => timestamp.to_bigrecord_timestamp)}
|
542
|
+
else
|
543
|
+
connection.get_columns(table_name, id, requested_columns, options)
|
544
|
+
end
|
545
|
+
|
546
|
+
# Instantiate the raw record (or records, if multiple versions were asked)
|
547
|
+
if raw_record
|
548
|
+
if raw_record.is_a?(Array)
|
549
|
+
unless raw_record.empty?
|
550
|
+
raw_record.collect do |r|
|
551
|
+
add_missing_cells(r, requested_columns)
|
552
|
+
rec = instantiate(r)
|
553
|
+
rec.all_attributes_loaded = true if options[:view] == :all
|
554
|
+
rec
|
555
|
+
end
|
556
|
+
else
|
557
|
+
raise RecordNotFound, "Couldn't find #{name} with ID=#{id}"
|
558
|
+
end
|
559
|
+
else
|
560
|
+
add_missing_cells(raw_record, requested_columns)
|
561
|
+
rec = instantiate(raw_record)
|
562
|
+
rec.all_attributes_loaded = true if options[:view] == :all
|
563
|
+
rec
|
564
|
+
end
|
565
|
+
else
|
566
|
+
raise RecordNotFound, "Couldn't find #{name} with ID=#{id}"
|
567
|
+
end
|
568
|
+
end
|
569
|
+
|
570
|
+
# return the list of columns to get from hbase
|
571
|
+
def columns_to_find(options={})
|
572
|
+
c =
|
573
|
+
if options[:columns]
|
574
|
+
options[:columns]
|
575
|
+
elsif options[:view]
|
576
|
+
raise ArgumentError, "Unknown view: #{options[:view]}" unless views_hash[options[:view]]
|
577
|
+
if options[:view] == :all
|
578
|
+
["#{default_family}:"]
|
579
|
+
else
|
580
|
+
views_hash[options[:view]].column_names
|
581
|
+
end
|
582
|
+
elsif views_hash[:default]
|
583
|
+
views_hash[:default].column_names
|
584
|
+
else
|
585
|
+
["#{default_family}:"]
|
586
|
+
end
|
587
|
+
c += [options[:include]] if options[:include]
|
588
|
+
c.flatten.reject{|x| x == "id"}
|
589
|
+
end
|
590
|
+
|
591
|
+
# Add the missing cells to the raw record and set them to nil. We know that it's
|
592
|
+
# nil because else we would have received those cells. That way, when the value of
|
593
|
+
# one of these cells will be requested by the client we won't try to lazy load it.
|
594
|
+
def add_missing_cells(raw_record, requested_columns)
|
595
|
+
requested_columns.each do |k, v|
|
596
|
+
# don't do it for column families (e.g. attribute:)
|
597
|
+
unless k =~ /:$/
|
598
|
+
raw_record[k] = nil unless raw_record.has_key?(k)
|
599
|
+
end
|
600
|
+
end
|
601
|
+
end
|
602
|
+
|
603
|
+
# Define aliases to the fully qualified attributes
|
604
|
+
def alias_attribute(alias_name, fully_qualified_name)
|
605
|
+
self.class_eval <<-EOF
|
606
|
+
def #{alias_name}(options={})
|
607
|
+
read_attribute("#{fully_qualified_name}", options)
|
608
|
+
end
|
609
|
+
def #{alias_name}=(value)
|
610
|
+
write_attribute("#{fully_qualified_name}", value)
|
611
|
+
end
|
612
|
+
EOF
|
613
|
+
end
|
614
|
+
|
615
|
+
end
|
616
|
+
|
617
|
+
end
|
618
|
+
end
|