db_memoize 0.1.6 → 0.2.1

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: 2134329e831a84420b606a2f64aeae8703ebcd7a
4
- data.tar.gz: 2abf35de9b06d53424513a0432cb884c73f1f4cd
3
+ metadata.gz: 254ec2be767020e1ff61a4b5ddbfc561f894b716
4
+ data.tar.gz: b27ccaf67e4a4dc32901fe802ab6b7da5872d9ba
5
5
  SHA512:
6
- metadata.gz: 208ba78d6912596d059e40e4f77f9393025a926c4c973a2599882a6980ce836c008442466d3db0c84fd8e1a9e2a625ca901f26d697f64054abdccf7976c700f9
7
- data.tar.gz: 994bdc8506e1886f57276380da78431812d9c74b99c66ea9f02ce9bffef87b1f13e27182c556ba5dab8363f8896de9c4ae9d2442be214a1c324cf007cb416c9f
6
+ metadata.gz: e430b58340a10f27910e558068cca7ac75ab8e07ec9d0aa5898d340aeeaa142dc3241779d3f1c02d16755d8390ddaee44518a704c563e9c5b7a5aa2ac670ea36
7
+ data.tar.gz: 82949ec2ac4094966459edbeadb5f34c91f03d9f20cda7e98ee9c1978b3b6fb12a671623473fdb7db88554a01f0ec9fcfb28c8fe8da873f5f218adbd1d07b430
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ /log/
@@ -4,5 +4,6 @@ rvm:
4
4
  - 2.3.1
5
5
  before_install: gem install bundler -v 1.13.6
6
6
  script:
7
+ - bundle exec rake db:test:create
7
8
  - bundle exec rspec
8
9
  - bundle exec rubocop
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('Maria')
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
@@ -3,4 +3,9 @@ require "rspec/core/rake_task"
3
3
 
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
- task :default => :spec
6
+ task "db:test:create" do
7
+ Rake.sh "dropdb db_memoize_test || true"
8
+ Rake.sh "createdb db_memoize_test"
9
+ end
10
+
11
+ task :default => %w(db:test:create spec)
@@ -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 'sqlite3'
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'
@@ -2,7 +2,9 @@ require 'active_record'
2
2
  require 'active_support'
3
3
  require 'digest'
4
4
  require 'benchmark'
5
+
5
6
  require 'db_memoize/version'
7
+ require 'db_memoize/metal'
6
8
  require 'db_memoize/value'
7
9
  require 'db_memoize/helpers'
8
10
  require 'db_memoize/model'
@@ -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, "DbMemoize <#{model.class.name} id: #{model.id}>##{method_name} - #{msg}")
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
@@ -8,11 +8,11 @@ module DbMemoize
8
8
  end
9
9
 
10
10
  value = nil
11
- args_hash = ::Digest::MD5.hexdigest(Marshal.dump(args))
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 = Marshal.load(cached_value.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
- args_hash = ::Digest::MD5.hexdigest(Marshal.dump(args))
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, args_hash, value)
57
- # [TODO] - It would be nice to have an optimized, pg-based inserter
58
- # here, for up to 10 times speed. However, the memoized_values
59
- # array must then be properly reset.
60
- memoized_values.create!(
61
- entity_table_name: self.class.table_name,
62
- method_name: method_name.to_s,
63
- arguments_hash: args_hash,
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 = Helpers.find_ids(records_or_ids)
106
- args_hash = ::Digest::MD5.hexdigest(Marshal.dump(args))
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 |name, value|
110
- DbMemoize::Value.create!(
111
- entity_table_name: table_name,
112
- entity_id: id,
113
- method_name: name,
114
- arguments_hash: args_hash,
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
@@ -1,5 +1,7 @@
1
1
  module DbMemoize
2
2
  class Value < ActiveRecord::Base
3
3
  self.table_name = 'memoized_values'
4
+
5
+ include DbMemoize::Metal
4
6
  end
5
7
  end
@@ -1,3 +1,3 @@
1
1
  module DbMemoize
2
- VERSION = '0.1.6'.freeze
2
+ VERSION = '0.2.1'.freeze
3
3
  end
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.6
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-01-31 00:00:00.000000000 Z
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: sqlite3
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