redis_storage_methods 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +70 -0
- data/Rakefile +2 -0
- data/lib/redis_storage_methods/version.rb +3 -0
- data/lib/redis_storage_methods.rb +228 -0
- data/redis_storage_methods.gemspec +17 -0
- metadata +55 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Jordan Prince
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# RedisStorageMethods
|
2
|
+
|
3
|
+
RedisStorageMethods allows you to include a MixIn to your models that will let you easily create copies in a redis store when you create actual objects with no extra logic, and expose methods that enable you to find in redis store, not in the database, to protect yourself from the massive time costs in querying with SQL compared to Redis.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'redis_storage_methods'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install redis_storage_methods
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
Pretty easy here, just do:
|
22
|
+
|
23
|
+
class Battle < ActiveRecord::Base
|
24
|
+
include RedisStorageMethods
|
25
|
+
...
|
26
|
+
end
|
27
|
+
|
28
|
+
Once you've done this, you'll need to add a few methods to Battle that the Mixin requires to be defined(even empty methods will do).
|
29
|
+
|
30
|
+
#accepts the part of the form hash with nested atributes(but with attributes taken off keys), like { "videos" => {"0" => {field => value}}}
|
31
|
+
#and then creates models. Because we don't support nested creation at the meta level, must be handled locally.
|
32
|
+
def create_associations_from_params(params)
|
33
|
+
end
|
34
|
+
|
35
|
+
#No nesting in Redis, no methods for listing associations on DataMapper/AR, This is custom, returns nested objects for use in storage
|
36
|
+
#with redis. Because I dont want to write metacode that detects nested objects within my own nested objects, I'm just leaving it
|
37
|
+
# to each model to implement this in a custom fashion.
|
38
|
+
def add_key_value_pairs_of_associations_for_redis!(array)
|
39
|
+
end
|
40
|
+
|
41
|
+
#expects to get hash of things like video0video_id => "555" from redis, needs to create associations from it.
|
42
|
+
def populate_associations_from_redis(redis_hash)
|
43
|
+
end
|
44
|
+
|
45
|
+
#if you have sets stored outside the redis hash, need to make a call to get them, haven't you?
|
46
|
+
# you can do that here, or leave this a nil method. It won't hurt you.
|
47
|
+
def populate_custom_fields_from_extraneous_redis_calls
|
48
|
+
end
|
49
|
+
|
50
|
+
#method to sync object in db with redis.
|
51
|
+
def sync_with_redis
|
52
|
+
end
|
53
|
+
|
54
|
+
#create the custom fields you need like lists for arrays etc, artist:name => id references,
|
55
|
+
#stuff like that.
|
56
|
+
def create_custom_redis_fields
|
57
|
+
end
|
58
|
+
|
59
|
+
def after_redis_create_do
|
60
|
+
end
|
61
|
+
|
62
|
+
Copy and paste these bad boys into the Battle class. You should be aware that nested associations are not stored in redis by default, most of these methods involve implementing them to allow for nesting.
|
63
|
+
|
64
|
+
## Contributing
|
65
|
+
|
66
|
+
1. Fork it
|
67
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
68
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
69
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
70
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,228 @@
|
|
1
|
+
#This module is for Toplevel Objects that are intended for storage in redis, with possible nested subobjects that are not meant to be
|
2
|
+
#stored independently in redis.
|
3
|
+
|
4
|
+
# So a Battle is a Toplevel Object. Video is not - it's always nested in redis, so it's just normal. Video does not include this
|
5
|
+
#module, but Battle would. A Toplevel Object should not reference another Toplevel Object via an association, but if it must, do not
|
6
|
+
#put it in the associations_names method so that this Toplevel object doesnt try to store it. That's what this module uses
|
7
|
+
# to determine associations with.
|
8
|
+
module RedisStorageMethods
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def redis
|
12
|
+
unless @redis
|
13
|
+
@redis ||= Redis.new( :driver => :hiredis,
|
14
|
+
:host => ENV['redis_host'],
|
15
|
+
:port => ENV['redis_port'])
|
16
|
+
@redis.auth(ENV['redis_auth']) if ENV['redis_auth']
|
17
|
+
end
|
18
|
+
|
19
|
+
@tries = 0 unless @tries
|
20
|
+
|
21
|
+
begin
|
22
|
+
@redis.get("test") #tests to see if we're still authed.
|
23
|
+
@tries = 0 #means we can reset our trymeter.
|
24
|
+
rescue Exception => e
|
25
|
+
@tries+=1 if @tries
|
26
|
+
if @tries<5
|
27
|
+
@redis = nil
|
28
|
+
redis #reset it up.
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
return @redis
|
33
|
+
end
|
34
|
+
|
35
|
+
#if an object is passed as params, we know we're only putting this in redis, it already exists in db
|
36
|
+
#if params is a hash, know we're creating in both.
|
37
|
+
def create_with_associations_and_redis(params=nil)
|
38
|
+
params = sanitize_all(params)
|
39
|
+
if params.is_a?(Hash)
|
40
|
+
if params.keys.select{ |k| k if k.to_s.match(/_attributes$/) }.size>0
|
41
|
+
associations = remove_and_return_associations_hash!(params)
|
42
|
+
me = self.create(params)
|
43
|
+
raise "Invalid #{me.class}: #{me.errors[me.errors.keys.first].first}" unless me.valid?
|
44
|
+
me.create_associations_from_params(associations)
|
45
|
+
else
|
46
|
+
me = self.create(params) # if submits as normal form, not AR form.
|
47
|
+
raise "Invalid #{me.class}: #{me.errors[me.errors.keys.first].first}" unless me.valid?
|
48
|
+
end
|
49
|
+
else
|
50
|
+
me = params
|
51
|
+
end
|
52
|
+
|
53
|
+
redis.pipelined {
|
54
|
+
redis.del("#{me.class.to_s.downcase}:#{me.id}") #overwrite it, make sure.
|
55
|
+
redis.hmset("#{me.class.to_s.downcase}:#{me.id}", me.assemble_key_value_array_for_redis)
|
56
|
+
redis.sadd("#{me.class.to_s.downcase.pluralize}", me.id)
|
57
|
+
me.create_custom_redis_fields
|
58
|
+
}
|
59
|
+
|
60
|
+
me.after_redis_create_do
|
61
|
+
return me
|
62
|
+
end
|
63
|
+
|
64
|
+
def sanitize_all(params)
|
65
|
+
#below sanitizes all top level inputs
|
66
|
+
return nil unless params
|
67
|
+
to_ret = nil
|
68
|
+
if params.is_a?(Hash)
|
69
|
+
to_ret = params.inject(Hash.new){ |hash, entry| hash[entry[0]] = entry[1].is_a?(String) ? Sanitize.clean(entry[1]) : sanitize_all(entry[1]); hash}
|
70
|
+
elsif params.is_a?(Array)
|
71
|
+
to_ret = params.inject(Array.new){ |arr, entry| arr << (entry.is_a?(String) ? Sanitize.clean(entry) : sanitize_all(entry)); arr}
|
72
|
+
end
|
73
|
+
return to_ret
|
74
|
+
end
|
75
|
+
|
76
|
+
#form submits with something like { "fields" => keys... "video_attributes" => { "0" => { "fields" => keys}}}
|
77
|
+
#but DataMapper doesnt support nested form submission like active record, so we have to remove these
|
78
|
+
#nestings and then handle them appropriately here..We use a class var that the class must set to know what
|
79
|
+
#to pull out.
|
80
|
+
def remove_and_return_associations_hash!(params)
|
81
|
+
associations = Hash.new
|
82
|
+
associations_names.each do |a|
|
83
|
+
associations[a] = params.delete("#{a.singularize}_attributes")
|
84
|
+
end
|
85
|
+
associations
|
86
|
+
end
|
87
|
+
|
88
|
+
#returns all redis stored objects
|
89
|
+
def all_redis
|
90
|
+
selfs = []
|
91
|
+
redis.smembers(self.to_s.downcase.pluralize).each do |id|
|
92
|
+
selfs << self.find_with_redis(id)
|
93
|
+
end
|
94
|
+
selfs
|
95
|
+
end
|
96
|
+
|
97
|
+
#populates a model with redis hash
|
98
|
+
#note you cannot write with this guy, it won't let you
|
99
|
+
#even though all the fields are set.
|
100
|
+
def find_with_redis(id)
|
101
|
+
me = self.new
|
102
|
+
redis_attr = redis.hgetall("#{me.class.to_s.downcase}:#{id}")
|
103
|
+
return nil unless redis_attr["id"]
|
104
|
+
me.attributes = redis_attr.reject { |field| field.match(/^\w+\d+\w+$/) or field.match(/^\w+_.*_\w+$/)}
|
105
|
+
me.populate_associations_from_redis(redis_attr.select { |field| field.match(/\w+\d\w+/)})
|
106
|
+
me.populate_custom_fields_from_extraneous_redis_calls
|
107
|
+
me
|
108
|
+
end
|
109
|
+
|
110
|
+
#should never get called by client, is method for cronjob to update db.
|
111
|
+
def sync_all_with_redis
|
112
|
+
self.all.each do |o|
|
113
|
+
o.sync_with_redis
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
#emergency method if redis db was lost, repopulate associations and stuff.
|
118
|
+
def put_all_in_redis
|
119
|
+
self.all.each do |a|
|
120
|
+
a.put_in_redis
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
#implementable
|
125
|
+
|
126
|
+
# ["videos", "user", ...] plural if an array, singular if a has_one/belongs_to.
|
127
|
+
def associations_names
|
128
|
+
raise 'Must be implemented in object class'
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.included(base)
|
134
|
+
base.extend ClassMethods
|
135
|
+
end
|
136
|
+
|
137
|
+
#instance methods
|
138
|
+
|
139
|
+
#must also define redis down here so it gets set,
|
140
|
+
#there is an instance on the class object and instances of it then.
|
141
|
+
def redis
|
142
|
+
@redis ||= self.class.redis
|
143
|
+
end
|
144
|
+
|
145
|
+
def assemble_key_value_array_for_redis
|
146
|
+
a = Array.new
|
147
|
+
self.class.properties.map { |prop| prop.name.to_s }.each do |p|
|
148
|
+
if(self.send(p))
|
149
|
+
#this if statement is so nil values wont be stored as "" in redis
|
150
|
+
#and come back as an empty string, they will come back as nil.
|
151
|
+
a<<p
|
152
|
+
a<<self.send(p)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
self.add_key_value_pairs_of_associations_for_redis!(a)
|
156
|
+
a
|
157
|
+
end
|
158
|
+
|
159
|
+
def destroy_with_redis
|
160
|
+
self.destroy
|
161
|
+
redis.del("#{self.class.to_s.downcase}:#{self.id}")
|
162
|
+
end
|
163
|
+
|
164
|
+
#this is a helper method that will construct objects from redis that have no
|
165
|
+
#nested associations themselves. So we can keep code DRY. If you have nesting,
|
166
|
+
#you must do it yourself.
|
167
|
+
def construct_low_level_model_from_redis_hash(redis_hash, association)
|
168
|
+
found = true
|
169
|
+
i = 0
|
170
|
+
while(found) do
|
171
|
+
if redis_hash["#{association.singularize}#{i}id"]
|
172
|
+
params_hash = {}
|
173
|
+
|
174
|
+
redis_hash.select {|k, v| k.to_s.match(/#{association.singularize}#{i}/)}.each do |key, value|
|
175
|
+
params_hash[key.to_s.match(/#{association.singularize}#{i}(\w+)/)[1]] = value
|
176
|
+
end
|
177
|
+
|
178
|
+
self.send(association)<< Kernel.const_get(association.singularize.capitalize).new(params_hash)
|
179
|
+
else
|
180
|
+
found = false and break
|
181
|
+
end
|
182
|
+
i+=1
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
#if the db object exists but needs to be placed in redis
|
187
|
+
def put_in_redis
|
188
|
+
self.class.create_with_associations_and_redis(self)
|
189
|
+
end
|
190
|
+
|
191
|
+
#implementable
|
192
|
+
|
193
|
+
#accepts the part of the form hash with nested atributes(but with attributes taken off keys), like { "videos" => {"0" => {field => value}}}
|
194
|
+
#and then creates models. Because we don't support nested creation at the meta level, must be handled locally.
|
195
|
+
def create_associations_from_params(params)
|
196
|
+
raise 'Must be implemented by the object class!'
|
197
|
+
end
|
198
|
+
|
199
|
+
#No nesting in Redis, no methods for listing associations on DataMapper, This is custom, returns nested objects for use in storage
|
200
|
+
#with redis. Because I dont want to write metacode that detects nested objects within my own nested objects, I'm just leaving it
|
201
|
+
# to each model to implement this in a custom fashion.
|
202
|
+
def add_key_value_pairs_of_associations_for_redis!(array)
|
203
|
+
raise 'Must be implemented by object class!'
|
204
|
+
end
|
205
|
+
|
206
|
+
#expects to get hash of things like video0video_id => "555" from redis, needs to create associations from it.
|
207
|
+
def populate_associations_from_redis(redis_hash)
|
208
|
+
raise 'Must be implemented by object class!'
|
209
|
+
end
|
210
|
+
|
211
|
+
#if you have sets stored outside the redis hash, need to make a call to get them, haven't you?
|
212
|
+
# you can do that here, or leave this a nil method. It won't hurt you.
|
213
|
+
def populate_custom_fields_from_extraneous_redis_calls
|
214
|
+
end
|
215
|
+
|
216
|
+
#method to sync object in db with redis.
|
217
|
+
def sync_with_redis
|
218
|
+
raise 'Must be implemented by object class!'
|
219
|
+
end
|
220
|
+
|
221
|
+
#create the custom fields you need like lists for arrays etc, artist:name => id references,
|
222
|
+
#stuff like that.
|
223
|
+
def create_custom_redis_fields
|
224
|
+
end
|
225
|
+
|
226
|
+
def after_redis_create_do
|
227
|
+
end
|
228
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/redis_storage_methods/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Jordan Prince"]
|
6
|
+
gem.email = ["jordanmprince@gmail.com"]
|
7
|
+
gem.description = %q{Transparently store objects both in redis and your sql store, but GET only on the redis store for speed.}
|
8
|
+
gem.summary = %q{Transparently store objects both in redis and your sql store, but GET only on the redis store for speed.}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "redis_storage_methods"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = RedisStorageMethods::VERSION
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: redis_storage_methods
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jordan Prince
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-02-05 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Transparently store objects both in redis and your sql store, but GET
|
15
|
+
only on the redis store for speed.
|
16
|
+
email:
|
17
|
+
- jordanmprince@gmail.com
|
18
|
+
executables: []
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- .gitignore
|
23
|
+
- Gemfile
|
24
|
+
- LICENSE
|
25
|
+
- README.md
|
26
|
+
- Rakefile
|
27
|
+
- lib/redis_storage_methods.rb
|
28
|
+
- lib/redis_storage_methods/version.rb
|
29
|
+
- redis_storage_methods.gemspec
|
30
|
+
homepage: ''
|
31
|
+
licenses: []
|
32
|
+
post_install_message:
|
33
|
+
rdoc_options: []
|
34
|
+
require_paths:
|
35
|
+
- lib
|
36
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ! '>='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
requirements: []
|
49
|
+
rubyforge_project:
|
50
|
+
rubygems_version: 1.8.11
|
51
|
+
signing_key:
|
52
|
+
specification_version: 3
|
53
|
+
summary: Transparently store objects both in redis and your sql store, but GET only
|
54
|
+
on the redis store for speed.
|
55
|
+
test_files: []
|