db_memoize 0.1.6 → 0.2.1
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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +1 -0
- data/README.md +12 -34
- data/Rakefile +6 -1
- data/db_memoize.gemspec +1 -1
- data/lib/db_memoize.rb +2 -0
- data/lib/db_memoize/helpers.rb +15 -1
- data/lib/db_memoize/metal.rb +102 -0
- data/lib/db_memoize/migrations.rb +16 -4
- data/lib/db_memoize/model.rb +21 -23
- data/lib/db_memoize/value.rb +2 -0
- data/lib/db_memoize/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 254ec2be767020e1ff61a4b5ddbfc561f894b716
|
4
|
+
data.tar.gz: b27ccaf67e4a4dc32901fe802ab6b7da5872d9ba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e430b58340a10f27910e558068cca7ac75ab8e07ec9d0aa5898d340aeeaa142dc3241779d3f1c02d16755d8390ddaee44518a704c563e9c5b7a5aa2ac670ea36
|
7
|
+
data.tar.gz: 82949ec2ac4094966459edbeadb5f34c91f03d9f20cda7e98ee9c1978b3b6fb12a671623473fdb7db88554a01f0ec9fcfb28c8fe8da873f5f218adbd1d07b430
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -35,10 +35,8 @@ will call the original method only once. Consecutive calls will return a cached
|
|
35
35
|
|
36
36
|
If the method takes arguments..
|
37
37
|
|
38
|
-
|
39
|
-
record.hello('
|
40
|
-
record.hello('John')
|
41
|
-
```
|
38
|
+
record.hello('Maria')
|
39
|
+
record.hello('John')
|
42
40
|
|
43
41
|
a cached value will be created for every set of arguments.
|
44
42
|
|
@@ -46,67 +44,47 @@ a cached value will be created for every set of arguments.
|
|
46
44
|
|
47
45
|
To clear cached values for a single method
|
48
46
|
|
49
|
-
|
50
|
-
record.unmemoize(:hello)
|
51
|
-
```
|
47
|
+
record.unmemoize(:hello)
|
52
48
|
|
53
49
|
To clear all cached values of one record
|
54
50
|
|
55
|
-
|
56
|
-
record.unmemoize
|
57
|
-
```
|
51
|
+
record.unmemoize
|
58
52
|
|
59
53
|
To clear cached values of given records for a single method
|
60
54
|
|
61
|
-
|
62
|
-
Letter.unmemoize([letter1, letter2], :hello)
|
63
|
-
```
|
55
|
+
Letter.unmemoize([letter1, letter2], :hello)
|
64
56
|
|
65
57
|
To clear all cached values of given records
|
66
58
|
|
67
|
-
|
68
|
-
Letter.unmemoize([letter1, letter2])
|
69
|
-
```
|
59
|
+
Letter.unmemoize([letter1, letter2])
|
70
60
|
|
71
61
|
Instead of ActiveRecord instances it's sufficient to pass in the ids of the records, too
|
72
62
|
|
73
|
-
|
74
|
-
Letter.unmemoize([23,24])
|
75
|
-
```
|
63
|
+
Letter.unmemoize([23,24])
|
76
64
|
|
77
65
|
### Gotchas
|
78
66
|
|
79
67
|
As the cached values themselves are writtten to the database, are ActiveRecord records (of type `DbMemoize::Value`) and are reqistered as an association you can access all of the cached values of an object like this:
|
80
68
|
|
81
|
-
|
82
|
-
record.memoized_values
|
83
|
-
```
|
69
|
+
record.memoized_values
|
84
70
|
|
85
71
|
This means you can also very easily perform eager loading on them:
|
86
72
|
|
87
|
-
|
88
|
-
Letter.includes(:memoized_values).all
|
89
|
-
```
|
73
|
+
Letter.includes(:memoized_values).all
|
90
74
|
|
91
75
|
DbMemoize by default will write log output to STDOUT. You can change this by setting another logger like so:
|
92
76
|
|
93
|
-
|
94
|
-
DbMemoize.logger = your_logger
|
95
|
-
```
|
77
|
+
DbMemoize.logger = your_logger
|
96
78
|
|
97
79
|
### Rake Tasks
|
98
80
|
|
99
81
|
To _warmup_ your cache you can pre-generate cached values via a rake task like this (only works for methods not depending on arguments)
|
100
82
|
|
101
|
-
|
102
|
-
bundle exec rake db_memoize:warmup class=Letter methods=hello,bye
|
103
|
-
```
|
83
|
+
bundle exec rake db_memoize:warmup class=Letter methods=hello,bye
|
104
84
|
|
105
85
|
Similarly you can wipe all cached values for a given class
|
106
86
|
|
107
|
-
|
108
|
-
bundle exec rake db_memoize:clear class=Letter
|
109
|
-
```
|
87
|
+
bundle exec rake db_memoize:clear class=Letter
|
110
88
|
|
111
89
|
### Setup
|
112
90
|
|
data/Rakefile
CHANGED
data/db_memoize.gemspec
CHANGED
@@ -25,7 +25,7 @@ Gem::Specification.new do |spec|
|
|
25
25
|
spec.add_development_dependency 'activerecord', '~> 4.2'
|
26
26
|
|
27
27
|
spec.add_development_dependency 'rake', '~> 10.5'
|
28
|
-
spec.add_development_dependency '
|
28
|
+
spec.add_development_dependency 'pg'
|
29
29
|
spec.add_development_dependency 'rspec-rails', '~> 3.4'
|
30
30
|
spec.add_development_dependency 'pry', '~> 0.10'
|
31
31
|
spec.add_development_dependency 'pry-byebug', '~> 2.0'
|
data/lib/db_memoize.rb
CHANGED
data/lib/db_memoize/helpers.rb
CHANGED
@@ -12,7 +12,21 @@ module DbMemoize
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def log(model, method_name, msg)
|
15
|
-
DbMemoize.logger.send(DbMemoize.log_level
|
15
|
+
DbMemoize.logger.send(DbMemoize.log_level) do
|
16
|
+
"DbMemoize <#{model.class.name}##{model.id}>##{method_name} - #{msg}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def calculate_arguments_hash(arguments)
|
21
|
+
arguments.empty? ? nil : ::Digest::MD5.hexdigest(Marshal.dump(arguments))
|
22
|
+
end
|
23
|
+
|
24
|
+
def marshal(value)
|
25
|
+
Marshal.dump(value)
|
26
|
+
end
|
27
|
+
|
28
|
+
def unmarshal(value)
|
29
|
+
Marshal.load(value)
|
16
30
|
end
|
17
31
|
end
|
18
32
|
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module DbMemoize
|
2
|
+
module Metal
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
# base.metal # initialize the metal adapter
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def metal
|
10
|
+
@metal ||= Adapter.new(self)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Adapter
|
15
|
+
class PkInfo
|
16
|
+
attr_reader :column, :type
|
17
|
+
|
18
|
+
def initialize(base_klass)
|
19
|
+
@column = base_klass.primary_key
|
20
|
+
@type = @column && base_klass.columns_hash.fetch(@column).type
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(base_klass)
|
25
|
+
@base_klass = base_klass
|
26
|
+
@query_cache = {}
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# setup primary key information. This is necessary to allow the create! method
|
32
|
+
# to return the primary key of a newly created entry.
|
33
|
+
def primary_key
|
34
|
+
@primary_key ||= PkInfo.new(@base_klass)
|
35
|
+
end
|
36
|
+
|
37
|
+
def table_name
|
38
|
+
@base_klass.table_name
|
39
|
+
end
|
40
|
+
|
41
|
+
def raw_connection
|
42
|
+
@base_klass.connection.raw_connection # do not memoize me!
|
43
|
+
end
|
44
|
+
|
45
|
+
def column?(column_name)
|
46
|
+
@base_klass.columns_hash.key?(column_name)
|
47
|
+
end
|
48
|
+
|
49
|
+
def insert_sql(field_names)
|
50
|
+
@query_cache[field_names] ||= _insert_sql(field_names)
|
51
|
+
end
|
52
|
+
|
53
|
+
DATABASE_IDENTIFIER_REGEX = /\A\w+\z/
|
54
|
+
|
55
|
+
def check_database_identifiers!(*strings)
|
56
|
+
strings.each do |s|
|
57
|
+
next if DATABASE_IDENTIFIER_REGEX =~ s.to_s
|
58
|
+
raise ArgumentError, "Invalid database identifier: #{s.inspect}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def _insert_sql(field_names)
|
63
|
+
check_database_identifiers! table_name, *field_names
|
64
|
+
|
65
|
+
placeholders = 0.upto(field_names.count - 1).map { |idx| "$#{idx + 1}" }
|
66
|
+
|
67
|
+
if column?('created_at')
|
68
|
+
field_names << 'created_at'
|
69
|
+
placeholders << 'current_timestamp'
|
70
|
+
end
|
71
|
+
|
72
|
+
if column?('updated_at')
|
73
|
+
field_names << 'updated_at'
|
74
|
+
placeholders << 'current_timestamp'
|
75
|
+
end
|
76
|
+
|
77
|
+
sql = "INSERT INTO #{table_name} (#{field_names.join(',')}) VALUES(#{placeholders.join(',')})"
|
78
|
+
sql += " RETURNING #{primary_key.column}" if primary_key.column
|
79
|
+
sql
|
80
|
+
end
|
81
|
+
|
82
|
+
public
|
83
|
+
|
84
|
+
def create!(record)
|
85
|
+
keys, values = record.to_a.transpose
|
86
|
+
|
87
|
+
sql = insert_sql(keys)
|
88
|
+
result = raw_connection.exec_params(sql, values)
|
89
|
+
|
90
|
+
# if we don't have an ID column then the sql does not return any value. The result
|
91
|
+
# object would be this: #<PG::Result status=PGRES_COMMAND_OK ntuples=0 nfields=0 cmd_tuples=1>
|
92
|
+
# we just return nil in that case; otherwise we return the first entry of the first result row
|
93
|
+
# which would be the stringified id.
|
94
|
+
first_row = result.each_row.first
|
95
|
+
return nil unless first_row
|
96
|
+
|
97
|
+
id = first_row.first
|
98
|
+
primary_key.type == :integer ? Integer(id) : id
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -3,15 +3,27 @@ module DbMemoize
|
|
3
3
|
class << self
|
4
4
|
def create_tables(migration)
|
5
5
|
migration.create_table :memoized_values, id: false do |t|
|
6
|
-
t.string :entity_table_name
|
7
|
-
t.integer :entity_id
|
8
|
-
t.string :method_name
|
6
|
+
t.string :entity_table_name, null: false
|
7
|
+
t.integer :entity_id, null: false
|
8
|
+
t.string :method_name, null: false
|
9
9
|
t.string :arguments_hash
|
10
10
|
t.binary :value
|
11
|
-
t.datetime :created_at
|
11
|
+
t.datetime :created_at, null: false
|
12
12
|
end
|
13
13
|
|
14
14
|
migration.add_index :memoized_values, [:entity_table_name, :entity_id]
|
15
|
+
migrate_empty_arguments_support(migration)
|
16
|
+
end
|
17
|
+
|
18
|
+
def migrate_empty_arguments_support(migration)
|
19
|
+
# entity_id/entity_table_name should have a better chance to be useful, since
|
20
|
+
# there is more variance in entity_ids than there is in entity_table_names.
|
21
|
+
migration.remove_index :memoized_values, [:entity_table_name, :entity_id]
|
22
|
+
migration.add_index :memoized_values, [:entity_id, :entity_table_name]
|
23
|
+
|
24
|
+
# add an index to be useful to look up entries where arguments_hash is NULL.
|
25
|
+
# (which is the case for plain attributes of an object)
|
26
|
+
migration.execute 'CREATE INDEX memoized_attributes_idx ON memoized_values((arguments_hash IS NULL))'
|
15
27
|
end
|
16
28
|
end
|
17
29
|
end
|
data/lib/db_memoize/model.rb
CHANGED
@@ -8,11 +8,11 @@ module DbMemoize
|
|
8
8
|
end
|
9
9
|
|
10
10
|
value = nil
|
11
|
-
args_hash =
|
11
|
+
args_hash = Helpers.calculate_arguments_hash(args)
|
12
12
|
cached_value = find_memoized_value(method_name, args_hash)
|
13
13
|
|
14
14
|
if cached_value
|
15
|
-
value =
|
15
|
+
value = Helpers.unmarshal(cached_value.value)
|
16
16
|
Helpers.log(self, method_name, 'cache hit')
|
17
17
|
else
|
18
18
|
time = ::Benchmark.realtime do
|
@@ -44,7 +44,8 @@ module DbMemoize
|
|
44
44
|
# autocomplete_info: "my autocomplete_info"
|
45
45
|
#
|
46
46
|
def memoize_values(values, *args)
|
47
|
-
|
47
|
+
# [TODO] - when creating many memoized values: should we even support arguments here?
|
48
|
+
args_hash = Helpers.calculate_arguments_hash(args)
|
48
49
|
|
49
50
|
values.each do |name, value|
|
50
51
|
create_memoized_value(name, args_hash, value)
|
@@ -53,16 +54,14 @@ module DbMemoize
|
|
53
54
|
|
54
55
|
private
|
55
56
|
|
56
|
-
def create_memoized_value(method_name,
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
value: Marshal.dump(value)
|
65
|
-
)
|
57
|
+
def create_memoized_value(method_name, arguments_hash, value)
|
58
|
+
::DbMemoize::Value.metal.create! entity_table_name: self.class.table_name,
|
59
|
+
entity_id: id,
|
60
|
+
method_name: method_name.to_s,
|
61
|
+
arguments_hash: arguments_hash,
|
62
|
+
value: Helpers.marshal(value)
|
63
|
+
|
64
|
+
@association_cache.delete :memoized_values
|
66
65
|
end
|
67
66
|
|
68
67
|
def find_memoized_value(method_name, args_hash)
|
@@ -101,19 +100,18 @@ module DbMemoize
|
|
101
100
|
end
|
102
101
|
|
103
102
|
def memoize_values(records_or_ids, values, *args)
|
103
|
+
# [TODO] - when creating many memoized values: should we even support arguments here?
|
104
104
|
transaction do
|
105
|
-
ids
|
106
|
-
|
105
|
+
ids = Helpers.find_ids(records_or_ids)
|
106
|
+
arguments_hash = Helpers.calculate_arguments_hash(args)
|
107
107
|
|
108
108
|
ids.each do |id|
|
109
|
-
values.each do |
|
110
|
-
DbMemoize::Value.create!
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
value: Marshal.dump(value)
|
116
|
-
)
|
109
|
+
values.each do |method_name, value|
|
110
|
+
::DbMemoize::Value.metal.create! entity_table_name: table_name,
|
111
|
+
entity_id: id,
|
112
|
+
method_name: method_name.to_s,
|
113
|
+
arguments_hash: arguments_hash,
|
114
|
+
value: Helpers.marshal(value)
|
117
115
|
end
|
118
116
|
end
|
119
117
|
end
|
data/lib/db_memoize/value.rb
CHANGED
data/lib/db_memoize/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: db_memoize
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- johannes-kostas goetzinger
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-06-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: railties
|
@@ -53,7 +53,7 @@ dependencies:
|
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '10.5'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: pg
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - ">="
|
@@ -240,6 +240,7 @@ files:
|
|
240
240
|
- db_memoize.gemspec
|
241
241
|
- lib/db_memoize.rb
|
242
242
|
- lib/db_memoize/helpers.rb
|
243
|
+
- lib/db_memoize/metal.rb
|
243
244
|
- lib/db_memoize/migrations.rb
|
244
245
|
- lib/db_memoize/model.rb
|
245
246
|
- lib/db_memoize/railtie.rb
|
@@ -247,6 +248,7 @@ files:
|
|
247
248
|
- lib/db_memoize/version.rb
|
248
249
|
- lib/tasks/clear.rake
|
249
250
|
- lib/tasks/warmup.rake
|
251
|
+
- log/.keep
|
250
252
|
homepage: https://github.com/mediapeers/db_memoize
|
251
253
|
licenses:
|
252
254
|
- MIT
|