redis_app_join 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/README.md +34 -10
- data/lib/redis_app_join.rb +68 -17
- data/lib/redis_app_join/version.rb +1 -1
- data/redis_app_join.gemspec +3 -0
- metadata +30 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eb2846a158c52758eb48351fa5ef8c2fbffdb56f
|
4
|
+
data.tar.gz: 0d1501faf05b6eb7cf0e327e39dd9c4e58347a9d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a95534d284153f796aa7a93d3697c3aa4285e56b7cdcf2d44e4f9bed0dce5c3b7bd7fd8d393f1f0b253e171cf4816a14d2e451e1e483a3d283bd7ffd7ae68eb9
|
7
|
+
data.tar.gz: 7b38be8d008bf4b543c13bd53913519cc309e163ac2a6e3a321ce5caa0f626aebee5bfef7ba28c6a843fdcd5ecb727a5894ed69181746bb065b60e727c3efa0e
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# RedisAppJoin
|
2
2
|
|
3
|
-
Sometimes
|
3
|
+
Sometimes you need to implement application level joins. It is easy to query User table and get list of user_ids and then query the child record table for records that belong to those users. But what if you need to combine data attributes from both tables? This can also be a use case when querying mutliple databases or 3rd party APIs.
|
4
4
|
|
5
5
|
You can use Redis Hashes as a place to cache data needed as you are looping through records. Warning - this is ALPHA quality software, be careful before running it in production.
|
6
6
|
|
@@ -22,14 +22,14 @@ Or install it yourself as:
|
|
22
22
|
|
23
23
|
## Usage
|
24
24
|
|
25
|
-
Create config/initializers/redis_app_join.rb or place this in environment specific config file. You can use a different namespace, DB, driver, etc.
|
25
|
+
Create `config/initializers/redis_app_join.rb` or place this in environment specific config file. You can use a different namespace, DB, driver, etc.
|
26
26
|
|
27
27
|
```ruby
|
28
28
|
redis_conn = Redis.new(host: 'localhost', port: 6379, db: 0)
|
29
29
|
REDIS_APP_JOIN = Redis::Namespace.new(:appjoin, redis: redis_conn)
|
30
30
|
```
|
31
31
|
|
32
|
-
In the Ruby class where you need to implement application-side join add `include RedisAppJoin`. Here is a sample report generator that will produce a report of comments created since yesterday and include associated article title and name of user who wrote the article.
|
32
|
+
In the Ruby class where you need to implement application-side join add `include RedisAppJoin`. Here is a sample report generator that will query DB to produce a report of comments created since yesterday and include associated article title and name of user who wrote the article.
|
33
33
|
|
34
34
|
```ruby
|
35
35
|
class ReportGen
|
@@ -39,7 +39,7 @@ class ReportGen
|
|
39
39
|
cache_records(records: comments)
|
40
40
|
comment_ids = comments.pluck(:id)
|
41
41
|
# =>
|
42
|
-
# =>
|
42
|
+
# => you also could have done comments.pluck(:article_id)
|
43
43
|
article_ids = fetch_records_field(record_class: 'Comment', record_ids: comment_ids, field: 'article_id')
|
44
44
|
articles = Article.in(id: article_ids).only(:title, :user_id)
|
45
45
|
cache_records(records: articles)
|
@@ -47,7 +47,7 @@ class ReportGen
|
|
47
47
|
user_ids = fetch_records_field(record_class: 'Article', record_ids: article_ids, field: 'user_id')
|
48
48
|
users = User.in(id: user_ids).only(:name)
|
49
49
|
cache_records(records: users)
|
50
|
-
# => instead of using cached comments
|
50
|
+
# => instead of using cached comments you could query DB again
|
51
51
|
cached_comments = fetch_records(record_class: 'Comment', record_ids: comment_ids)
|
52
52
|
cached_comments.each do |comment|
|
53
53
|
article = fetch_records(record_class: 'Article', record_ids: [comment.article_id]).first
|
@@ -85,17 +85,41 @@ Comment, Article and User records will be returned like this.
|
|
85
85
|
|
86
86
|
You can do `article.title` and `user = fetch_records(record_class: 'User', record_ids: [article.user_id]).first`.
|
87
87
|
|
88
|
-
###
|
88
|
+
### Querying 3rd party APIs
|
89
|
+
|
90
|
+
When you query APIs (like [GitHub](https://api.github.com/users/dmitrypol)) you get back JSON. You might want to correlate this with data from different APIs or internal DBs. You can cache it in Redis while you are running the process and persist only what you need. Since these records are not ActiveModels you need to specify the `record_class` which will be part of the Redis key to ensure uniqueness.
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
class DataDownloader
|
94
|
+
include RedisAppJoin
|
95
|
+
def perform
|
96
|
+
profiles = User.where(...).only(:profile).pluck(:profile)
|
97
|
+
profiles.each do |p|
|
98
|
+
url = "https://api.github.com/users/#{p}"
|
99
|
+
data = HTTP.get(url).slice(:name, :bio, :location)
|
100
|
+
cache_records(records: data, record_class: 'Github')
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
# => here is my profile https://api.github.com/users/dmitrypol
|
105
|
+
{"db":0,"key":"appjoin:Github:210308","ttl":-1,"type":"hash","value":{"name":"...","bio":"...","location":"..."}}
|
106
|
+
```
|
107
|
+
|
108
|
+
When you delete such records you need to `delete_records(records: profiles, record_class: 'Github')`.
|
89
109
|
|
90
|
-
|
110
|
+
### Other config options
|
91
111
|
|
92
|
-
|
112
|
+
If you do not call `delete_records` after you are done all data cached in Redis will expire in 1 week. Set `REDIS_APP_JOIN_TTL = 1.day` to modify this behavior. Or set `REDIS_APP_JOIN_TTL = -1` to not expire records.
|
113
|
+
|
114
|
+
The gem uses [Redis pipelining](http://redis.io/topics/pipelining) in default batches of 100. To change that set `REDIS_APP_JOIN_BATCH = 1000` in your initializer.
|
115
|
+
|
116
|
+
### TODO:
|
93
117
|
|
94
|
-
|
118
|
+
more tests, integrate with CI tool
|
95
119
|
|
96
120
|
Support non-string fields. For example, if your DB supports array fields you cannot store those attributes in Redis hash values.
|
97
121
|
|
98
|
-
Methods to fetch associated records so
|
122
|
+
Methods to fetch associated records so you can do `article.user.name` from Redis cache.
|
99
123
|
|
100
124
|
## Development
|
101
125
|
|
data/lib/redis_app_join.rb
CHANGED
@@ -1,45 +1,66 @@
|
|
1
1
|
require "redis_app_join/version"
|
2
|
+
require "redis"
|
3
|
+
require "redis-namespace"
|
4
|
+
require "readthis"
|
5
|
+
#require "active_support/concern"
|
2
6
|
|
3
7
|
module RedisAppJoin
|
4
8
|
|
9
|
+
# => default of 1 week
|
10
|
+
REDIS_APP_JOIN_TTL = 60*60*24*7
|
11
|
+
# => default batch size for Redis pipelining when caching records
|
12
|
+
REDIS_APP_JOIN_BATCH = 100
|
13
|
+
|
5
14
|
# will loop through records creating keys using combination of class and ID.
|
6
|
-
# can combine different record types (Users and Articles) in the same method call
|
15
|
+
# can combine different record types (Users and Articles) in the same method call unless passing hashes
|
7
16
|
# record's attributes will be hash fields
|
8
17
|
#
|
9
18
|
# @see https://github.com/dmitrypol/redis_app_join
|
10
19
|
# @param records [Array] ActiveModels to cache
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
REDIS_APP_JOIN.
|
20
|
+
# @param record_class [String] name of class, used when records are NOT ActiveModel
|
21
|
+
# @raise [RuntimeError] if record is missing ID
|
22
|
+
def cache_records(records:, record_class: nil)
|
23
|
+
records.each_slice(REDIS_APP_JOIN_BATCH) do |batch|
|
24
|
+
REDIS_APP_JOIN.pipelined do
|
25
|
+
batch.each do |record|
|
26
|
+
key = get_key_for_record(record: record, record_class: record_class)
|
27
|
+
if record.is_a?(Hash)
|
28
|
+
data = record
|
29
|
+
else
|
30
|
+
data = record.attributes
|
31
|
+
end
|
32
|
+
REDIS_APP_JOIN.mapped_hmset(key, data.except(:_id, :id))
|
33
|
+
REDIS_APP_JOIN.expire(key, REDIS_APP_JOIN_TTL) unless REDIS_APP_JOIN_TTL == -1
|
34
|
+
end
|
35
|
+
end
|
16
36
|
end
|
17
37
|
end
|
18
38
|
|
19
39
|
# used to delete cached records after the process is done
|
20
|
-
# can combine different record types (Users and Articles) in the same method call
|
40
|
+
# can combine different record types (Users and Articles) in the same method call unless passing hashes
|
21
41
|
#
|
22
42
|
# @param records [Array] ActiveModels to delete
|
23
|
-
|
43
|
+
# @param record_class [String] name of class, used when records are NOT ActiveModel
|
44
|
+
def delete_records(records:, record_class: nil)
|
24
45
|
records.each do |record|
|
25
|
-
key =
|
46
|
+
key = get_key_for_record(record: record, record_class: record_class)
|
26
47
|
REDIS_APP_JOIN.del(key)
|
27
48
|
end
|
28
49
|
end
|
29
50
|
|
30
|
-
# fetch
|
51
|
+
# fetch records from cache,
|
31
52
|
# cannot combine different record types (Users and Articles) in the same method call
|
32
53
|
#
|
33
|
-
# @param record_class [String]
|
54
|
+
# @param record_class [String] name of class, used in lookup
|
34
55
|
# @param record_ids [Array] array of IDs to lookup
|
35
56
|
# @return [Array] array of objects and include the original record ID as one of the attributes for each object.
|
36
57
|
def fetch_records(record_class:, record_ids:)
|
37
58
|
output = []
|
38
59
|
record_ids.each do |record_id|
|
39
|
-
key =
|
60
|
+
key = get_key_for_record_id(record_id: record_id, record_class: record_class)
|
40
61
|
data = REDIS_APP_JOIN.hgetall(key)
|
41
|
-
# => add the key as ID attribute
|
42
|
-
output << OpenStruct.new(data.merge(id: record_id.to_s))
|
62
|
+
# => add the key as ID attribute if there is data hash
|
63
|
+
output << OpenStruct.new(data.merge(id: record_id.to_s)) if data.size > 0
|
43
64
|
end
|
44
65
|
return output
|
45
66
|
end
|
@@ -48,18 +69,48 @@ module RedisAppJoin
|
|
48
69
|
# only returns the field if it's present
|
49
70
|
# cannot combine different record types in the same method call
|
50
71
|
#
|
51
|
-
# @param record_class [String]
|
72
|
+
# @param record_class [String] name of class, used in lookup
|
52
73
|
# @param record_ids [Array] array of IDs to lookup
|
53
74
|
# @param field [String] name of field/attribute to retrieve
|
54
75
|
# @return [Array] array of unique strings
|
55
76
|
def fetch_records_field(record_class:, record_ids:, field:)
|
56
77
|
output = []
|
57
78
|
record_ids.each do |record_id|
|
58
|
-
key =
|
79
|
+
key = get_key_for_record_id(record_id: record_id, record_class: record_class)
|
59
80
|
data = REDIS_APP_JOIN.hget(key, field)
|
60
|
-
output << data
|
81
|
+
output << data if data # checks if nil
|
61
82
|
end
|
62
83
|
return output.uniq
|
63
84
|
end
|
64
85
|
|
86
|
+
private
|
87
|
+
|
88
|
+
# creates a key for specific record id and class
|
89
|
+
#
|
90
|
+
# @param record [Object]
|
91
|
+
# @param record_class [String]
|
92
|
+
# @return [String]
|
93
|
+
def get_key_for_record (record:, record_class:)
|
94
|
+
if record.is_a?(Hash)
|
95
|
+
record_id = record[:id] || record[:_id]
|
96
|
+
else
|
97
|
+
record_id = record.id
|
98
|
+
end
|
99
|
+
raise RuntimeError, 'missing record_id' if record_id == nil
|
100
|
+
record_class ||= record.class.name
|
101
|
+
raise RuntimeError, 'missing record_class' if ['', nil, 'Hash'].include?(record_class)
|
102
|
+
key = [record_class, record_id.to_s].join(':')
|
103
|
+
return key
|
104
|
+
end
|
105
|
+
|
106
|
+
# @param record_id [String]
|
107
|
+
# @param record_class [String]
|
108
|
+
# @return [String]
|
109
|
+
def get_key_for_record_id (record_id:, record_class:)
|
110
|
+
raise RuntimeError, 'missing record_id' if record_id == nil
|
111
|
+
raise RuntimeError, 'missing record_class' if ['', nil, 'Hash'].include?(record_class)
|
112
|
+
key = [record_class, record_id.to_s].join(':')
|
113
|
+
return key
|
114
|
+
end
|
115
|
+
|
65
116
|
end
|
data/redis_app_join.gemspec
CHANGED
@@ -32,7 +32,10 @@ Gem::Specification.new do |spec|
|
|
32
32
|
spec.add_development_dependency "rspec", "~> 3.0"
|
33
33
|
# =>
|
34
34
|
spec.add_development_dependency 'mock_redis', '~> 0.17', '>= 0.17.0'
|
35
|
+
spec.add_development_dependency 'rspec-activemodel-mocks', '>=1.0.3'
|
36
|
+
spec.add_development_dependency 'byebug', '>=9.0.6'
|
35
37
|
# =>
|
38
|
+
#spec.add_runtime_dependency 'activesupport', '~> 4.2', '>= 4.2.7'
|
36
39
|
spec.add_runtime_dependency 'redis', '~> 3.3'
|
37
40
|
spec.add_runtime_dependency 'redis-namespace', '~> 1.5'
|
38
41
|
spec.add_runtime_dependency 'readthis', '~> 1.3', '>= 1.3.0'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis_app_join
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dmitry Polyakovsky
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-10-
|
11
|
+
date: 2016-10-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -72,6 +72,34 @@ dependencies:
|
|
72
72
|
- - ">="
|
73
73
|
- !ruby/object:Gem::Version
|
74
74
|
version: 0.17.0
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: rspec-activemodel-mocks
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: 1.0.3
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: 1.0.3
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: byebug
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: 9.0.6
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: 9.0.6
|
75
103
|
- !ruby/object:Gem::Dependency
|
76
104
|
name: redis
|
77
105
|
requirement: !ruby/object:Gem::Requirement
|