simple_record 2.1.3 → 2.1.6
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/README.markdown +23 -2
- data/lib/simple_record.rb +50 -47
- data/lib/simple_record/active_sdb.rb +941 -938
- data/lib/simple_record/json.rb +37 -57
- data/lib/simple_record/sharding.rb +48 -20
- data/lib/simple_record/validations.rb +1 -1
- metadata +47 -82
- data/test/conversions_test.rb +0 -44
- data/test/model_with_enc.rb +0 -6
- data/test/my_base_model.rb +0 -8
- data/test/my_child_model.rb +0 -35
- data/test/my_model.rb +0 -101
- data/test/my_sharded_model.rb +0 -46
- data/test/my_simple_model.rb +0 -13
- data/test/paging_array_test.rb +0 -15
- data/test/temp.rb +0 -9
- data/test/temp_test.rb +0 -63
- data/test/test_base.rb +0 -67
- data/test/test_dirty.rb +0 -79
- data/test/test_encodings.rb +0 -43
- data/test/test_global_options.rb +0 -63
- data/test/test_helpers.rb +0 -15
- data/test/test_json.rb +0 -82
- data/test/test_lobs.rb +0 -118
- data/test/test_marshalled.rb +0 -43
- data/test/test_pagination.rb +0 -31
- data/test/test_rails3.rb +0 -24
- data/test/test_results_array.rb +0 -57
- data/test/test_shards.rb +0 -148
- data/test/test_simple_record.rb +0 -693
- data/test/test_usage.rb +0 -40
- data/test/test_validations.rb +0 -55
data/README.markdown
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
An ActiveRecord interface for SimpleDB. Can be used as a drop in replacement for ActiveRecord in rails.
|
4
4
|
|
5
|
-
Brought to you by: [](http://www.appoxy.com)
|
6
6
|
|
7
7
|
## Discussion Group
|
8
8
|
|
@@ -261,6 +261,15 @@ all the clobs on the object.
|
|
261
261
|
|
262
262
|
Setting this will automatically use :s3_bucket=>:new as well.
|
263
263
|
|
264
|
+
## JSON Support
|
265
|
+
|
266
|
+
You can easily marshal and unmarshal SimpleRecord objects and results by calling `to_json` on them. The default
|
267
|
+
serialization will include a `json_class` value which will be used when deserializing to find the class. If you're using
|
268
|
+
the results in an API though, you may not want to include json_class because the receiving end may not have that class
|
269
|
+
around, so you can pass in `:exclude_json_class` option to to_json, eg:
|
270
|
+
|
271
|
+
my_ob.to_json(:exclude_json_class=>true)
|
272
|
+
|
264
273
|
## Tips and Tricks and Things to Know
|
265
274
|
|
266
275
|
### Automagic Stuff
|
@@ -366,10 +375,22 @@ The :map function should return which shard name the object should be stored to.
|
|
366
375
|
When executing a find() operation, you can explicitly specify the shard(s) you'd like to find on. This is
|
367
376
|
particularly useful if you know in advance which shard the data will be in.
|
368
377
|
|
369
|
-
|
378
|
+
MyShardedClass.find(:all, :conditions=>....., :shard=>["CA", "FL"])
|
370
379
|
|
371
380
|
You can see some [example classes here](https://github.com/appoxy/simple_record/blob/master/test/my_sharded_model.rb).
|
372
381
|
|
382
|
+
## Concurrency
|
383
|
+
|
384
|
+
**Subject to change**
|
385
|
+
|
386
|
+
This was brought on as a way to query across shards in parallel. Not being able to find a good generic concurrency library,
|
387
|
+
I ended up rolling my own called [concur](https://github.com/appoxy/concur).
|
388
|
+
|
389
|
+
MyShardedClass.find(:all, :concurrent=>true)
|
390
|
+
|
391
|
+
We may enable a global [Executor](https://github.com/appoxy/concur/blob/master/lib/executor.rb) so you can have a fixed
|
392
|
+
thread pool across your app, but for now, it will fire up a thread per shard.
|
393
|
+
|
373
394
|
## Kudos
|
374
395
|
|
375
396
|
Special thanks to Garrett Cox for creating Activerecord2sdb which SimpleRecord is based on:
|
data/lib/simple_record.rb
CHANGED
@@ -51,20 +51,20 @@ require_relative 'simple_record/sharding'
|
|
51
51
|
|
52
52
|
module SimpleRecord
|
53
53
|
|
54
|
-
@@options
|
55
|
-
@@stats
|
56
|
-
@@logging
|
57
|
-
@@s3
|
54
|
+
@@options = {}
|
55
|
+
@@stats = SimpleRecord::Stats.new
|
56
|
+
@@logging = false
|
57
|
+
@@s3 = nil
|
58
58
|
@@auto_close_s3 = false
|
59
|
-
@@logger
|
60
|
-
@@logger.level
|
59
|
+
@@logger = Logger.new(STDOUT)
|
60
|
+
@@logger.level = Logger::INFO
|
61
61
|
|
62
62
|
class << self;
|
63
63
|
attr_accessor :aws_access_key, :aws_secret_key
|
64
64
|
|
65
65
|
# Deprecated
|
66
66
|
def enable_logging
|
67
|
-
@@logging
|
67
|
+
@@logging = true
|
68
68
|
@@logger.level = Logger::DEBUG
|
69
69
|
end
|
70
70
|
|
@@ -135,7 +135,7 @@ module SimpleRecord
|
|
135
135
|
# todo: should we init this only when needed?
|
136
136
|
end
|
137
137
|
s3_ops = {:connection_mode=>options[:connection_mode] || :default}
|
138
|
-
@@s3
|
138
|
+
@@s3 = Aws::S3.new(SimpleRecord.aws_access_key, SimpleRecord.aws_secret_key, s3_ops)
|
139
139
|
|
140
140
|
if options[:created_col]
|
141
141
|
SimpleRecord::Base.has_dates options[:created_col]
|
@@ -230,12 +230,12 @@ module SimpleRecord
|
|
230
230
|
|
231
231
|
clear_errors
|
232
232
|
|
233
|
-
@dirty
|
233
|
+
@dirty = {}
|
234
234
|
|
235
|
-
@attributes
|
235
|
+
@attributes = {} # sdb values
|
236
236
|
@attributes_rb = {} # ruby values
|
237
|
-
@lobs
|
238
|
-
@new_record
|
237
|
+
@lobs = {}
|
238
|
+
@new_record = true
|
239
239
|
|
240
240
|
end
|
241
241
|
|
@@ -333,7 +333,7 @@ module SimpleRecord
|
|
333
333
|
end
|
334
334
|
|
335
335
|
def get_att_meta(name)
|
336
|
-
name_s
|
336
|
+
name_s = name.to_s
|
337
337
|
att_meta = defined_attributes_local[name.to_sym]
|
338
338
|
if att_meta.nil? && has_id_on_end(name_s)
|
339
339
|
att_meta = defined_attributes_local[name_s[0..-4].to_sym]
|
@@ -365,7 +365,7 @@ module SimpleRecord
|
|
365
365
|
|
366
366
|
def make_dirty(arg, value)
|
367
367
|
sdb_att_name = sdb_att_name(arg)
|
368
|
-
arg
|
368
|
+
arg = arg.to_s
|
369
369
|
|
370
370
|
# puts "Marking #{arg} dirty with #{value}" if SimpleRecord.logging?
|
371
371
|
if @dirty.include?(sdb_att_name)
|
@@ -469,7 +469,7 @@ module SimpleRecord
|
|
469
469
|
begin
|
470
470
|
is_create = new_record? # self[:id].nil?
|
471
471
|
|
472
|
-
dirty
|
472
|
+
dirty = @dirty
|
473
473
|
# puts 'dirty before=' + @dirty.inspect
|
474
474
|
if options[:dirty]
|
475
475
|
# puts '@dirty=' + @dirty.inspect
|
@@ -505,7 +505,7 @@ module SimpleRecord
|
|
505
505
|
_run_save_callbacks do
|
506
506
|
result = new_record? ? create(options) : update(options)
|
507
507
|
# puts 'save_callbacks result=' + result.inspect
|
508
|
-
ret
|
508
|
+
ret = result
|
509
509
|
end
|
510
510
|
ret
|
511
511
|
end
|
@@ -514,7 +514,7 @@ module SimpleRecord
|
|
514
514
|
puts '3 create'
|
515
515
|
ret = true
|
516
516
|
_run_create_callbacks do
|
517
|
-
x
|
517
|
+
x = do_actual_save(options)
|
518
518
|
# puts 'create old_save result=' + x.to_s
|
519
519
|
ret = x
|
520
520
|
end
|
@@ -526,7 +526,7 @@ module SimpleRecord
|
|
526
526
|
puts '3 update'
|
527
527
|
ret = true
|
528
528
|
_run_update_callbacks do
|
529
|
-
x
|
529
|
+
x = do_actual_save(options)
|
530
530
|
# puts 'update old_save result=' + x.to_s
|
531
531
|
ret = x
|
532
532
|
end
|
@@ -580,12 +580,12 @@ module SimpleRecord
|
|
580
580
|
def save_lobs(dirty=nil)
|
581
581
|
# puts 'dirty.inspect=' + dirty.inspect
|
582
582
|
dirty = @dirty if dirty.nil?
|
583
|
-
all_clobs
|
583
|
+
all_clobs = {}
|
584
584
|
dirty_clobs = {}
|
585
585
|
defined_attributes_local.each_pair do |k, v|
|
586
586
|
# collect up the clobs in case it's a single put
|
587
587
|
if v.type == :clob
|
588
|
-
val
|
588
|
+
val = @lobs[k]
|
589
589
|
all_clobs[k] = val
|
590
590
|
if dirty.include?(k.to_s)
|
591
591
|
dirty_clobs[k] = val
|
@@ -700,7 +700,7 @@ module SimpleRecord
|
|
700
700
|
|
701
701
|
# puts '@@active_model ? ' + @@active_model.inspect
|
702
702
|
|
703
|
-
ok
|
703
|
+
ok = true
|
704
704
|
is_create = self[:id].nil?
|
705
705
|
unless @@active_model
|
706
706
|
ok = run_before_validation && (is_create ? run_before_validation_on_create : run_before_validation_on_update)
|
@@ -810,10 +810,10 @@ module SimpleRecord
|
|
810
810
|
end
|
811
811
|
|
812
812
|
# Pass in the same OPTIONS you'd pass into a find(:all, OPTIONS)
|
813
|
-
def self.delete_all(options)
|
813
|
+
def self.delete_all(options={})
|
814
814
|
# could make this quicker by just getting item_names and deleting attributes rather than creating objects
|
815
815
|
obs = self.find(:all, options)
|
816
|
-
i
|
816
|
+
i = 0
|
817
817
|
obs.each do |a|
|
818
818
|
a.delete
|
819
819
|
i+=1
|
@@ -822,9 +822,9 @@ module SimpleRecord
|
|
822
822
|
end
|
823
823
|
|
824
824
|
# Pass in the same OPTIONS you'd pass into a find(:all, OPTIONS)
|
825
|
-
def self.destroy_all(options)
|
825
|
+
def self.destroy_all(options={})
|
826
826
|
obs = self.find(:all, options)
|
827
|
-
i
|
827
|
+
i = 0
|
828
828
|
obs.each do |a|
|
829
829
|
a.destroy
|
830
830
|
i+=1
|
@@ -921,7 +921,7 @@ module SimpleRecord
|
|
921
921
|
def self.find(*params)
|
922
922
|
#puts 'params=' + params.inspect
|
923
923
|
|
924
|
-
q_type
|
924
|
+
q_type = :all
|
925
925
|
select_attributes=[]
|
926
926
|
if params.size > 0
|
927
927
|
q_type = params[0]
|
@@ -932,7 +932,7 @@ module SimpleRecord
|
|
932
932
|
end
|
933
933
|
conditions = options[:conditions]
|
934
934
|
if conditions && conditions.is_a?(String)
|
935
|
-
conditions
|
935
|
+
conditions = [conditions]
|
936
936
|
options[:conditions] = conditions
|
937
937
|
end
|
938
938
|
|
@@ -948,13 +948,13 @@ module SimpleRecord
|
|
948
948
|
#puts 'options=' + options.inspect
|
949
949
|
#puts 'after collect=' + options.inspect
|
950
950
|
convert_condition_params(options)
|
951
|
-
per_token
|
951
|
+
per_token = options[:per_token]
|
952
952
|
consistent_read = options[:consistent_read]
|
953
953
|
if per_token || consistent_read then
|
954
|
-
op_dup
|
955
|
-
op_dup[:limit]
|
954
|
+
op_dup = options.dup
|
955
|
+
op_dup[:limit] = per_token # simpledb uses Limit as a paging thing, not what is normal
|
956
956
|
op_dup[:consistent_read] = consistent_read
|
957
|
-
params_dup[1]
|
957
|
+
params_dup[1] = op_dup
|
958
958
|
end
|
959
959
|
end
|
960
960
|
# puts 'params2=' + params.inspect
|
@@ -962,7 +962,7 @@ module SimpleRecord
|
|
962
962
|
ret = q_type == :all ? [] : nil
|
963
963
|
begin
|
964
964
|
results=find_with_metadata(*params_dup)
|
965
|
-
|
965
|
+
puts "RESULT=" + results.inspect
|
966
966
|
write_usage(:select, domain, q_type, options, results)
|
967
967
|
#puts 'params3=' + params.inspect
|
968
968
|
SimpleRecord.stats.selects += 1
|
@@ -972,22 +972,25 @@ module SimpleRecord
|
|
972
972
|
ret = results[:items].first
|
973
973
|
# todo: we should store request_id and box_usage with the object maybe?
|
974
974
|
cache_results(ret)
|
975
|
-
elsif results[:
|
975
|
+
elsif results[:single_only]
|
976
976
|
ret = results[:single]
|
977
|
+
puts 'results[:single] ' + ret.inspect
|
977
978
|
cache_results(ret)
|
978
979
|
else
|
980
|
+
puts 'last step items = ' + results.inspect
|
979
981
|
if results[:items] #.is_a?(Array)
|
980
982
|
cache_results(results[:items])
|
981
983
|
ret = SimpleRecord::ResultsArray.new(self, params, results, next_token)
|
982
984
|
end
|
983
985
|
end
|
984
986
|
rescue Aws::AwsError, SimpleRecord::ActiveSdb::ActiveSdbError => ex
|
985
|
-
|
987
|
+
puts "RESCUED: " + ex.message
|
986
988
|
if (ex.message().index("NoSuchDomain") != nil)
|
987
989
|
# this is ok
|
988
|
-
elsif (ex.message() =~ @@regex_no_id)
|
989
|
-
ret = nil
|
990
|
+
# elsif (ex.message() =~ @@regex_no_id) This is RecordNotFound now
|
991
|
+
# ret = nil
|
990
992
|
else
|
993
|
+
puts 're-raising'
|
991
994
|
raise ex
|
992
995
|
end
|
993
996
|
end
|
@@ -1016,14 +1019,14 @@ module SimpleRecord
|
|
1016
1019
|
def self.paginate(options={})
|
1017
1020
|
# options = args.pop
|
1018
1021
|
# puts 'paginate options=' + options.inspect if SimpleRecord.logging?
|
1019
|
-
page
|
1020
|
-
per_page
|
1022
|
+
page = options[:page] || 1
|
1023
|
+
per_page = options[:per_page] || 50
|
1021
1024
|
# total = options[:total_entries].to_i
|
1022
|
-
options[:page]
|
1025
|
+
options[:page] = page.to_i # makes sure it's to_i
|
1023
1026
|
options[:per_page] = per_page.to_i
|
1024
|
-
options[:limit]
|
1027
|
+
options[:limit] = options[:page] * options[:per_page]
|
1025
1028
|
# puts 'paging options=' + options.inspect
|
1026
|
-
fr
|
1029
|
+
fr = find(:all, options)
|
1027
1030
|
return fr
|
1028
1031
|
|
1029
1032
|
end
|
@@ -1048,15 +1051,15 @@ module SimpleRecord
|
|
1048
1051
|
# todo: cache each result
|
1049
1052
|
results.each do |item|
|
1050
1053
|
class_name = item.class.name
|
1051
|
-
id
|
1052
|
-
cache_key
|
1054
|
+
id = item.id
|
1055
|
+
cache_key = self.cache_key(class_name, id)
|
1053
1056
|
#puts 'caching result at ' + cache_key + ': ' + results.inspect
|
1054
1057
|
cache_store.write(cache_key, item, :expires_in =>30)
|
1055
1058
|
end
|
1056
1059
|
else
|
1057
1060
|
class_name = results.class.name
|
1058
|
-
id
|
1059
|
-
cache_key
|
1061
|
+
id = results.id
|
1062
|
+
cache_key = self.cache_key(class_name, id)
|
1060
1063
|
#puts 'caching result at ' + cache_key + ': ' + results.inspect
|
1061
1064
|
cache_store.write(cache_key, results, :expires_in =>30)
|
1062
1065
|
end
|
@@ -1111,7 +1114,7 @@ module SimpleRecord
|
|
1111
1114
|
|
1112
1115
|
class Activerecordtosdb_subrecord_array
|
1113
1116
|
def initialize(subname, referencename, referencevalue)
|
1114
|
-
@subname
|
1117
|
+
@subname =subname.classify
|
1115
1118
|
@referencename =referencename.tableize.singularize + "_id"
|
1116
1119
|
@referencevalue=referencevalue
|
1117
1120
|
end
|
@@ -1168,7 +1171,7 @@ module SimpleRecord
|
|
1168
1171
|
|
1169
1172
|
def create(*params)
|
1170
1173
|
params[0][@referencename]=@referencevalue
|
1171
|
-
record
|
1174
|
+
record = eval(@subname).new(*params)
|
1172
1175
|
record.save
|
1173
1176
|
end
|
1174
1177
|
|
@@ -22,989 +22,992 @@
|
|
22
22
|
#
|
23
23
|
|
24
24
|
begin
|
25
|
-
|
25
|
+
require 'uuidtools'
|
26
26
|
rescue LoadError => e
|
27
|
-
|
28
|
-
|
27
|
+
STDERR.puts("SimpleRecord requires the uuidtools gem. Run \'gem install uuidtools\' and try again.")
|
28
|
+
exit
|
29
29
|
end
|
30
30
|
|
31
31
|
module SimpleRecord
|
32
32
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
33
|
+
# = Aws::ActiveSdb -- RightScale SDB interface (alpha release)
|
34
|
+
# The Aws::ActiveSdb class provides a complete interface to Amazon's Simple
|
35
|
+
# Database Service.
|
36
|
+
#
|
37
|
+
# ActiveSdb is in alpha and does not load by default with the rest of Aws. You must use an additional require statement to load the ActiveSdb class. For example:
|
38
|
+
#
|
39
|
+
# require 'right_aws'
|
40
|
+
# require 'sdb/active_sdb'
|
41
|
+
#
|
42
|
+
# Additionally, the ActiveSdb class requires the 'uuidtools' gem; this gem is not normally required by Aws and is not installed as a
|
43
|
+
# dependency of Aws.
|
44
|
+
#
|
45
|
+
# Simple ActiveSdb usage example:
|
46
|
+
#
|
47
|
+
# class Client < Aws::ActiveSdb::Base
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# # connect to SDB
|
51
|
+
# Aws::ActiveSdb.establish_connection
|
52
|
+
#
|
53
|
+
# # create domain
|
54
|
+
# Client.create_domain
|
55
|
+
#
|
56
|
+
# # create initial DB
|
57
|
+
# Client.create 'name' => 'Bush', 'country' => 'USA', 'gender' => 'male', 'expiration' => '2009', 'post' => 'president'
|
58
|
+
# Client.create 'name' => 'Putin', 'country' => 'Russia', 'gender' => 'male', 'expiration' => '2008', 'post' => 'president'
|
59
|
+
# Client.create 'name' => 'Medvedev', 'country' => 'Russia', 'gender' => 'male', 'expiration' => '2012', 'post' => 'president'
|
60
|
+
# Client.create 'name' => 'Mary', 'country' => 'USA', 'gender' => 'female', 'hobby' => ['patchwork', 'bundle jumping']
|
61
|
+
# Client.create 'name' => 'Mary', 'country' => 'Russia', 'gender' => 'female', 'hobby' => ['flowers', 'cats', 'cooking']
|
62
|
+
# sandy_id = Client.create('name' => 'Sandy', 'country' => 'Russia', 'gender' => 'female', 'hobby' => ['flowers', 'cats', 'cooking']).id
|
63
|
+
#
|
64
|
+
# # find all Bushes in USA
|
65
|
+
# Client.find(:all, :conditions => ["['name'=?] intersection ['country'=?]",'Bush','USA']).each do |client|
|
66
|
+
# client.reload
|
67
|
+
# puts client.attributes.inspect
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# # find all Maries through the world
|
71
|
+
# Client.find_all_by_name_and_gender('Mary','female').each do |mary|
|
72
|
+
# mary.reload
|
73
|
+
# puts "#{mary[:name]}, gender: #{mary[:gender]}, hobbies: #{mary[:hobby].join(',')}"
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# # find new russian president
|
77
|
+
# medvedev = Client.find_by_post_and_country_and_expiration('president','Russia','2012')
|
78
|
+
# if medvedev
|
79
|
+
# medvedev.reload
|
80
|
+
# medvedev.save_attributes('age' => '42', 'hobby' => 'Gazprom')
|
81
|
+
# end
|
82
|
+
#
|
83
|
+
# # retire old president
|
84
|
+
# Client.find_by_name('Putin').delete
|
85
|
+
#
|
86
|
+
# # Sandy disappointed in 'cooking' and decided to hide her 'gender' and 'country' ()
|
87
|
+
# sandy = Client.find(sandy_id)
|
88
|
+
# sandy.reload
|
89
|
+
# sandy.delete_values('hobby' => ['cooking'] )
|
90
|
+
# sandy.delete_attributes('country', 'gender')
|
91
|
+
#
|
92
|
+
# # remove domain
|
93
|
+
# Client.delete_domain
|
94
|
+
#
|
95
|
+
class ActiveSdb
|
96
|
+
|
97
|
+
module ActiveSdbConnect
|
98
|
+
def connection
|
99
|
+
@connection || raise(ActiveSdbError.new('Connection to SDB is not established'))
|
100
|
+
end
|
101
|
+
|
102
|
+
# Create a new handle to an Sdb account. All handles share the same per process or per thread
|
103
|
+
# HTTP connection to Amazon Sdb. Each handle is for a specific account.
|
104
|
+
# The +params+ are passed through as-is to Aws::SdbInterface.new
|
105
|
+
# Params:
|
106
|
+
# { :server => 'sdb.amazonaws.com' # Amazon service host: 'sdb.amazonaws.com'(default)
|
107
|
+
# :port => 443 # Amazon service port: 80 or 443(default)
|
108
|
+
# :protocol => 'https' # Amazon service protocol: 'http' or 'https'(default)
|
109
|
+
# :signature_version => '2' # The signature version : '0', '1' or '2' (default)
|
110
|
+
# DEPRECATED :multi_thread => true|false # Multi-threaded (connection per each thread): true or false(default)
|
111
|
+
# :connection_mode => :default # options are :default (will use best known option, may change in the future)
|
112
|
+
# :per_request (opens and closes a connection on every request to SDB)
|
113
|
+
# :single (same as old multi_thread=>false)
|
114
|
+
# :per_thread (same as old multi_thread=>true)
|
115
|
+
# :pool (uses a connection pool with a maximum number of connections - NOT IMPLEMENTED YET)
|
116
|
+
# :logger => Logger Object # Logger instance: logs to STDOUT if omitted
|
117
|
+
# :nil_representation => 'mynil'} # interpret Ruby nil as this string value; i.e. use this string in SDB to represent Ruby nils (default is the string 'nil')
|
118
|
+
# :service_endpoint => '/' # Set this to /mdb/request.mgwsi for usage with M/DB
|
119
|
+
|
120
|
+
def establish_connection(aws_access_key_id=nil, aws_secret_access_key=nil, params={})
|
121
|
+
@connection = Aws::SdbInterface.new(aws_access_key_id, aws_secret_access_key, params)
|
122
|
+
end
|
123
|
+
|
124
|
+
def close_connection
|
125
|
+
@connection.close_connection unless @connection.nil?
|
126
|
+
end
|
127
|
+
end
|
101
128
|
|
102
|
-
|
103
|
-
|
104
|
-
# The +params+ are passed through as-is to Aws::SdbInterface.new
|
105
|
-
# Params:
|
106
|
-
# { :server => 'sdb.amazonaws.com' # Amazon service host: 'sdb.amazonaws.com'(default)
|
107
|
-
# :port => 443 # Amazon service port: 80 or 443(default)
|
108
|
-
# :protocol => 'https' # Amazon service protocol: 'http' or 'https'(default)
|
109
|
-
# :signature_version => '2' # The signature version : '0', '1' or '2' (default)
|
110
|
-
# DEPRECATED :multi_thread => true|false # Multi-threaded (connection per each thread): true or false(default)
|
111
|
-
# :connection_mode => :default # options are :default (will use best known option, may change in the future)
|
112
|
-
# :per_request (opens and closes a connection on every request to SDB)
|
113
|
-
# :single (same as old multi_thread=>false)
|
114
|
-
# :per_thread (same as old multi_thread=>true)
|
115
|
-
# :pool (uses a connection pool with a maximum number of connections - NOT IMPLEMENTED YET)
|
116
|
-
# :logger => Logger Object # Logger instance: logs to STDOUT if omitted
|
117
|
-
# :nil_representation => 'mynil'} # interpret Ruby nil as this string value; i.e. use this string in SDB to represent Ruby nils (default is the string 'nil')
|
118
|
-
# :service_endpoint => '/' # Set this to /mdb/request.mgwsi for usage with M/DB
|
119
|
-
|
120
|
-
def establish_connection(aws_access_key_id=nil, aws_secret_access_key=nil, params={})
|
121
|
-
@connection = Aws::SdbInterface.new(aws_access_key_id, aws_secret_access_key, params)
|
122
|
-
end
|
129
|
+
class ActiveSdbError < RuntimeError
|
130
|
+
end
|
123
131
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
132
|
+
class << self
|
133
|
+
include ActiveSdbConnect
|
134
|
+
|
135
|
+
# Retreive a list of domains.
|
136
|
+
#
|
137
|
+
# put Aws::ActiveSdb.domains #=> ['co-workers','family','friends','clients']
|
138
|
+
#
|
139
|
+
def domains
|
140
|
+
connection.list_domains[:domains]
|
141
|
+
end
|
142
|
+
|
143
|
+
# Create new domain.
|
144
|
+
# Raises no errors if the domain already exists.
|
145
|
+
#
|
146
|
+
# Aws::ActiveSdb.create_domain('alpha') #=> {:request_id=>"6fc652a0-0000-41d5-91f4-3ed390a3d3b2", :box_usage=>"0.0055590278"}
|
147
|
+
#
|
148
|
+
def create_domain(domain_name)
|
149
|
+
connection.create_domain(domain_name)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Remove domain from SDB.
|
153
|
+
# Raises no errors if the domain does not exist.
|
154
|
+
#
|
155
|
+
# Aws::ActiveSdb.create_domain('alpha') #=> {:request_id=>"6fc652a0-0000-41c6-91f4-3ed390a3d3b2", :box_usage=>"0.0055590001"}
|
156
|
+
#
|
157
|
+
def delete_domain(domain_name)
|
158
|
+
connection.delete_domain(domain_name)
|
159
|
+
end
|
160
|
+
end
|
128
161
|
|
129
|
-
|
162
|
+
class Base
|
163
|
+
|
164
|
+
class << self
|
165
|
+
include ActiveSdbConnect
|
166
|
+
|
167
|
+
# next_token value returned by last find: is useful to continue finding
|
168
|
+
attr_accessor :next_token
|
169
|
+
|
170
|
+
# Returns a Aws::SdbInterface object
|
171
|
+
#
|
172
|
+
# class A < Aws::ActiveSdb::Base
|
173
|
+
# end
|
174
|
+
#
|
175
|
+
# class B < Aws::ActiveSdb::Base
|
176
|
+
# end
|
177
|
+
#
|
178
|
+
# class C < Aws::ActiveSdb::Base
|
179
|
+
# end
|
180
|
+
#
|
181
|
+
# Aws::ActiveSdb.establish_connection 'key_id_1', 'secret_key_1'
|
182
|
+
#
|
183
|
+
# C.establish_connection 'key_id_2', 'secret_key_2'
|
184
|
+
#
|
185
|
+
# # A and B uses the default connection, C - uses its own
|
186
|
+
# puts A.connection #=> #<Aws::SdbInterface:0xb76d6d7c>
|
187
|
+
# puts B.connection #=> #<Aws::SdbInterface:0xb76d6d7c>
|
188
|
+
# puts C.connection #=> #<Aws::SdbInterface:0xb76d6ca0>
|
189
|
+
#
|
190
|
+
def connection
|
191
|
+
@connection || ActiveSdb::connection
|
130
192
|
end
|
131
193
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
end
|
194
|
+
@domain = nil
|
195
|
+
|
196
|
+
# Current domain name.
|
197
|
+
#
|
198
|
+
# # if 'ActiveSupport' is not loaded then class name converted to downcase
|
199
|
+
# class Client < Aws::ActiveSdb::Base
|
200
|
+
# end
|
201
|
+
# puts Client.domain #=> 'client'
|
202
|
+
#
|
203
|
+
# # if 'ActiveSupport' is loaded then class name being tableized
|
204
|
+
# require 'activesupport'
|
205
|
+
# class Client < Aws::ActiveSdb::Base
|
206
|
+
# end
|
207
|
+
# puts Client.domain #=> 'clients'
|
208
|
+
#
|
209
|
+
# # Explicit domain name definition
|
210
|
+
# class Client < Aws::ActiveSdb::Base
|
211
|
+
# set_domain_name :foreign_clients
|
212
|
+
# end
|
213
|
+
# puts Client.domain #=> 'foreign_clients'
|
214
|
+
#
|
215
|
+
def domain
|
216
|
+
unless @domain
|
217
|
+
if defined? ActiveSupport::CoreExtensions::String::Inflections
|
218
|
+
@domain = name.tableize
|
219
|
+
else
|
220
|
+
@domain = name.downcase
|
221
|
+
end
|
222
|
+
end
|
223
|
+
@domain
|
160
224
|
end
|
161
225
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
#
|
172
|
-
# class A < Aws::ActiveSdb::Base
|
173
|
-
# end
|
174
|
-
#
|
175
|
-
# class B < Aws::ActiveSdb::Base
|
176
|
-
# end
|
177
|
-
#
|
178
|
-
# class C < Aws::ActiveSdb::Base
|
179
|
-
# end
|
180
|
-
#
|
181
|
-
# Aws::ActiveSdb.establish_connection 'key_id_1', 'secret_key_1'
|
182
|
-
#
|
183
|
-
# C.establish_connection 'key_id_2', 'secret_key_2'
|
184
|
-
#
|
185
|
-
# # A and B uses the default connection, C - uses its own
|
186
|
-
# puts A.connection #=> #<Aws::SdbInterface:0xb76d6d7c>
|
187
|
-
# puts B.connection #=> #<Aws::SdbInterface:0xb76d6d7c>
|
188
|
-
# puts C.connection #=> #<Aws::SdbInterface:0xb76d6ca0>
|
189
|
-
#
|
190
|
-
def connection
|
191
|
-
@connection || ActiveSdb::connection
|
192
|
-
end
|
193
|
-
|
194
|
-
@domain = nil
|
195
|
-
|
196
|
-
# Current domain name.
|
197
|
-
#
|
198
|
-
# # if 'ActiveSupport' is not loaded then class name converted to downcase
|
199
|
-
# class Client < Aws::ActiveSdb::Base
|
200
|
-
# end
|
201
|
-
# puts Client.domain #=> 'client'
|
202
|
-
#
|
203
|
-
# # if 'ActiveSupport' is loaded then class name being tableized
|
204
|
-
# require 'activesupport'
|
205
|
-
# class Client < Aws::ActiveSdb::Base
|
206
|
-
# end
|
207
|
-
# puts Client.domain #=> 'clients'
|
208
|
-
#
|
209
|
-
# # Explicit domain name definition
|
210
|
-
# class Client < Aws::ActiveSdb::Base
|
211
|
-
# set_domain_name :foreign_clients
|
212
|
-
# end
|
213
|
-
# puts Client.domain #=> 'foreign_clients'
|
214
|
-
#
|
215
|
-
def domain
|
216
|
-
unless @domain
|
217
|
-
if defined? ActiveSupport::CoreExtensions::String::Inflections
|
218
|
-
@domain = name.tableize
|
219
|
-
else
|
220
|
-
@domain = name.downcase
|
221
|
-
end
|
222
|
-
end
|
223
|
-
@domain
|
224
|
-
end
|
225
|
-
|
226
|
-
# Change the default domain name to user defined.
|
227
|
-
#
|
228
|
-
# class Client < Aws::ActiveSdb::Base
|
229
|
-
# set_domain_name :foreign_clients
|
230
|
-
# end
|
231
|
-
#
|
232
|
-
def set_domain_name(domain)
|
233
|
-
@domain = domain.to_s
|
234
|
-
end
|
235
|
-
|
236
|
-
# Create domain at SDB.
|
237
|
-
# Raises no errors if the domain already exists.
|
238
|
-
#
|
239
|
-
# class Client < Aws::ActiveSdb::Base
|
240
|
-
# end
|
241
|
-
# Client.create_domain #=> {:request_id=>"6fc652a0-0000-41d5-91f4-3ed390a3d3b2", :box_usage=>"0.0055590278"}
|
242
|
-
#
|
243
|
-
def create_domain(dom=nil)
|
244
|
-
dom = domain if dom.nil?
|
245
|
-
puts "Creating new SimpleDB Domain: " + dom
|
246
|
-
connection.create_domain(dom)
|
247
|
-
end
|
248
|
-
|
249
|
-
# Remove domain from SDB.
|
250
|
-
# Raises no errors if the domain does not exist.
|
251
|
-
#
|
252
|
-
# class Client < Aws::ActiveSdb::Base
|
253
|
-
# end
|
254
|
-
# Client.delete_domain #=> {:request_id=>"e14d90d3-0000-4898-9995-0de28cdda270", :box_usage=>"0.0055590278"}
|
255
|
-
#
|
256
|
-
def delete_domain(dom=nil)
|
257
|
-
dom = domain if dom.nil?
|
258
|
-
puts "!!! DELETING SimpleDB Domain: " + dom
|
259
|
-
connection.delete_domain(dom)
|
260
|
-
end
|
261
|
-
|
262
|
-
#
|
263
|
-
# See select(), original find with QUERY syntax is deprecated so now find and select are synonyms.
|
264
|
-
#
|
265
|
-
def find(*args)
|
266
|
-
options = args.last.is_a?(Hash) ? args.pop : {}
|
267
|
-
case args.first
|
268
|
-
when nil then
|
269
|
-
raise "Invalid parameters passed to find: nil."
|
270
|
-
when :all then
|
271
|
-
sql_select(options)[:items]
|
272
|
-
when :first then
|
273
|
-
sql_select(options.merge(:limit => 1))[:items].first
|
274
|
-
when :count then
|
275
|
-
res = sql_select(options.merge(:count => true))[:count]
|
276
|
-
res
|
277
|
-
else
|
278
|
-
res = select_from_ids(args, options)
|
279
|
-
return res[:single] if res[:single]
|
280
|
-
return res[:items]
|
281
|
-
end
|
282
|
-
end
|
283
|
-
|
284
|
-
#
|
285
|
-
# Same as find, but will return SimpleDB metadata like :request_id and :box_usage
|
286
|
-
#
|
287
|
-
def find_with_metadata(*args)
|
288
|
-
options = args.last.is_a?(Hash) ? args.pop : {}
|
289
|
-
case args.first
|
290
|
-
when nil then
|
291
|
-
raise "Invalid parameters passed to find: nil."
|
292
|
-
when :all then
|
293
|
-
sql_select(options)
|
294
|
-
when :first then
|
295
|
-
sql_select(options.merge(:limit => 1))
|
296
|
-
when :count then
|
297
|
-
res = sql_select(options.merge(:count => true))
|
298
|
-
res
|
299
|
-
else
|
300
|
-
select_from_ids args, options
|
301
|
-
end
|
302
|
-
end
|
303
|
-
|
304
|
-
# Perform a SQL-like select request.
|
305
|
-
#
|
306
|
-
# Single record:
|
307
|
-
#
|
308
|
-
# Client.select(:first)
|
309
|
-
# Client.select(:first, :conditions=> [ "name=? AND wife=?", "Jon", "Sandy"])
|
310
|
-
# Client.select(:first, :conditions=> { :name=>"Jon", :wife=>"Sandy" }, :select => :girfriends)
|
311
|
-
#
|
312
|
-
# Bunch of records:
|
313
|
-
#
|
314
|
-
# Client.select(:all)
|
315
|
-
# Client.select(:all, :limit => 10)
|
316
|
-
# Client.select(:all, :conditions=> [ "name=? AND 'girlfriend'=?", "Jon", "Judy"])
|
317
|
-
# Client.select(:all, :conditions=> { :name=>"Sandy" }, :limit => 3)
|
318
|
-
#
|
319
|
-
# Records by ids:
|
320
|
-
#
|
321
|
-
# Client.select('1')
|
322
|
-
# Client.select('1234987b4583475347523948')
|
323
|
-
# Client.select('1','2','3','4', :conditions=> ["toys=?", "beer"])
|
324
|
-
#
|
325
|
-
# Find helpers: Aws::ActiveSdb::Base.select_by_... and Aws::ActiveSdb::Base.select_all_by_...
|
326
|
-
#
|
327
|
-
# Client.select_by_name('Matias Rust')
|
328
|
-
# Client.select_by_name_and_city('Putin','Moscow')
|
329
|
-
# Client.select_by_name_and_city_and_post('Medvedev','Moscow','president')
|
330
|
-
#
|
331
|
-
# Client.select_all_by_author('G.Bush jr')
|
332
|
-
# Client.select_all_by_age_and_gender_and_ethnicity('34','male','russian')
|
333
|
-
# Client.select_all_by_gender_and_country('male', 'Russia', :order => 'name')
|
334
|
-
#
|
335
|
-
# Continue listing:
|
336
|
-
#
|
337
|
-
# # initial listing
|
338
|
-
# Client.select(:all, :limit => 10)
|
339
|
-
# # continue listing
|
340
|
-
# begin
|
341
|
-
# Client.select(:all, :limit => 10, :next_token => Client.next_token)
|
342
|
-
# end while Client.next_token
|
343
|
-
#
|
344
|
-
# Sort oder:
|
345
|
-
# If :order=>'attribute' option is specified then result response (ordered by 'attribute') will contain only items where attribute is defined (is not null).
|
346
|
-
#
|
347
|
-
# Client.select(:all) # returns all records
|
348
|
-
# Client.select(:all, :order => 'gender') # returns all records ordered by gender where gender attribute exists
|
349
|
-
# Client.select(:all, :order => 'name desc') # returns all records ordered by name in desc order where name attribute exists
|
350
|
-
#
|
351
|
-
# see http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?UsingSelect.html
|
352
|
-
#
|
353
|
-
def select(*args)
|
354
|
-
find(*args)
|
355
|
-
end
|
356
|
-
|
357
|
-
def generate_id # :nodoc:
|
358
|
-
UUIDTools::UUID.timestamp_create().to_s
|
359
|
-
end
|
360
|
-
|
361
|
-
protected
|
362
|
-
|
363
|
-
def logger
|
364
|
-
SimpleRecord.logger
|
365
|
-
end
|
366
|
-
# Select
|
367
|
-
|
368
|
-
def select_from_ids(args, options) # :nodoc:
|
369
|
-
cond = []
|
370
|
-
# detect amount of records requested
|
371
|
-
bunch_of_records_requested = args.size > 1 || args.first.is_a?(Array)
|
372
|
-
# flatten ids
|
373
|
-
args = args.to_a.flatten
|
374
|
-
args.each { |id| cond << "itemName() = #{self.connection.escape(id)}" }
|
375
|
-
ids_cond = "(#{cond.join(' OR ')})"
|
376
|
-
# user defined :conditions to string (if it was defined)
|
377
|
-
options[:conditions] = build_conditions(options[:conditions])
|
378
|
-
# join ids condition and user defined conditions
|
379
|
-
options[:conditions] = options[:conditions].blank? ? ids_cond : "(#{options[:conditions]}) AND #{ids_cond}"
|
380
|
-
#puts 'options=' + options.inspect
|
381
|
-
result = sql_select(options)
|
382
|
-
#puts 'select_from_ids result=' + result.inspect
|
383
|
-
# if one record was requested then return it
|
384
|
-
unless bunch_of_records_requested
|
385
|
-
record = result[:items].first
|
386
|
-
# railse if nothing was found
|
387
|
-
raise ActiveSdbError.new("Couldn't find #{name} with ID #{args}") unless record
|
388
|
-
result[:single] = record
|
389
|
-
else
|
390
|
-
# if a bunch of records was requested then return check that we found all of them
|
391
|
-
# and return as an array
|
392
|
-
unless args.size == result[:items].size
|
393
|
-
# todo: might make sense to return the array but with nil values in the slots where an item wasn't found?
|
394
|
-
id_list = args.map { |i| "'#{i}'" }.join(',')
|
395
|
-
raise ActiveSdbError.new("Couldn't find all #{name} with IDs (#{id_list}) (found #{result[:items].size} results, but was looking for #{args.size})")
|
396
|
-
else
|
397
|
-
result
|
398
|
-
end
|
399
|
-
end
|
400
|
-
result
|
401
|
-
end
|
402
|
-
|
403
|
-
def sql_select(options) # :nodoc:
|
404
|
-
count = options[:count] || false
|
405
|
-
#puts 'count? ' + count.to_s
|
406
|
-
@next_token = options[:next_token]
|
407
|
-
@consistent_read = options[:consistent_read]
|
408
|
-
select_expression = build_select(options)
|
409
|
-
logger.debug 'SELECT=' + select_expression
|
410
|
-
# request items
|
411
|
-
query_result = self.connection.select(select_expression, options)
|
412
|
-
# puts 'QR=' + query_result.inspect
|
413
|
-
@next_token = query_result[:next_token]
|
414
|
-
ret = {}
|
415
|
-
if count
|
416
|
-
ret[:count] = query_result.delete(:items)[0]["Domain"]["Count"][0].to_i
|
417
|
-
ret.merge!(query_result)
|
418
|
-
return ret
|
419
|
-
end
|
420
|
-
|
421
|
-
items = query_result.delete(:items).map do |hash|
|
422
|
-
id, attributes = hash.shift
|
423
|
-
new_item = self.new()
|
424
|
-
new_item.initialize_from_db(attributes.merge({'id' => id}))
|
425
|
-
new_item.mark_as_old
|
426
|
-
new_item
|
427
|
-
end
|
428
|
-
ret[:items] = items
|
429
|
-
ret.merge!(query_result)
|
430
|
-
ret
|
431
|
-
end
|
432
|
-
|
433
|
-
# select_by helpers
|
434
|
-
def select_all_by_(format_str, args, options) # :nodoc:
|
435
|
-
fields = format_str.to_s.sub(/^select_(all_)?by_/, '').split('_and_')
|
436
|
-
conditions = fields.map { |field| "#{field}=?" }.join(' AND ')
|
437
|
-
options[:conditions] = [conditions, *args]
|
438
|
-
find(:all, options)
|
439
|
-
end
|
440
|
-
|
441
|
-
def select_by_(format_str, args, options) # :nodoc:
|
442
|
-
options[:limit] = 1
|
443
|
-
select_all_by_(format_str, args, options).first
|
444
|
-
end
|
445
|
-
|
446
|
-
# Query
|
447
|
-
|
448
|
-
# Returns an array of query attributes.
|
449
|
-
# Query_expression must be a well formated SDB query string:
|
450
|
-
# query_attributes("['title' starts-with 'O\\'Reily'] intersection ['year' = '2007']") #=> ["title", "year"]
|
451
|
-
def query_attributes(query_expression) # :nodoc:
|
452
|
-
attrs = []
|
453
|
-
array = query_expression.scan(/['"](.*?[^\\])['"]/).flatten
|
454
|
-
until array.empty? do
|
455
|
-
attrs << array.shift # skip it's value
|
456
|
-
array.shift #
|
457
|
-
end
|
458
|
-
attrs
|
459
|
-
end
|
460
|
-
|
461
|
-
# Returns an array of [attribute_name, 'asc'|'desc']
|
462
|
-
def sort_options(sort_string)
|
463
|
-
sort_string[/['"]?(\w+)['"]? *(asc|desc)?/i]
|
464
|
-
[$1, ($2 || 'asc')]
|
465
|
-
end
|
466
|
-
|
467
|
-
# Perform a query request.
|
468
|
-
#
|
469
|
-
# Options
|
470
|
-
# :query_expression nil | string | array
|
471
|
-
# :max_number_of_items nil | integer
|
472
|
-
# :next_token nil | string
|
473
|
-
# :sort_option nil | string "name desc|asc"
|
474
|
-
#
|
475
|
-
def query(options) # :nodoc:
|
476
|
-
@next_token = options[:next_token]
|
477
|
-
@consistent_read = options[:consistent_read]
|
478
|
-
query_expression = build_conditions(options[:query_expression])
|
479
|
-
# add sort_options to the query_expression
|
480
|
-
if options[:sort_option]
|
481
|
-
sort_by, sort_order = sort_options(options[:sort_option])
|
482
|
-
sort_query_expression = "['#{sort_by}' starts-with '']"
|
483
|
-
sort_by_expression = " sort '#{sort_by}' #{sort_order}"
|
484
|
-
# make query_expression to be a string (it may be null)
|
485
|
-
query_expression = query_expression.to_s
|
486
|
-
# quote from Amazon:
|
487
|
-
# The sort attribute must be present in at least one of the predicates of the query expression.
|
488
|
-
if query_expression.blank?
|
489
|
-
query_expression = sort_query_expression
|
490
|
-
elsif !query_attributes(query_expression).include?(sort_by)
|
491
|
-
query_expression += " intersection #{sort_query_expression}"
|
492
|
-
end
|
493
|
-
query_expression += sort_by_expression
|
494
|
-
end
|
495
|
-
# request items
|
496
|
-
query_result = self.connection.query(domain, query_expression, options[:max_number_of_items], @next_token, @consistent_read)
|
497
|
-
@next_token = query_result[:next_token]
|
498
|
-
items = query_result[:items].map do |name|
|
499
|
-
new_item = self.new('id' => name)
|
500
|
-
new_item.mark_as_old
|
501
|
-
reload_if_exists(record) if options[:auto_load]
|
502
|
-
new_item
|
503
|
-
end
|
504
|
-
items
|
505
|
-
end
|
506
|
-
|
507
|
-
# reload a record unless it is nil
|
508
|
-
def reload_if_exists(record) # :nodoc:
|
509
|
-
record && record.reload
|
510
|
-
end
|
511
|
-
|
512
|
-
def reload_all_records(*list) # :nodoc:
|
513
|
-
list.flatten.each { |record| reload_if_exists(record) }
|
514
|
-
end
|
515
|
-
|
516
|
-
def find_every(options) # :nodoc:
|
517
|
-
records = query(:query_expression => options[:conditions],
|
518
|
-
:max_number_of_items => options[:limit],
|
519
|
-
:next_token => options[:next_token],
|
520
|
-
:sort_option => options[:sort] || options[:order],
|
521
|
-
:consistent_read => options[:consistent_read])
|
522
|
-
options[:auto_load] ? reload_all_records(records) : records
|
523
|
-
end
|
524
|
-
|
525
|
-
def find_initial(options) # :nodoc:
|
526
|
-
options[:limit] = 1
|
527
|
-
record = find_every(options).first
|
528
|
-
options[:auto_load] ? reload_all_records(record).first : record
|
529
|
-
end
|
530
|
-
|
531
|
-
def find_from_ids(args, options) # :nodoc:
|
532
|
-
cond = []
|
533
|
-
# detect amount of records requested
|
534
|
-
bunch_of_records_requested = args.size > 1 || args.first.is_a?(Array)
|
535
|
-
# flatten ids
|
536
|
-
args = args.to_a.flatten
|
537
|
-
args.each { |id| cond << "'id'=#{self.connection.escape(id)}" }
|
538
|
-
ids_cond = "[#{cond.join(' OR ')}]"
|
539
|
-
# user defined :conditions to string (if it was defined)
|
540
|
-
options[:conditions] = build_conditions(options[:conditions])
|
541
|
-
# join ids condition and user defined conditions
|
542
|
-
options[:conditions] = options[:conditions].blank? ? ids_cond : "#{options[:conditions]} intersection #{ids_cond}"
|
543
|
-
result = find_every(options)
|
544
|
-
# if one record was requested then return it
|
545
|
-
unless bunch_of_records_requested
|
546
|
-
record = result.first
|
547
|
-
# railse if nothing was found
|
548
|
-
raise ActiveSdbError.new("Couldn't find #{name} with ID #{args}") unless record
|
549
|
-
options[:auto_load] ? reload_all_records(record).first : record
|
550
|
-
else
|
551
|
-
# if a bunch of records was requested then return check that we found all of them
|
552
|
-
# and return as an array
|
553
|
-
unless args.size == result.size
|
554
|
-
id_list = args.map { |i| "'#{i}'" }.join(',')
|
555
|
-
raise ActiveSdbError.new("Couldn't find all #{name} with IDs (#{id_list}) (found #{result.size} results, but was looking for #{args.size})")
|
556
|
-
else
|
557
|
-
options[:auto_load] ? reload_all_records(result) : result
|
558
|
-
end
|
559
|
-
end
|
560
|
-
end
|
561
|
-
|
562
|
-
# find_by helpers
|
563
|
-
def find_all_by_(format_str, args, options) # :nodoc:
|
564
|
-
fields = format_str.to_s.sub(/^find_(all_)?by_/, '').split('_and_')
|
565
|
-
conditions = fields.map { |field| "['#{field}'=?]" }.join(' intersection ')
|
566
|
-
options[:conditions] = [conditions, *args]
|
567
|
-
find(:all, options)
|
568
|
-
end
|
569
|
-
|
570
|
-
def find_by_(format_str, args, options) # :nodoc:
|
571
|
-
options[:limit] = 1
|
572
|
-
find_all_by_(format_str, args, options).first
|
573
|
-
end
|
574
|
-
|
575
|
-
# Misc
|
576
|
-
|
577
|
-
def method_missing(method, *args) # :nodoc:
|
578
|
-
if method.to_s[/^(find_all_by_|find_by_|select_all_by_|select_by_)/]
|
579
|
-
# get rid of the find ones, only select now
|
580
|
-
to_send_to = $1
|
581
|
-
attributes = method.to_s[$1.length..method.to_s.length]
|
582
|
-
# puts 'attributes=' + attributes
|
583
|
-
if to_send_to[0...4] == "find"
|
584
|
-
to_send_to = "select" + to_send_to[4..to_send_to.length]
|
585
|
-
# puts 'CONVERTED ' + $1 + " to " + to_send_to
|
586
|
-
end
|
587
|
-
|
588
|
-
options = args.last.is_a?(Hash) ? args.pop : {}
|
589
|
-
__send__(to_send_to, attributes, args, options)
|
590
|
-
else
|
591
|
-
super(method, *args)
|
592
|
-
end
|
593
|
-
end
|
594
|
-
|
595
|
-
def build_select(options) # :nodoc:
|
596
|
-
select = options[:select] || '*'
|
597
|
-
select = options[:count] ? "count(*)" : select
|
598
|
-
#puts 'select=' + select.to_s
|
599
|
-
from = options[:from] || domain
|
600
|
-
condition_fields = parse_condition_fields(options[:conditions])
|
601
|
-
conditions = options[:conditions] ? "#{build_conditions(options[:conditions])}" : ''
|
602
|
-
order = options[:order] ? " ORDER BY #{options[:order]}" : ''
|
603
|
-
limit = options[:limit] ? " LIMIT #{options[:limit]}" : ''
|
604
|
-
# mix sort by argument (it must present in response)
|
605
|
-
unless order.blank?
|
606
|
-
sort_by, sort_order = sort_options(options[:order])
|
607
|
-
if condition_fields.nil? || !condition_fields.include?(sort_by)
|
608
|
-
# conditions << (conditions.blank? ? " WHERE " : " AND ") << "(#{sort_by} IS NOT NULL)"
|
609
|
-
conditions = (conditions.blank? ? "" : "(#{conditions}) AND ") << "(#{sort_by} IS NOT NULL)"
|
610
|
-
else
|
611
|
-
# puts 'skipping is not null on sort because already there.'
|
612
|
-
end
|
613
|
-
|
614
|
-
end
|
615
|
-
conditions = conditions.blank? ? "" : " WHERE #{conditions}"
|
616
|
-
# puts 'CONDITIONS=' + conditions
|
617
|
-
"SELECT #{select} FROM `#{from}`#{conditions}#{order}#{limit}"
|
618
|
-
end
|
619
|
-
|
620
|
-
def build_conditions(conditions) # :nodoc:
|
621
|
-
case
|
622
|
-
when conditions.is_a?(Array) then
|
623
|
-
connection.query_expression_from_array(conditions)
|
624
|
-
when conditions.is_a?(Hash) then
|
625
|
-
connection.query_expression_from_hash(conditions)
|
626
|
-
else
|
627
|
-
conditions
|
628
|
-
end
|
629
|
-
end
|
630
|
-
|
631
|
-
# This will currently return and's, or's and betweens. Doesn't hurt anything, but could remove.
|
632
|
-
def parse_condition_fields(conditions)
|
633
|
-
return nil unless conditions && conditions.present?
|
634
|
-
rx = /\b(\w*)[\s|>=|<=|!=|=|>|<|like|between]/
|
635
|
-
fields = conditions[0].scan(rx)
|
636
|
-
# puts 'condition_fields = ' + fields.inspect
|
637
|
-
fields.flatten
|
638
|
-
end
|
226
|
+
# Change the default domain name to user defined.
|
227
|
+
#
|
228
|
+
# class Client < Aws::ActiveSdb::Base
|
229
|
+
# set_domain_name :foreign_clients
|
230
|
+
# end
|
231
|
+
#
|
232
|
+
def set_domain_name(domain)
|
233
|
+
@domain = domain.to_s
|
234
|
+
end
|
639
235
|
|
640
|
-
|
236
|
+
# Create domain at SDB.
|
237
|
+
# Raises no errors if the domain already exists.
|
238
|
+
#
|
239
|
+
# class Client < Aws::ActiveSdb::Base
|
240
|
+
# end
|
241
|
+
# Client.create_domain #=> {:request_id=>"6fc652a0-0000-41d5-91f4-3ed390a3d3b2", :box_usage=>"0.0055590278"}
|
242
|
+
#
|
243
|
+
def create_domain(dom=nil)
|
244
|
+
dom = domain if dom.nil?
|
245
|
+
puts "Creating new SimpleDB Domain: " + dom
|
246
|
+
connection.create_domain(dom)
|
247
|
+
end
|
641
248
|
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
# puts item.inspect #=> #<Client:0xb77a2698 @new_record=true, @attributes={"name"=>["Jon"], "toys"=>["girls", "beer", "pub"]}>
|
655
|
-
# item.save #=> {"name"=>["Jon"], "id"=>"c03edb7e-e45c-11dc-bede-001bfc466dd7", "toys"=>["girls", "beer", "pub"]}
|
656
|
-
# puts item.inspect #=> #<Client:0xb77a2698 @new_record=false, @attributes={"name"=>["Jon"], "id"=>"c03edb7e-e45c-11dc-bede-001bfc466dd7", "toys"=>["girls", "beer", "pub"]}>
|
657
|
-
#
|
658
|
-
def initialize(attrs={})
|
659
|
-
@attributes = uniq_values(attrs)
|
660
|
-
@new_record = true
|
661
|
-
end
|
249
|
+
# Remove domain from SDB.
|
250
|
+
# Raises no errors if the domain does not exist.
|
251
|
+
#
|
252
|
+
# class Client < Aws::ActiveSdb::Base
|
253
|
+
# end
|
254
|
+
# Client.delete_domain #=> {:request_id=>"e14d90d3-0000-4898-9995-0de28cdda270", :box_usage=>"0.0055590278"}
|
255
|
+
#
|
256
|
+
def delete_domain(dom=nil)
|
257
|
+
dom = domain if dom.nil?
|
258
|
+
puts "!!! DELETING SimpleDB Domain: " + dom
|
259
|
+
connection.delete_domain(dom)
|
260
|
+
end
|
662
261
|
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
262
|
+
#
|
263
|
+
# See select(), original find with QUERY syntax is deprecated so now find and select are synonyms.
|
264
|
+
#
|
265
|
+
def find(*args)
|
266
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
267
|
+
case args.first
|
268
|
+
when nil then
|
269
|
+
raise "Invalid parameters passed to find: nil."
|
270
|
+
when :all then
|
271
|
+
sql_select(options)[:items]
|
272
|
+
when :first then
|
273
|
+
sql_select(options.merge(:limit => 1))[:items].first
|
274
|
+
when :count then
|
275
|
+
res = sql_select(options.merge(:count => true))[:count]
|
276
|
+
res
|
277
|
+
else
|
278
|
+
res = select_from_ids(args, options)
|
279
|
+
return res[:single] if res[:single]
|
280
|
+
return res[:items]
|
281
|
+
end
|
282
|
+
end
|
667
283
|
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
284
|
+
#
|
285
|
+
# Same as find, but will return SimpleDB metadata like :request_id and :box_usage
|
286
|
+
#
|
287
|
+
def find_with_metadata(*args)
|
288
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
289
|
+
case args.first
|
290
|
+
when nil then
|
291
|
+
raise "Invalid parameters passed to find: nil."
|
292
|
+
when :all then
|
293
|
+
sql_select(options)
|
294
|
+
when :first then
|
295
|
+
sql_select(options.merge(:limit => 1))
|
296
|
+
when :count then
|
297
|
+
res = sql_select(options.merge(:count => true))
|
298
|
+
res
|
299
|
+
else
|
300
|
+
select_from_ids args, options
|
301
|
+
end
|
302
|
+
end
|
679
303
|
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
304
|
+
# Perform a SQL-like select request.
|
305
|
+
#
|
306
|
+
# Single record:
|
307
|
+
#
|
308
|
+
# Client.select(:first)
|
309
|
+
# Client.select(:first, :conditions=> [ "name=? AND wife=?", "Jon", "Sandy"])
|
310
|
+
# Client.select(:first, :conditions=> { :name=>"Jon", :wife=>"Sandy" }, :select => :girfriends)
|
311
|
+
#
|
312
|
+
# Bunch of records:
|
313
|
+
#
|
314
|
+
# Client.select(:all)
|
315
|
+
# Client.select(:all, :limit => 10)
|
316
|
+
# Client.select(:all, :conditions=> [ "name=? AND 'girlfriend'=?", "Jon", "Judy"])
|
317
|
+
# Client.select(:all, :conditions=> { :name=>"Sandy" }, :limit => 3)
|
318
|
+
#
|
319
|
+
# Records by ids:
|
320
|
+
#
|
321
|
+
# Client.select('1')
|
322
|
+
# Client.select('1234987b4583475347523948')
|
323
|
+
# Client.select('1','2','3','4', :conditions=> ["toys=?", "beer"])
|
324
|
+
#
|
325
|
+
# Find helpers: Aws::ActiveSdb::Base.select_by_... and Aws::ActiveSdb::Base.select_all_by_...
|
326
|
+
#
|
327
|
+
# Client.select_by_name('Matias Rust')
|
328
|
+
# Client.select_by_name_and_city('Putin','Moscow')
|
329
|
+
# Client.select_by_name_and_city_and_post('Medvedev','Moscow','president')
|
330
|
+
#
|
331
|
+
# Client.select_all_by_author('G.Bush jr')
|
332
|
+
# Client.select_all_by_age_and_gender_and_ethnicity('34','male','russian')
|
333
|
+
# Client.select_all_by_gender_and_country('male', 'Russia', :order => 'name')
|
334
|
+
#
|
335
|
+
# Continue listing:
|
336
|
+
#
|
337
|
+
# # initial listing
|
338
|
+
# Client.select(:all, :limit => 10)
|
339
|
+
# # continue listing
|
340
|
+
# begin
|
341
|
+
# Client.select(:all, :limit => 10, :next_token => Client.next_token)
|
342
|
+
# end while Client.next_token
|
343
|
+
#
|
344
|
+
# Sort oder:
|
345
|
+
# If :order=>'attribute' option is specified then result response (ordered by 'attribute') will contain only items where attribute is defined (is not null).
|
346
|
+
#
|
347
|
+
# Client.select(:all) # returns all records
|
348
|
+
# Client.select(:all, :order => 'gender') # returns all records ordered by gender where gender attribute exists
|
349
|
+
# Client.select(:all, :order => 'name desc') # returns all records ordered by name in desc order where name attribute exists
|
350
|
+
#
|
351
|
+
# see http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?UsingSelect.html
|
352
|
+
#
|
353
|
+
def select(*args)
|
354
|
+
find(*args)
|
355
|
+
end
|
684
356
|
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
end
|
357
|
+
def generate_id # :nodoc:
|
358
|
+
UUIDTools::UUID.timestamp_create().to_s
|
359
|
+
end
|
689
360
|
|
690
|
-
|
691
|
-
#
|
692
|
-
# puts item.attributes.inspect #=> {"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "clew", "mice"]}
|
693
|
-
#
|
694
|
-
def attributes
|
695
|
-
@attributes.dup
|
696
|
-
end
|
361
|
+
protected
|
697
362
|
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
# puts item.attributes.inspect #=> {"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "clew", "mice"]}
|
702
|
-
# # set new attributes ('id' is missed)
|
703
|
-
# item.attributes = { 'name'=>'Dog', 'toys'=>['bones','cats'] }
|
704
|
-
# puts item.attributes.inspect #=> {"name"=>["Dog"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["bones", "cats"]}
|
705
|
-
# # set new attributes ('id' is set)
|
706
|
-
# item.attributes = { 'id' => 'blah-blah', 'name'=>'Birds', 'toys'=>['seeds','dogs tail'] }
|
707
|
-
# puts item.attributes.inspect #=> {"name"=>["Birds"], "id"=>"blah-blah", "toys"=>["seeds", "dogs tail"]}
|
708
|
-
#
|
709
|
-
def attributes=(attrs)
|
710
|
-
old_id = @attributes['id']
|
711
|
-
@attributes = uniq_values(attrs)
|
712
|
-
@attributes['id'] = old_id if @attributes['id'].blank? && !old_id.blank?
|
713
|
-
self.attributes
|
714
|
-
end
|
363
|
+
def logger
|
364
|
+
SimpleRecord.logger
|
365
|
+
end
|
715
366
|
|
716
|
-
|
717
|
-
|
718
|
-
|
367
|
+
# Select
|
368
|
+
|
369
|
+
def select_from_ids(args, options) # :nodoc:
|
370
|
+
cond = []
|
371
|
+
# detect amount of records requested
|
372
|
+
bunch_of_records_requested = args.size > 1 || args.first.is_a?(Array)
|
373
|
+
# flatten ids
|
374
|
+
args = args.to_a.flatten
|
375
|
+
args.each { |id| cond << "itemName() = #{self.connection.escape(id)}" }
|
376
|
+
ids_cond = "(#{cond.join(' OR ')})"
|
377
|
+
# user defined :conditions to string (if it was defined)
|
378
|
+
options[:conditions] = build_conditions(options[:conditions])
|
379
|
+
# join ids condition and user defined conditions
|
380
|
+
options[:conditions] = options[:conditions].blank? ? ids_cond : "(#{options[:conditions]}) AND #{ids_cond}"
|
381
|
+
#puts 'options=' + options.inspect
|
382
|
+
result = sql_select(options)
|
383
|
+
puts 'select_from_ids result=' + result.inspect
|
384
|
+
# if one record was requested then return it
|
385
|
+
unless bunch_of_records_requested
|
386
|
+
result[:single_only] = true
|
387
|
+
record = result[:items].first
|
388
|
+
# railse if nothing was found
|
389
|
+
raise SimpleRecord::RecordNotFound.new("Couldn't find #{name} with ID #{args}") unless record || is_sharded?
|
390
|
+
result[:single] = record
|
391
|
+
else
|
392
|
+
# if a bunch of records was requested then return check that we found all of them
|
393
|
+
# and return as an array
|
394
|
+
puts 'is_sharded? ' + is_sharded?.to_s
|
395
|
+
unless is_sharded? || args.size == result[:items].size
|
396
|
+
# todo: might make sense to return the array but with nil values in the slots where an item wasn't found?
|
397
|
+
id_list = args.map { |i| "'#{i}'" }.join(',')
|
398
|
+
raise SimpleRecord::RecordNotFound.new("Couldn't find all #{name} with IDs (#{id_list}) (found #{result[:items].size} results, but was looking for #{args.size})")
|
399
|
+
else
|
400
|
+
result
|
401
|
+
end
|
402
|
+
end
|
403
|
+
result
|
404
|
+
end
|
719
405
|
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
406
|
+
def sql_select(options) # :nodoc:
|
407
|
+
count = options[:count] || false
|
408
|
+
#puts 'count? ' + count.to_s
|
409
|
+
@next_token = options[:next_token]
|
410
|
+
@consistent_read = options[:consistent_read]
|
411
|
+
select_expression = build_select(options)
|
412
|
+
logger.debug 'SELECT=' + select_expression
|
413
|
+
# request items
|
414
|
+
query_result = self.connection.select(select_expression, options)
|
415
|
+
# puts 'QR=' + query_result.inspect
|
416
|
+
@next_token = query_result[:next_token]
|
417
|
+
ret = {}
|
418
|
+
if count
|
419
|
+
ret[:count] = query_result.delete(:items)[0]["Domain"]["Count"][0].to_i
|
420
|
+
ret.merge!(query_result)
|
421
|
+
return ret
|
422
|
+
end
|
423
|
+
|
424
|
+
items = query_result.delete(:items).map do |hash|
|
425
|
+
id, attributes = hash.shift
|
426
|
+
new_item = self.new()
|
427
|
+
new_item.initialize_from_db(attributes.merge({'id' => id}))
|
428
|
+
new_item.mark_as_old
|
429
|
+
new_item
|
430
|
+
end
|
431
|
+
ret[:items] = items
|
432
|
+
ret.merge!(query_result)
|
433
|
+
ret
|
434
|
+
end
|
724
435
|
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
436
|
+
# select_by helpers
|
437
|
+
def select_all_by_(format_str, args, options) # :nodoc:
|
438
|
+
fields = format_str.to_s.sub(/^select_(all_)?by_/, '').split('_and_')
|
439
|
+
conditions = fields.map { |field| "#{field}=?" }.join(' AND ')
|
440
|
+
options[:conditions] = [conditions, *args]
|
441
|
+
find(:all, options)
|
442
|
+
end
|
732
443
|
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
# puts item['Cat'].inspect #=> ["Whiskas", "chicken"]
|
738
|
-
#
|
739
|
-
def []=(attribute, values)
|
740
|
-
attribute = attribute.to_s
|
741
|
-
@attributes[attribute] = attribute == 'id' ? values.to_s : values.is_a?(Array) ? values.uniq : [values]
|
444
|
+
def select_by_(format_str, args, options) # :nodoc:
|
445
|
+
options[:limit] = 1
|
446
|
+
select_all_by_(format_str, args, options).first
|
447
|
+
end
|
742
448
|
|
743
|
-
|
449
|
+
# Query
|
450
|
+
|
451
|
+
# Returns an array of query attributes.
|
452
|
+
# Query_expression must be a well formated SDB query string:
|
453
|
+
# query_attributes("['title' starts-with 'O\\'Reily'] intersection ['year' = '2007']") #=> ["title", "year"]
|
454
|
+
def query_attributes(query_expression) # :nodoc:
|
455
|
+
attrs = []
|
456
|
+
array = query_expression.scan(/['"](.*?[^\\])['"]/).flatten
|
457
|
+
until array.empty? do
|
458
|
+
attrs << array.shift # skip it's value
|
459
|
+
array.shift #
|
460
|
+
end
|
461
|
+
attrs
|
462
|
+
end
|
744
463
|
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
def reload
|
751
|
-
raise_on_id_absence
|
752
|
-
old_id = id
|
753
|
-
attrs = connection.get_attributes(domain, id)[:attributes]
|
754
|
-
@attributes = {}
|
755
|
-
unless attrs.blank?
|
756
|
-
attrs.each { |attribute, values| @attributes[attribute] = values }
|
757
|
-
@attributes['id'] = old_id
|
758
|
-
end
|
759
|
-
mark_as_old
|
760
|
-
@attributes
|
761
|
-
end
|
464
|
+
# Returns an array of [attribute_name, 'asc'|'desc']
|
465
|
+
def sort_options(sort_string)
|
466
|
+
sort_string[/['"]?(\w+)['"]? *(asc|desc)?/i]
|
467
|
+
[$1, ($2 || 'asc')]
|
468
|
+
end
|
762
469
|
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
470
|
+
# Perform a query request.
|
471
|
+
#
|
472
|
+
# Options
|
473
|
+
# :query_expression nil | string | array
|
474
|
+
# :max_number_of_items nil | integer
|
475
|
+
# :next_token nil | string
|
476
|
+
# :sort_option nil | string "name desc|asc"
|
477
|
+
#
|
478
|
+
def query(options) # :nodoc:
|
479
|
+
@next_token = options[:next_token]
|
480
|
+
@consistent_read = options[:consistent_read]
|
481
|
+
query_expression = build_conditions(options[:query_expression])
|
482
|
+
# add sort_options to the query_expression
|
483
|
+
if options[:sort_option]
|
484
|
+
sort_by, sort_order = sort_options(options[:sort_option])
|
485
|
+
sort_query_expression = "['#{sort_by}' starts-with '']"
|
486
|
+
sort_by_expression = " sort '#{sort_by}' #{sort_order}"
|
487
|
+
# make query_expression to be a string (it may be null)
|
488
|
+
query_expression = query_expression.to_s
|
489
|
+
# quote from Amazon:
|
490
|
+
# The sort attribute must be present in at least one of the predicates of the query expression.
|
491
|
+
if query_expression.blank?
|
492
|
+
query_expression = sort_query_expression
|
493
|
+
elsif !query_attributes(query_expression).include?(sort_by)
|
494
|
+
query_expression += " intersection #{sort_query_expression}"
|
495
|
+
end
|
496
|
+
query_expression += sort_by_expression
|
497
|
+
end
|
498
|
+
# request items
|
499
|
+
query_result = self.connection.query(domain, query_expression, options[:max_number_of_items], @next_token, @consistent_read)
|
500
|
+
@next_token = query_result[:next_token]
|
501
|
+
items = query_result[:items].map do |name|
|
502
|
+
new_item = self.new('id' => name)
|
503
|
+
new_item.mark_as_old
|
504
|
+
reload_if_exists(record) if options[:auto_load]
|
505
|
+
new_item
|
506
|
+
end
|
507
|
+
items
|
508
|
+
end
|
790
509
|
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
# sandy = Client.new(:name => 'Sandy') #=> #<Client:0xb775a7a8 @attributes={"name"=>["Sandy"]}, @new_record=true>
|
796
|
-
# sandy['toys'] = 'boys'
|
797
|
-
# sandy.put
|
798
|
-
# sandy['toys'] = 'patchwork'
|
799
|
-
# sandy.put
|
800
|
-
# sandy['toys'] = 'kids'
|
801
|
-
# sandy.put
|
802
|
-
# puts sandy.attributes.inspect #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]}
|
803
|
-
# sandy.reload #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"]}
|
804
|
-
#
|
805
|
-
# compare to +save+ method
|
806
|
-
def put
|
807
|
-
@attributes = uniq_values(@attributes)
|
808
|
-
prepare_for_update
|
809
|
-
attrs = @attributes.dup
|
810
|
-
attrs.delete('id')
|
811
|
-
connection.put_attributes(domain, id, attrs) unless attrs.blank?
|
812
|
-
connection.put_attributes(domain, id, {'id' => id}, :replace)
|
813
|
-
mark_as_old
|
814
|
-
@attributes
|
815
|
-
end
|
510
|
+
# reload a record unless it is nil
|
511
|
+
def reload_if_exists(record) # :nodoc:
|
512
|
+
record && record.reload
|
513
|
+
end
|
816
514
|
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
#
|
821
|
-
# see to +put+ method
|
822
|
-
def put_attributes(attrs)
|
823
|
-
attrs = uniq_values(attrs)
|
824
|
-
prepare_for_update
|
825
|
-
# if 'id' is present in attrs hash:
|
826
|
-
# replace internal 'id' attribute and remove it from the attributes to be sent
|
827
|
-
@attributes['id'] = attrs['id'] unless attrs['id'].blank?
|
828
|
-
attrs.delete('id')
|
829
|
-
# add new values to all attributes from list
|
830
|
-
connection.put_attributes(domain, id, attrs) unless attrs.blank?
|
831
|
-
connection.put_attributes(domain, id, {'id' => id}, :replace)
|
832
|
-
attrs.each do |attribute, values|
|
833
|
-
@attributes[attribute] ||= []
|
834
|
-
@attributes[attribute] += values
|
835
|
-
@attributes[attribute].uniq!
|
836
|
-
end
|
837
|
-
mark_as_old
|
838
|
-
attributes
|
839
|
-
end
|
515
|
+
def reload_all_records(*list) # :nodoc:
|
516
|
+
list.flatten.each { |record| reload_if_exists(record) }
|
517
|
+
end
|
840
518
|
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
# sandy.save
|
850
|
-
# sandy['toys'] = 'kids'
|
851
|
-
# sandy.save
|
852
|
-
# puts sandy.attributes.inspect #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]}
|
853
|
-
# sandy.reload #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]}
|
854
|
-
#
|
855
|
-
# Options:
|
856
|
-
# - :except => Array of attributes to NOT save
|
857
|
-
#
|
858
|
-
# compare to +put+ method
|
859
|
-
def save2(options={})
|
860
|
-
options[:create_domain] = true if options[:create_domain].nil?
|
861
|
-
pre_save2
|
862
|
-
atts_to_save = @attributes.dup
|
863
|
-
#puts 'atts_to_save=' + atts_to_save.inspect
|
864
|
-
#options = params.first.is_a?(Hash) ? params.pop : {}
|
865
|
-
if options[:except]
|
866
|
-
options[:except].each do |e|
|
867
|
-
atts_to_save.delete(e).inspect
|
868
|
-
end
|
869
|
-
end
|
870
|
-
if options[:dirty] # Only used in simple_record right now
|
871
|
-
# only save if the attribute is dirty
|
872
|
-
dirty_atts = options[:dirty_atts]
|
873
|
-
atts_to_save.delete_if { |key, value| !dirty_atts.has_key?(key) }
|
874
|
-
end
|
875
|
-
dom = options[:domain] || domain
|
876
|
-
#puts 'atts_to_save2=' + atts_to_save.inspect
|
877
|
-
connection.put_attributes(dom, id, atts_to_save, :replace, options)
|
878
|
-
apres_save2
|
879
|
-
@attributes
|
880
|
-
end
|
519
|
+
def find_every(options) # :nodoc:
|
520
|
+
records = query(:query_expression => options[:conditions],
|
521
|
+
:max_number_of_items => options[:limit],
|
522
|
+
:next_token => options[:next_token],
|
523
|
+
:sort_option => options[:sort] || options[:order],
|
524
|
+
:consistent_read => options[:consistent_read])
|
525
|
+
options[:auto_load] ? reload_all_records(records) : records
|
526
|
+
end
|
881
527
|
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
|
528
|
+
def find_initial(options) # :nodoc:
|
529
|
+
options[:limit] = 1
|
530
|
+
record = find_every(options).first
|
531
|
+
options[:auto_load] ? reload_all_records(record).first : record
|
532
|
+
end
|
886
533
|
|
887
|
-
|
888
|
-
|
889
|
-
|
534
|
+
def find_from_ids(args, options) # :nodoc:
|
535
|
+
cond = []
|
536
|
+
# detect amount of records requested
|
537
|
+
bunch_of_records_requested = args.size > 1 || args.first.is_a?(Array)
|
538
|
+
# flatten ids
|
539
|
+
args = args.to_a.flatten
|
540
|
+
args.each { |id| cond << "'id'=#{self.connection.escape(id)}" }
|
541
|
+
ids_cond = "[#{cond.join(' OR ')}]"
|
542
|
+
# user defined :conditions to string (if it was defined)
|
543
|
+
options[:conditions] = build_conditions(options[:conditions])
|
544
|
+
# join ids condition and user defined conditions
|
545
|
+
options[:conditions] = options[:conditions].blank? ? ids_cond : "#{options[:conditions]} intersection #{ids_cond}"
|
546
|
+
result = find_every(options)
|
547
|
+
# if one record was requested then return it
|
548
|
+
unless bunch_of_records_requested
|
549
|
+
record = result.first
|
550
|
+
# railse if nothing was found
|
551
|
+
raise ActiveSdbError.new("Couldn't find #{name} with ID #{args}") unless record
|
552
|
+
options[:auto_load] ? reload_all_records(record).first : record
|
553
|
+
else
|
554
|
+
# if a bunch of records was requested then return check that we found all of them
|
555
|
+
# and return as an array
|
556
|
+
unless args.size == result.size
|
557
|
+
id_list = args.map { |i| "'#{i}'" }.join(',')
|
558
|
+
raise ActiveSdbError.new("Couldn't find all #{name} with IDs (#{id_list}) (found #{result.size} results, but was looking for #{args.size})")
|
559
|
+
else
|
560
|
+
options[:auto_load] ? reload_all_records(result) : result
|
561
|
+
end
|
562
|
+
end
|
563
|
+
end
|
890
564
|
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
prepare_for_update
|
899
|
-
attrs = uniq_values(attrs)
|
900
|
-
# if 'id' is present in attrs hash then replace internal 'id' attribute
|
901
|
-
unless attrs['id'].blank?
|
902
|
-
@attributes['id'] = attrs['id']
|
903
|
-
else
|
904
|
-
attrs['id'] = id
|
905
|
-
end
|
906
|
-
connection.put_attributes(domain, id, attrs, :replace) unless attrs.blank?
|
907
|
-
attrs.each { |attribute, values| attrs[attribute] = values }
|
908
|
-
mark_as_old
|
909
|
-
attrs
|
910
|
-
end
|
565
|
+
# find_by helpers
|
566
|
+
def find_all_by_(format_str, args, options) # :nodoc:
|
567
|
+
fields = format_str.to_s.sub(/^find_(all_)?by_/, '').split('_and_')
|
568
|
+
conditions = fields.map { |field| "['#{field}'=?]" }.join(' intersection ')
|
569
|
+
options[:conditions] = [conditions, *args]
|
570
|
+
find(:all, options)
|
571
|
+
end
|
911
572
|
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
# sandy.reload
|
917
|
-
# puts sandy.inspect #=> #<Client:0xb77b48fc @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"]}>
|
918
|
-
# puts sandy.delete_values('toys' => 'patchwork') #=> { 'toys' => ['patchwork'] }
|
919
|
-
# puts sandy.inspect #=> #<Client:0xb77b48fc @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids"]}>
|
920
|
-
#
|
921
|
-
def delete_values(attrs)
|
922
|
-
raise_on_id_absence
|
923
|
-
attrs = uniq_values(attrs)
|
924
|
-
attrs.delete('id')
|
925
|
-
unless attrs.blank?
|
926
|
-
connection.delete_attributes(domain, id, attrs)
|
927
|
-
attrs.each do |attribute, values|
|
928
|
-
# remove the values from the attribute
|
929
|
-
if @attributes[attribute]
|
930
|
-
@attributes[attribute] -= values
|
931
|
-
else
|
932
|
-
# if the attribute is unknown remove it from a resulting list of fixed attributes
|
933
|
-
attrs.delete(attribute)
|
934
|
-
end
|
935
|
-
end
|
936
|
-
end
|
937
|
-
attrs
|
938
|
-
end
|
573
|
+
def find_by_(format_str, args, options) # :nodoc:
|
574
|
+
options[:limit] = 1
|
575
|
+
find_all_by_(format_str, args, options).first
|
576
|
+
end
|
939
577
|
|
940
|
-
|
941
|
-
# +attrs_list+ is an array or comma separated list of attributes names.
|
942
|
-
# Returns the list of deleted attributes.
|
943
|
-
#
|
944
|
-
# sandy = Client.find_by_name 'Sandy'
|
945
|
-
# sandy.reload
|
946
|
-
# puts sandy.inspect #=> #<Client:0xb7761d28 @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"}>
|
947
|
-
# puts sandy.delete_attributes('toys') #=> ['toys']
|
948
|
-
# puts sandy.inspect #=> #<Client:0xb7761d28 @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7"}>
|
949
|
-
#
|
950
|
-
def delete_attributes(*attrs_list)
|
951
|
-
raise_on_id_absence
|
952
|
-
attrs_list = attrs_list.flatten.map { |attribute| attribute.to_s }
|
953
|
-
attrs_list.delete('id')
|
954
|
-
unless attrs_list.blank?
|
955
|
-
connection.delete_attributes(domain, id, attrs_list)
|
956
|
-
attrs_list.each { |attribute| @attributes.delete(attribute) }
|
957
|
-
end
|
958
|
-
attrs_list
|
959
|
-
end
|
578
|
+
# Misc
|
960
579
|
|
961
|
-
|
962
|
-
|
963
|
-
#
|
964
|
-
|
965
|
-
|
966
|
-
|
967
|
-
|
968
|
-
|
969
|
-
|
970
|
-
def delete(options={})
|
971
|
-
raise_on_id_absence
|
972
|
-
connection.delete_attributes(options[:domain] || domain, id)
|
580
|
+
def method_missing(method, *args) # :nodoc:
|
581
|
+
if method.to_s[/^(find_all_by_|find_by_|select_all_by_|select_by_)/]
|
582
|
+
# get rid of the find ones, only select now
|
583
|
+
to_send_to = $1
|
584
|
+
attributes = method.to_s[$1.length..method.to_s.length]
|
585
|
+
# puts 'attributes=' + attributes
|
586
|
+
if to_send_to[0...4] == "find"
|
587
|
+
to_send_to = "select" + to_send_to[4..to_send_to.length]
|
588
|
+
# puts 'CONVERTED ' + $1 + " to " + to_send_to
|
973
589
|
end
|
974
590
|
|
975
|
-
|
976
|
-
|
977
|
-
|
978
|
-
|
591
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
592
|
+
__send__(to_send_to, attributes, args, options)
|
593
|
+
else
|
594
|
+
super(method, *args)
|
595
|
+
end
|
596
|
+
end
|
979
597
|
|
980
|
-
|
981
|
-
|
598
|
+
def build_select(options) # :nodoc:
|
599
|
+
select = options[:select] || '*'
|
600
|
+
select = options[:count] ? "count(*)" : select
|
601
|
+
#puts 'select=' + select.to_s
|
602
|
+
from = options[:from] || domain
|
603
|
+
condition_fields = parse_condition_fields(options[:conditions])
|
604
|
+
conditions = options[:conditions] ? "#{build_conditions(options[:conditions])}" : ''
|
605
|
+
order = options[:order] ? " ORDER BY #{options[:order]}" : ''
|
606
|
+
limit = options[:limit] ? " LIMIT #{options[:limit]}" : ''
|
607
|
+
# mix sort by argument (it must present in response)
|
608
|
+
unless order.blank?
|
609
|
+
sort_by, sort_order = sort_options(options[:order])
|
610
|
+
if condition_fields.nil? || !condition_fields.include?(sort_by)
|
611
|
+
# conditions << (conditions.blank? ? " WHERE " : " AND ") << "(#{sort_by} IS NOT NULL)"
|
612
|
+
conditions = (conditions.blank? ? "" : "(#{conditions}) AND ") << "(#{sort_by} IS NOT NULL)"
|
613
|
+
else
|
614
|
+
# puts 'skipping is not null on sort because already there.'
|
982
615
|
end
|
983
616
|
|
984
|
-
|
617
|
+
end
|
618
|
+
conditions = conditions.blank? ? "" : " WHERE #{conditions}"
|
619
|
+
# puts 'CONDITIONS=' + conditions
|
620
|
+
"SELECT #{select} FROM `#{from}`#{conditions}#{order}#{limit}"
|
621
|
+
end
|
985
622
|
|
986
|
-
|
987
|
-
|
988
|
-
|
623
|
+
def build_conditions(conditions) # :nodoc:
|
624
|
+
case
|
625
|
+
when conditions.is_a?(Array) then
|
626
|
+
connection.query_expression_from_array(conditions)
|
627
|
+
when conditions.is_a?(Hash) then
|
628
|
+
connection.query_expression_from_hash(conditions)
|
629
|
+
else
|
630
|
+
conditions
|
631
|
+
end
|
632
|
+
end
|
989
633
|
|
990
|
-
|
991
|
-
|
992
|
-
|
634
|
+
# This will currently return and's, or's and betweens. Doesn't hurt anything, but could remove.
|
635
|
+
def parse_condition_fields(conditions)
|
636
|
+
return nil unless conditions && conditions.present?
|
637
|
+
rx = /\b(\w*)[\s|>=|<=|!=|=|>|<|like|between]/
|
638
|
+
fields = conditions[0].scan(rx)
|
639
|
+
# puts 'condition_fields = ' + fields.inspect
|
640
|
+
fields.flatten
|
641
|
+
end
|
993
642
|
|
994
|
-
|
995
|
-
|
996
|
-
|
997
|
-
|
998
|
-
|
999
|
-
|
1000
|
-
|
643
|
+
end
|
644
|
+
|
645
|
+
public
|
646
|
+
|
647
|
+
# instance attributes
|
648
|
+
attr_accessor :attributes
|
649
|
+
|
650
|
+
# item name
|
651
|
+
attr_accessor :id
|
652
|
+
|
653
|
+
# Create new Item instance.
|
654
|
+
# +attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
|
655
|
+
#
|
656
|
+
# item = Client.new('name' => 'Jon', 'toys' => ['girls', 'beer', 'pub'])
|
657
|
+
# puts item.inspect #=> #<Client:0xb77a2698 @new_record=true, @attributes={"name"=>["Jon"], "toys"=>["girls", "beer", "pub"]}>
|
658
|
+
# item.save #=> {"name"=>["Jon"], "id"=>"c03edb7e-e45c-11dc-bede-001bfc466dd7", "toys"=>["girls", "beer", "pub"]}
|
659
|
+
# puts item.inspect #=> #<Client:0xb77a2698 @new_record=false, @attributes={"name"=>["Jon"], "id"=>"c03edb7e-e45c-11dc-bede-001bfc466dd7", "toys"=>["girls", "beer", "pub"]}>
|
660
|
+
#
|
661
|
+
def initialize(attrs={})
|
662
|
+
@attributes = uniq_values(attrs)
|
663
|
+
@new_record = true
|
664
|
+
end
|
665
|
+
|
666
|
+
# This is to separate initialization from user vs coming from db (ie: find())
|
667
|
+
def initialize_from_db(attrs={})
|
668
|
+
initialize(attrs)
|
669
|
+
end
|
670
|
+
|
671
|
+
# Create and save new Item instance.
|
672
|
+
# +Attributes+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
|
673
|
+
#
|
674
|
+
# item = Client.create('name' => 'Cat', 'toys' => ['Jons socks', 'mice', 'clew'])
|
675
|
+
# puts item.inspect #=> #<Client:0xb77a0a78 @new_record=false, @attributes={"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "mice", "clew"]}>
|
676
|
+
#
|
677
|
+
def self.create(attributes={})
|
678
|
+
item = self.new(attributes)
|
679
|
+
item.save
|
680
|
+
item
|
681
|
+
end
|
682
|
+
|
683
|
+
# Returns an item id. Same as: item['id'] or item.attributes['id']
|
684
|
+
def id
|
685
|
+
@attributes['id']
|
686
|
+
end
|
687
|
+
|
688
|
+
# Sets an item id.
|
689
|
+
def id=(id)
|
690
|
+
@attributes['id'] = id.to_s
|
691
|
+
end
|
692
|
+
|
693
|
+
# Returns a hash of all the attributes.
|
694
|
+
#
|
695
|
+
# puts item.attributes.inspect #=> {"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "clew", "mice"]}
|
696
|
+
#
|
697
|
+
def attributes
|
698
|
+
@attributes.dup
|
699
|
+
end
|
700
|
+
|
701
|
+
# Allows one to set all the attributes at once by passing in a hash with keys matching the attribute names.
|
702
|
+
# if '+id+' attribute is not set in new attributes has then it being derived from old attributes.
|
703
|
+
#
|
704
|
+
# puts item.attributes.inspect #=> {"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "clew", "mice"]}
|
705
|
+
# # set new attributes ('id' is missed)
|
706
|
+
# item.attributes = { 'name'=>'Dog', 'toys'=>['bones','cats'] }
|
707
|
+
# puts item.attributes.inspect #=> {"name"=>["Dog"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["bones", "cats"]}
|
708
|
+
# # set new attributes ('id' is set)
|
709
|
+
# item.attributes = { 'id' => 'blah-blah', 'name'=>'Birds', 'toys'=>['seeds','dogs tail'] }
|
710
|
+
# puts item.attributes.inspect #=> {"name"=>["Birds"], "id"=>"blah-blah", "toys"=>["seeds", "dogs tail"]}
|
711
|
+
#
|
712
|
+
def attributes=(attrs)
|
713
|
+
old_id = @attributes['id']
|
714
|
+
@attributes = uniq_values(attrs)
|
715
|
+
@attributes['id'] = old_id if @attributes['id'].blank? && !old_id.blank?
|
716
|
+
self.attributes
|
717
|
+
end
|
718
|
+
|
719
|
+
def connection
|
720
|
+
self.class.connection
|
721
|
+
end
|
722
|
+
|
723
|
+
# Item domain name.
|
724
|
+
def domain
|
725
|
+
self.class.domain
|
726
|
+
end
|
727
|
+
|
728
|
+
# Returns the values of the attribute identified by +attribute+.
|
729
|
+
#
|
730
|
+
# puts item['Cat'].inspect #=> ["Jons socks", "clew", "mice"]
|
731
|
+
#
|
732
|
+
def [](attribute)
|
733
|
+
@attributes[attribute.to_s]
|
734
|
+
end
|
735
|
+
|
736
|
+
# Updates the attribute identified by +attribute+ with the specified +values+.
|
737
|
+
#
|
738
|
+
# puts item['Cat'].inspect #=> ["Jons socks", "clew", "mice"]
|
739
|
+
# item['Cat'] = ["Whiskas", "chicken"]
|
740
|
+
# puts item['Cat'].inspect #=> ["Whiskas", "chicken"]
|
741
|
+
#
|
742
|
+
def []=(attribute, values)
|
743
|
+
attribute = attribute.to_s
|
744
|
+
@attributes[attribute] = attribute == 'id' ? values.to_s : values.is_a?(Array) ? values.uniq : [values]
|
745
|
+
|
746
|
+
end
|
747
|
+
|
748
|
+
# Reload attributes from SDB. Replaces in-memory attributes.
|
749
|
+
#
|
750
|
+
# item = Client.find_by_name('Cat') #=> #<Client:0xb77d0d40 @attributes={"id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7"}, @new_record=false>
|
751
|
+
# item.reload #=> #<Client:0xb77d0d40 @attributes={"id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "name"=>["Cat"], "toys"=>["Jons socks", "clew", "mice"]}, @new_record=false>
|
752
|
+
#
|
753
|
+
def reload
|
754
|
+
raise_on_id_absence
|
755
|
+
old_id = id
|
756
|
+
attrs = connection.get_attributes(domain, id)[:attributes]
|
757
|
+
@attributes = {}
|
758
|
+
unless attrs.blank?
|
759
|
+
attrs.each { |attribute, values| @attributes[attribute] = values }
|
760
|
+
@attributes['id'] = old_id
|
761
|
+
end
|
762
|
+
mark_as_old
|
763
|
+
@attributes
|
764
|
+
end
|
765
|
+
|
766
|
+
# Reload a set of attributes from SDB. Adds the loaded list to in-memory data.
|
767
|
+
# +attrs_list+ is an array or comma separated list of attributes names.
|
768
|
+
# Returns a hash of loaded attributes.
|
769
|
+
#
|
770
|
+
# This is not the best method to get a bunch of attributes because
|
771
|
+
# a web service call is being performed for every attribute.
|
772
|
+
#
|
773
|
+
# item = Client.find_by_name('Cat')
|
774
|
+
# item.reload_attributes('toys', 'name') #=> {"name"=>["Cat"], "toys"=>["Jons socks", "clew", "mice"]}
|
775
|
+
#
|
776
|
+
def reload_attributes(*attrs_list)
|
777
|
+
raise_on_id_absence
|
778
|
+
attrs_list = attrs_list.flatten.map { |attribute| attribute.to_s }
|
779
|
+
attrs_list.delete('id')
|
780
|
+
result = {}
|
781
|
+
attrs_list.flatten.uniq.each do |attribute|
|
782
|
+
attribute = attribute.to_s
|
783
|
+
values = connection.get_attributes(domain, id, attribute)[:attributes][attribute]
|
784
|
+
unless values.blank?
|
785
|
+
@attributes[attribute] = result[attribute] = values
|
786
|
+
else
|
787
|
+
@attributes.delete(attribute)
|
788
|
+
end
|
789
|
+
end
|
790
|
+
mark_as_old
|
791
|
+
result
|
792
|
+
end
|
793
|
+
|
794
|
+
# Stores in-memory attributes to SDB.
|
795
|
+
# Adds the attributes values to already stored at SDB.
|
796
|
+
# Returns a hash of stored attributes.
|
797
|
+
#
|
798
|
+
# sandy = Client.new(:name => 'Sandy') #=> #<Client:0xb775a7a8 @attributes={"name"=>["Sandy"]}, @new_record=true>
|
799
|
+
# sandy['toys'] = 'boys'
|
800
|
+
# sandy.put
|
801
|
+
# sandy['toys'] = 'patchwork'
|
802
|
+
# sandy.put
|
803
|
+
# sandy['toys'] = 'kids'
|
804
|
+
# sandy.put
|
805
|
+
# puts sandy.attributes.inspect #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]}
|
806
|
+
# sandy.reload #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"]}
|
807
|
+
#
|
808
|
+
# compare to +save+ method
|
809
|
+
def put
|
810
|
+
@attributes = uniq_values(@attributes)
|
811
|
+
prepare_for_update
|
812
|
+
attrs = @attributes.dup
|
813
|
+
attrs.delete('id')
|
814
|
+
connection.put_attributes(domain, id, attrs) unless attrs.blank?
|
815
|
+
connection.put_attributes(domain, id, {'id' => id}, :replace)
|
816
|
+
mark_as_old
|
817
|
+
@attributes
|
818
|
+
end
|
819
|
+
|
820
|
+
# Stores specified attributes.
|
821
|
+
# +attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
|
822
|
+
# Returns a hash of saved attributes.
|
823
|
+
#
|
824
|
+
# see to +put+ method
|
825
|
+
def put_attributes(attrs)
|
826
|
+
attrs = uniq_values(attrs)
|
827
|
+
prepare_for_update
|
828
|
+
# if 'id' is present in attrs hash:
|
829
|
+
# replace internal 'id' attribute and remove it from the attributes to be sent
|
830
|
+
@attributes['id'] = attrs['id'] unless attrs['id'].blank?
|
831
|
+
attrs.delete('id')
|
832
|
+
# add new values to all attributes from list
|
833
|
+
connection.put_attributes(domain, id, attrs) unless attrs.blank?
|
834
|
+
connection.put_attributes(domain, id, {'id' => id}, :replace)
|
835
|
+
attrs.each do |attribute, values|
|
836
|
+
@attributes[attribute] ||= []
|
837
|
+
@attributes[attribute] += values
|
838
|
+
@attributes[attribute].uniq!
|
839
|
+
end
|
840
|
+
mark_as_old
|
841
|
+
attributes
|
842
|
+
end
|
843
|
+
|
844
|
+
# Store in-memory attributes to SDB.
|
845
|
+
# Replaces the attributes values already stored at SDB by in-memory data.
|
846
|
+
# Returns a hash of stored attributes.
|
847
|
+
#
|
848
|
+
# sandy = Client.new(:name => 'Sandy') #=> #<Client:0xb775a7a8 @attributes={"name"=>["Sandy"]}, @new_record=true>
|
849
|
+
# sandy['toys'] = 'boys'
|
850
|
+
# sandy.save
|
851
|
+
# sandy['toys'] = 'patchwork'
|
852
|
+
# sandy.save
|
853
|
+
# sandy['toys'] = 'kids'
|
854
|
+
# sandy.save
|
855
|
+
# puts sandy.attributes.inspect #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]}
|
856
|
+
# sandy.reload #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]}
|
857
|
+
#
|
858
|
+
# Options:
|
859
|
+
# - :except => Array of attributes to NOT save
|
860
|
+
#
|
861
|
+
# compare to +put+ method
|
862
|
+
def save2(options={})
|
863
|
+
options[:create_domain] = true if options[:create_domain].nil?
|
864
|
+
pre_save2
|
865
|
+
atts_to_save = @attributes.dup
|
866
|
+
#puts 'atts_to_save=' + atts_to_save.inspect
|
867
|
+
#options = params.first.is_a?(Hash) ? params.pop : {}
|
868
|
+
if options[:except]
|
869
|
+
options[:except].each do |e|
|
870
|
+
atts_to_save.delete(e).inspect
|
871
|
+
end
|
872
|
+
end
|
873
|
+
if options[:dirty] # Only used in simple_record right now
|
874
|
+
# only save if the attribute is dirty
|
875
|
+
dirty_atts = options[:dirty_atts]
|
876
|
+
atts_to_save.delete_if { |key, value| !dirty_atts.has_key?(key) }
|
877
|
+
end
|
878
|
+
dom = options[:domain] || domain
|
879
|
+
#puts 'atts_to_save2=' + atts_to_save.inspect
|
880
|
+
connection.put_attributes(dom, id, atts_to_save, :replace, options)
|
881
|
+
apres_save2
|
882
|
+
@attributes
|
883
|
+
end
|
884
|
+
|
885
|
+
def pre_save2
|
886
|
+
@attributes = uniq_values(@attributes)
|
887
|
+
prepare_for_update
|
888
|
+
end
|
889
|
+
|
890
|
+
def apres_save2
|
891
|
+
mark_as_old
|
892
|
+
end
|
893
|
+
|
894
|
+
# Replaces the attributes at SDB by the given values.
|
895
|
+
# +Attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
|
896
|
+
# The other in-memory attributes are not being saved.
|
897
|
+
# Returns a hash of stored attributes.
|
898
|
+
#
|
899
|
+
# see +save+ method
|
900
|
+
def save_attributes(attrs)
|
901
|
+
prepare_for_update
|
902
|
+
attrs = uniq_values(attrs)
|
903
|
+
# if 'id' is present in attrs hash then replace internal 'id' attribute
|
904
|
+
unless attrs['id'].blank?
|
905
|
+
@attributes['id'] = attrs['id']
|
906
|
+
else
|
907
|
+
attrs['id'] = id
|
908
|
+
end
|
909
|
+
connection.put_attributes(domain, id, attrs, :replace) unless attrs.blank?
|
910
|
+
attrs.each { |attribute, values| attrs[attribute] = values }
|
911
|
+
mark_as_old
|
912
|
+
attrs
|
913
|
+
end
|
914
|
+
|
915
|
+
# Remove specified values from corresponding attributes.
|
916
|
+
# +attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
|
917
|
+
#
|
918
|
+
# sandy = Client.find_by_name 'Sandy'
|
919
|
+
# sandy.reload
|
920
|
+
# puts sandy.inspect #=> #<Client:0xb77b48fc @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"]}>
|
921
|
+
# puts sandy.delete_values('toys' => 'patchwork') #=> { 'toys' => ['patchwork'] }
|
922
|
+
# puts sandy.inspect #=> #<Client:0xb77b48fc @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids"]}>
|
923
|
+
#
|
924
|
+
def delete_values(attrs)
|
925
|
+
raise_on_id_absence
|
926
|
+
attrs = uniq_values(attrs)
|
927
|
+
attrs.delete('id')
|
928
|
+
unless attrs.blank?
|
929
|
+
connection.delete_attributes(domain, id, attrs)
|
930
|
+
attrs.each do |attribute, values|
|
931
|
+
# remove the values from the attribute
|
932
|
+
if @attributes[attribute]
|
933
|
+
@attributes[attribute] -= values
|
934
|
+
else
|
935
|
+
# if the attribute is unknown remove it from a resulting list of fixed attributes
|
936
|
+
attrs.delete(attribute)
|
937
|
+
end
|
938
|
+
end
|
939
|
+
end
|
940
|
+
attrs
|
941
|
+
end
|
942
|
+
|
943
|
+
# Removes specified attributes from the item.
|
944
|
+
# +attrs_list+ is an array or comma separated list of attributes names.
|
945
|
+
# Returns the list of deleted attributes.
|
946
|
+
#
|
947
|
+
# sandy = Client.find_by_name 'Sandy'
|
948
|
+
# sandy.reload
|
949
|
+
# puts sandy.inspect #=> #<Client:0xb7761d28 @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"}>
|
950
|
+
# puts sandy.delete_attributes('toys') #=> ['toys']
|
951
|
+
# puts sandy.inspect #=> #<Client:0xb7761d28 @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7"}>
|
952
|
+
#
|
953
|
+
def delete_attributes(*attrs_list)
|
954
|
+
raise_on_id_absence
|
955
|
+
attrs_list = attrs_list.flatten.map { |attribute| attribute.to_s }
|
956
|
+
attrs_list.delete('id')
|
957
|
+
unless attrs_list.blank?
|
958
|
+
connection.delete_attributes(domain, id, attrs_list)
|
959
|
+
attrs_list.each { |attribute| @attributes.delete(attribute) }
|
960
|
+
end
|
961
|
+
attrs_list
|
962
|
+
end
|
963
|
+
|
964
|
+
# Delete the Item entirely from SDB.
|
965
|
+
#
|
966
|
+
# sandy = Client.find_by_name 'Sandy'
|
967
|
+
# sandy.reload
|
968
|
+
# sandy.inspect #=> #<Client:0xb7761d28 @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"}>
|
969
|
+
# puts sandy.delete
|
970
|
+
# sandy.reload
|
971
|
+
# puts sandy.inspect #=> #<Client:0xb7761d28 @attributes={}, @new_record=false>
|
972
|
+
#
|
973
|
+
def delete(options={})
|
974
|
+
raise_on_id_absence
|
975
|
+
connection.delete_attributes(options[:domain] || domain, id)
|
976
|
+
end
|
977
|
+
|
978
|
+
# Returns true if this object hasn�t been saved yet.
|
979
|
+
def new_record?
|
980
|
+
@new_record
|
981
|
+
end
|
982
|
+
|
983
|
+
def mark_as_old # :nodoc:
|
984
|
+
@new_record = false
|
985
|
+
end
|
986
|
+
|
987
|
+
private
|
988
|
+
|
989
|
+
def raise_on_id_absence
|
990
|
+
raise ActiveSdbError.new('Unknown record id') unless id
|
991
|
+
end
|
992
|
+
|
993
|
+
def prepare_for_update
|
994
|
+
@attributes['id'] = self.class.generate_id if @attributes['id'].blank?
|
995
|
+
end
|
996
|
+
|
997
|
+
def uniq_values(attributes=nil) # :nodoc:
|
998
|
+
attrs = {}
|
999
|
+
attributes.each do |attribute, values|
|
1000
|
+
attribute = attribute.to_s
|
1001
|
+
newval = attribute == 'id' ? values.to_s : values.is_a?(Array) ? values.uniq : [values]
|
1002
|
+
attrs[attribute] = newval
|
1003
|
+
if newval.blank?
|
1001
1004
|
# puts "VALUE IS BLANK " + attribute.to_s + " val=" + values.inspect
|
1002
|
-
|
1003
|
-
|
1004
|
-
end
|
1005
|
-
attrs
|
1006
|
-
end
|
1007
|
-
|
1005
|
+
attrs.delete(attribute)
|
1006
|
+
end
|
1008
1007
|
end
|
1008
|
+
attrs
|
1009
|
+
end
|
1010
|
+
|
1009
1011
|
end
|
1012
|
+
end
|
1010
1013
|
end
|