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,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
|