joffice_redis 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,73 @@
1
+ #encoding: utf-8
2
+ =begin
3
+ Copyright 2010 Denis Kokorin <virkdi@mail.ru>
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+ =end
17
+
18
+
19
+ module JOffice::Redis
20
+ class HashTable
21
+ attr_reader :redis
22
+
23
+ def initialize(id, redis)
24
+ @id, @redis, @_attributes=id, redis,{}
25
+ end
26
+
27
+ def [](key)
28
+ get(key)
29
+ end
30
+
31
+ def get(key)
32
+ @_attributes[key]||=((v=redis.get(hash_key(key))) ? Marshal.load(v) : v)
33
+ end
34
+
35
+ def []=(key, value)
36
+ set(key, value)
37
+ end
38
+
39
+ def set(key, value, expire=nil)
40
+ if value
41
+ @_attributes[key]=value unless expire
42
+ k=hash_key(key)
43
+ redis.set(k, Marshal.dump(value))
44
+ redis.expire(k, expire) if expire
45
+ else
46
+ redis.del(hash_key(key))
47
+ @_attributes.delete(key)
48
+ end
49
+ value
50
+ end
51
+
52
+ def hash_key(key)
53
+ "#{@id}:#{key}".force_encoding('ASCII-8BIT')
54
+ end
55
+
56
+ def exists?(key)
57
+ k=hash_key(key)
58
+ (@_attributes.has_key?(k) || redis.exists?(k))
59
+ end
60
+
61
+ def delete(key)
62
+ redis.del(hash_key(key))
63
+ @_attributes.delete(key)
64
+ end
65
+
66
+ class << self
67
+ def keys(id, redis)
68
+ (r=redis.keys("#{id}:*")) ? r : []
69
+ end
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,65 @@
1
+ #encoding: utf-8
2
+ =begin
3
+ Copyright 2010 Denis Kokorin <virkdi@mail.ru>
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+ =end
17
+
18
+
19
+ require 'msgpack'
20
+
21
+ module JOffice::Redis
22
+ module MarshalValue
23
+
24
+ def unbox_object(value)
25
+ value.blank? ? value : Marshal.load(value)
26
+ end
27
+ module_function :unbox_object
28
+
29
+ def box_object(value)
30
+ value.blank? ? value : Marshal.dump(value)
31
+ end
32
+ module_function :box_object
33
+
34
+ def unbox_string(value)
35
+ value.blank? ? '' : MessagePack.unpack(value).encode2utf8
36
+ end
37
+ module_function :unbox_string
38
+
39
+ def box_string(value)
40
+ (value || '').to_s.to_msgpack
41
+ end
42
+ module_function :box_string
43
+
44
+ def unbox_integer(value)
45
+ value.to_i
46
+ end
47
+ module_function :unbox_integer
48
+
49
+ def box_integer(value)
50
+ value ? value.to_s : '0'
51
+ end
52
+ module_function :box_integer
53
+
54
+ def unbox_boolean(value)
55
+ '1' == value
56
+ end
57
+ module_function :unbox_boolean
58
+
59
+ def box_boolean(value)
60
+ value ? '1' : '0'
61
+ end
62
+ module_function :box_boolean
63
+
64
+ end
65
+ end
@@ -0,0 +1,417 @@
1
+ #encoding: utf-8
2
+ =begin
3
+ Copyright 2010 Denis Kokorin <virkdi@mail.ru>
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+ =end
17
+
18
+
19
+ require 'fileutils'
20
+ require 'digest/sha1'
21
+
22
+ module JOffice::Redis
23
+ class ModelFactory
24
+ attr_reader :parent
25
+ attr_reader :attributes
26
+ attr_reader :attributes_hash
27
+ attr_reader :attribute_lists
28
+ attr_reader :unique_indexes
29
+ attr_accessor :prefix
30
+ attr_reader :logger
31
+ attr_reader :class_module_name
32
+ attr_reader :instance_module_name
33
+
34
+ def initialize(parent, logger)
35
+ @parent=parent
36
+ @logger=logger
37
+ @consts=[]
38
+ @static_methods=[]
39
+ @instance_methods=[]
40
+ @indexes={}
41
+ @composite_indexes={}
42
+ @static_alias=[]
43
+ @index_by_field={}
44
+ @attributes,@attributes_hash,@attribute_lists,@unique_indexes = {},{},{},{}
45
+
46
+ yield self if block_given?
47
+
48
+ #@class_name="CS#{Digest::SHA1.hexdigest(parent.name)}"
49
+ @class_name="CS#{class_table}_#{prefix}"
50
+ @class_module_name="JOffice::Redis::Metadata::#{@class_name}::ClassMethods"
51
+ @instance_module_name="JOffice::Redis::Metadata::#{@class_name}::InstanceMethods"
52
+
53
+ generate
54
+
55
+
56
+ #@buffer=["", "", "module JOffice::Redis","module Metadata","# Metadata for class #{parent.name}", "module #{@class_name}"]
57
+ #@buffer << "module ClassMethods"
58
+ @buffer = ["class << self"]
59
+ @buffer << "include JOffice::Redis::MarshalValue"
60
+ @buffer+=@consts
61
+ @buffer << ""
62
+ @buffer+=@static_methods.map{|v| " #{v}"}
63
+ @buffer<< @static_alias.join("\n")
64
+ @buffer << "end"
65
+
66
+ #@buffer << "module InstanceMethods"
67
+ @buffer << ""
68
+ @buffer+=@instance_methods.map{|v| " #{v}"}
69
+ @buffer << ""
70
+ #@buffer << "end"
71
+ #@buffer << "#{parent.name}.send(:include, InstanceMethods)"
72
+ ####@buffer << "#{parent.name}.send(:extend, JOffice::Redis::CacheModelClassMethods)"
73
+ #@buffer << "#{parent.name}.send(:extend, ClassMethods)"
74
+ #@buffer+=["end","end","end"]
75
+ parent.class_eval @buffer.join("\n")
76
+ #save_and_include
77
+ end
78
+
79
+ def save_and_include
80
+ dir=File.join(File.absolute_path(File.dirname(__FILE__)),'metadata')
81
+ FileUtils.mkdir_p(dir)
82
+
83
+ path=File.join(dir, "#{@class_name}.rb")
84
+ f=File.open(path,'w')
85
+ f.write(@buffer.join("\n"))
86
+ f.close
87
+ #require path
88
+ end
89
+
90
+ def attribute(name, options={})
91
+ unique=options[:unique]
92
+ key_name=(options[:key_name] || name)
93
+ attributes[name.to_sym]=[options[:type] || :object, unique, key_name]
94
+ unique_indexes[name]=key_name if unique
95
+ end
96
+
97
+ def attribute_list(name, options={})
98
+ attribute_lists[name]=options
99
+ end
100
+
101
+ def hash(name, options={})
102
+ attributes_hash[name]=options
103
+ end
104
+
105
+ def index(name, field)
106
+ (@indexes[name]||=[]) << field
107
+ @index_by_field[field]=name
108
+ end
109
+
110
+ def composite_index(name, *fields)
111
+ options=fields.extract_options!
112
+ @composite_indexes[name]=[fields.flatten, options]
113
+ end
114
+
115
+ def set_table_name(name)
116
+ @class_table=name
117
+ end
118
+
119
+ private
120
+ def generate_unbox_value(value_name, type)
121
+ case type
122
+ when :boolean then "('1'==#{value_name})"
123
+ when :integer then "#{value_name}.to_i"
124
+ when :string then "JOffice::Redis::MarshalValue.unbox_string(#{value_name})"
125
+ else "JOffice::Redis::MarshalValue.unbox_object(#{value_name})"
126
+ end
127
+ end
128
+
129
+ def const(name, value)
130
+ @consts << "#{name}=#{value}"
131
+ end
132
+
133
+ def method(name, args=nil, static=false, log=true, block=false)
134
+ buf=[]
135
+ buf << ("JOffice::Redis::CacheModel.logger.info('Redis[#{class_table}]'){'#{name}('+[#{args}].flatten.inspect+')'}") if logger && log
136
+ yield buf
137
+ args="#{args}, &block" if block
138
+ buf=buf.map{|v| " #{v}"}.join("\n")
139
+ signature=(args ? "#{name}(#{args})" : name)
140
+ buf=["def #{signature}\n#{buf}","end"]
141
+ buf << "public :#{name}" if static
142
+ static ? (@static_methods << buf.join("\n")) : (@instance_methods << buf.join("\n"))
143
+ end
144
+
145
+ def class_table
146
+ @class_table||=@parent.name
147
+ end
148
+
149
+ def key_id_set
150
+ ":'"+((prefix ? "#{prefix}::" : '') +"id").gsub(/ /,'%20')+"'"
151
+ end
152
+
153
+ def define_db
154
+ method('db', nil, true, false) do |m|
155
+ m << "@@db||=JOffice::RedisManager.instance.open_db_em('#{class_table}')"
156
+ end
157
+
158
+ method('db=', 'value', true, false) do |m|
159
+ m << "@@db=value"
160
+ end
161
+
162
+ method('db', nil, false, false) do |m|
163
+ m << "self.class.db"
164
+ end
165
+ end
166
+
167
+ def generate_key
168
+ const("ALL_MODEL_ATTRIBUTES_KEYS",attributes.keys.inspect)
169
+ const("PREFIX", ":'#{prefix}'") unless prefix.blank?
170
+
171
+ method('all_attributes',nil, true, false) do |m|
172
+ m << "#{@class_module_name}::ALL_MODEL_ATTRIBUTES_KEYS"
173
+ end
174
+
175
+ method('all_attributes', nil, false, false) do |m|
176
+ m << "#{@class_module_name}::ALL_MODEL_ATTRIBUTES_KEYS"
177
+ end
178
+
179
+ method('key_id_set',nil, true, false) do |m|
180
+ m << ":'"+(prefix ? "#{prefix}::" : '') +"id'"
181
+ end
182
+
183
+ method('prefix',nil, true, false) do |m|
184
+ m << 'PREFIX'
185
+ end unless prefix.blank?
186
+
187
+ method('prefix', nil, false, false) do |m|
188
+ m << 'PREFIX'
189
+ end if prefix && !prefix.empty?
190
+
191
+ method('key','*args', true, false) do |m|
192
+ m << "args.#{prefix ? 'unshift(prefix).' : ''}join(':').gsub(/ /,'%20').force_encoding('ASCII-8BIT')"
193
+ end
194
+
195
+ method('keys_cache', nil, false, false) do |m|
196
+ m << "@keys_cache||={}"
197
+ end
198
+
199
+ method('key','*args', false, false) do |m|
200
+ if prefix
201
+ s='([prefix, id]+args)'
202
+ else
203
+ s='args.unshift(id)'
204
+ end
205
+ m << s+".join(':').gsub(/ /,'%20').force_encoding('ASCII-8BIT')"
206
+ end
207
+
208
+ if prefix
209
+ build_key_by_id_name='"'+prefix+':#{id}:#{name}"'
210
+ else
211
+ build_key_by_id_name='"#{id}:#{name}"'
212
+ end
213
+ build_key_by_id_name+=".gsub(/ /,'%20').force_encoding('ASCII-8BIT')"
214
+
215
+ method "key_name", "id, name", true, false do |m|
216
+ m << build_key_by_id_name
217
+ end
218
+
219
+ method "key_name", "name", false do |m|
220
+ m << "keys_cache[name]||=" + build_key_by_id_name
221
+ end
222
+
223
+ method "key2id", "key", true, false do |m|
224
+ m << (prefix ? "key[#{prefix.length+1}..key.length] if key" : "key")
225
+ end
226
+
227
+ end
228
+
229
+ def generate_attributes
230
+ attributes.each do |name, options|
231
+ unique_key_name=options[2]
232
+
233
+ method name do |m|
234
+ m << "@#{name}||="+generate_unbox_value("db.get(key_name(:#{name}))", options[0])
235
+ end
236
+
237
+ method "#{name}=","value" do |m|
238
+ m << "db.set(key_name(:#{name}),JOffice::Redis::MarshalValue.box_#{options[0]}(@#{name}=value))"
239
+ m << "db.set(self.class.key_name(:#{unique_key_name},value), id)" if options[1]
240
+ m << "db.set_add(self.class.key_name(:#{@index_by_field[name]}, value), id)" if @index_by_field.has_key?(name)
241
+ end
242
+
243
+ @static_alias << "alias :unbox_value_#{name} :unbox_#{options[0]}"
244
+
245
+ if options[1]
246
+ method "find_by_#{unique_key_name}","value, select=[]", true do |m|
247
+ m << "id=db.get(key_name(:#{unique_key_name}, value))"
248
+ if unique_key_name==name
249
+ m << "id ? new(:id=>id, :#{name}=>value).ensure_attributes(select) : nil"
250
+ else
251
+ m << "id ? new(:id=>id).ensure_attributes(select) : nil"
252
+ end
253
+ end
254
+ end
255
+
256
+ if @index_by_field.has_key?(name)
257
+ index_name=@index_by_field[name]
258
+ method "find_all_by_#{index_name}", "value, select=[]", true do |m|
259
+ m << "ids=db.set_members(key_name(:#{index_name}, value))"
260
+ m << "ids ? load_by_ids(ids, select) : []"
261
+ end
262
+
263
+ method "find_all_id_by_#{name}", "value", true do |m|
264
+ m << "db.set_members(key_name(:#{index_name}, value))"
265
+ end
266
+
267
+ method "count_by_#{name}", "value", true do |m|
268
+ m << "db.scard(key_name(:#{index_name}, value))"
269
+ end
270
+
271
+ end
272
+ end
273
+ end
274
+
275
+ def generate_hash_attributes
276
+ attributes_hash.each do |name, options|
277
+ method name do |m|
278
+ m << "@#{name}||=JOffice::Redis::HashTable.new(key_name(:'#{name}'), db)"
279
+ end
280
+ end
281
+ end
282
+
283
+ def generate_save_composite_index
284
+ method "save_composite_index" do |m|
285
+ @composite_indexes.each do |index_name, d|
286
+ fields,options=*d
287
+ if options[:unique]
288
+ m << "db.set(self.class.key(:#{index_name}, #{fields.join(',')}), id)"
289
+ else
290
+ m << "db.set_add(self.class.key(:#{index_name}, #{fields.join(',')}), id)"
291
+ end
292
+ end
293
+ end
294
+
295
+ @composite_indexes.each do |index_name, d|
296
+ fields,options=*d
297
+ fields_map=fields.map{|v| ":#{v}=>#{v}"}.join(',')
298
+ fields_sym_array=":#{fields.join(',:')}"
299
+ if options[:unique]
300
+ method "find_by_#{fields.join('_and_')}","#{fields.join(',')}, select=[]", true do |m|
301
+ m << "id=db.get(key(:#{index_name}, #{fields.join(',')}))"
302
+ m << "select=select.map{|v| v.to_sym}-[#{fields_sym_array}] if select && !select.empty?"
303
+ m << "id ? new({:id=>id, #{fields_map}}).ensure_attributes(select) : nil"
304
+ end
305
+
306
+ method "exists_by_#{fields.join('_and_')}?", fields.join(','), true do |m|
307
+ m << "db.exists(key(:#{index_name}, #{fields.join(',')}))"
308
+ end
309
+ else
310
+ method "find_all_by_#{fields.join('_and_')}", "#{fields.join(',')}, select=[]", true, true, true do |m|
311
+ m << "find_by_batch_set(key(:#{index_name}, #{fields.join(',')}), {:select=>select,:default=>{#{fields_map}}}, &block)"
312
+ end
313
+
314
+ method "exists_by_#{fields.join('_and_')}?", fields.join(','), true do |m|
315
+ m << "db.exists(key(:#{index_name}, #{fields.join(',')}))"
316
+ end
317
+
318
+ method "count_by_#{fields.join('_and_')}", fields.join(','), true do |m|
319
+ m << "db.scard(key(:#{index_name}, #{fields.join(',')}))"
320
+ end
321
+ end
322
+ end
323
+ end
324
+
325
+ def generate_clone_method
326
+ method "clone", "v, id=nil", true do |m|
327
+ m << "item=new(:id=>(id ? id : v.id))"
328
+ m << "db.bulk_set do"
329
+ attributes.each do |name, options|
330
+ m << "item.#{name}=v.#{name}"
331
+ end
332
+ m << ""
333
+ m << "item.save_custom" if parent.instance_methods.index('save_custom')
334
+ m << "item.save"
335
+ m << "db.set_add(#{key_id_set}, item.id)"
336
+ m << "end"
337
+ m << "item"
338
+ end
339
+ end
340
+
341
+ def generate_exists
342
+
343
+ method 'exists?','id', true do |m|
344
+ m << "db.sismember(#{key_id_set}, id) if id"
345
+ end
346
+
347
+ method "exists?" do |m|
348
+ m << "self.class.exists?(id)"
349
+ end
350
+ end
351
+
352
+ def generate_delete_method
353
+
354
+ method "delete" do |m|
355
+ m << "return unless exists?"
356
+ m << "key_to_delete=#{@class_module_name}::ALL_MODEL_ATTRIBUTES_KEYS.map{|name| key_name( name) }"
357
+ keys_to_ensure=[]
358
+ keys_to_ensure+=@indexes.values if @indexes.size>0
359
+ keys_to_ensure+=@composite_indexes.values.map{|v| v[0]} if @composite_indexes.size>0
360
+ m << "ensure_attributes(#{keys_to_ensure.flatten.uniq.map{|v| v.to_sym}.inspect})" unless keys_to_ensure.empty?
361
+
362
+ @composite_indexes.each do |index_name, d|
363
+ fields,options=*d
364
+ if options[:unique]
365
+ m << "key_to_delete << self.class.key(:#{index_name}, #{fields.join(',')})"
366
+ else
367
+ m << "db.set_remove_if_empty(self.class.key(:#{index_name}, #{fields.join(',')}), id)"
368
+ end
369
+ end
370
+
371
+ if unique_indexes.size>0
372
+ unique_indexes.each do |name, index|
373
+ m << "key_to_delete << self.class.key_name(:#{index}, #{name})"
374
+ end
375
+ end
376
+ if attributes_hash.size>0
377
+ m << "key_to_delete+=(db.keys(key_name('*')) || [])"
378
+ end
379
+ m << "key_to_delete+=delete_custom" if parent.instance_methods.index('delete_custom')
380
+
381
+ @indexes.each do |index, fields|
382
+ fields.each do |field|
383
+ m << "db.set_remove_if_empty(self.class.key_name(:#{index}, #{field}), id)"
384
+ end
385
+ end
386
+
387
+ m << "db.del(key_to_delete)"
388
+ m << "db.set_remove(#{key_id_set}, id)"
389
+ end
390
+
391
+ method "delete", nil, true do |m|
392
+ m << "new(:id=>id).delete"
393
+ end
394
+
395
+ end
396
+
397
+ def generate_save
398
+ generate_save_composite_index
399
+
400
+ method "save" do |m|
401
+ #m << "db.set_add(#{key_id_set}, id)"
402
+ m << "save_composite_index" if @composite_indexes.size>0
403
+ end
404
+ end
405
+
406
+ def generate
407
+ define_db
408
+ generate_key
409
+ generate_exists
410
+ generate_attributes
411
+ generate_hash_attributes
412
+ generate_clone_method
413
+ generate_delete_method
414
+ generate_save
415
+ end
416
+ end
417
+ end