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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f0254b1ea1caa6281771b11bb2457c7c27e0e1ca
4
- data.tar.gz: f51c7597abee8e79fec7034a9fce12f5f1e1782d
3
+ metadata.gz: eb2846a158c52758eb48351fa5ef8c2fbffdb56f
4
+ data.tar.gz: 0d1501faf05b6eb7cf0e327e39dd9c4e58347a9d
5
5
  SHA512:
6
- metadata.gz: bf7863cac7483b98c5bf03b8aa20794d98b7d907a3764e23264ec0e5f578ddc471901a48c045fe5cd3b6121f4b6b71b1af1ce739d3d3398c3eab383861bdc75f
7
- data.tar.gz: 35b9feb9220403d25edb0be5402430a15998705ad127c64a6c385562e55fdd62c820ab3e37829cdc6bbff041cd2818ce6bc83af94f204a966f2da0be2fd82e80
6
+ metadata.gz: a95534d284153f796aa7a93d3697c3aa4285e56b7cdcf2d44e4f9bed0dce5c3b7bd7fd8d393f1f0b253e171cf4816a14d2e451e1e483a3d283bd7ffd7ae68eb9
7
+ data.tar.gz: 7b38be8d008bf4b543c13bd53913519cc309e163ac2a6e3a321ce5caa0f626aebee5bfef7ba28c6a843fdcd5ecb727a5894ed69181746bb065b60e727c3efa0e
data/.gitignore CHANGED
@@ -9,3 +9,4 @@
9
9
  /tmp/
10
10
 
11
11
  *.gem
12
+ .byebug_history
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # RedisAppJoin
2
2
 
3
- Sometimes we 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 we need to combine data attributes from both tables? This can also be a use case when querying mutliple databases or 3rd party APIs.
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
- # => we also could have done comments.pluck(:article_id)
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 we could query DB again
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
- ### TODO:
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
- Write tests
110
+ ### Other config options
91
111
 
92
- Default TTL of 1.week
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
- Support JSON structures in caching (getting data from API), not just ActiveModels
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 we can do `article.user.name` from Redis cache.
122
+ Methods to fetch associated records so you can do `article.user.name` from Redis cache.
99
123
 
100
124
  ## Development
101
125
 
@@ -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
- def cache_records(records:)
12
- records.each do |record|
13
- key = [record.class.name, record.id.to_s].join(':')
14
- data = record.attributes.except(:_id, :id)
15
- REDIS_APP_JOIN.mapped_hmset(key, data)
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
- def delete_records(records:)
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 = [record.class.name, record.id.to_s].join(':')
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 recors from cache,
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] - name of class, used in lookup
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 = [record_class, record_id.to_s].join(':')
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] - name of class, used in lookup
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 = [record_class, record_id.to_s].join(':')
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
@@ -1,3 +1,3 @@
1
1
  module RedisAppJoin
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.2"
3
3
  end
@@ -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.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-19 00:00:00.000000000 Z
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