smart_collection 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/smart_collection/associations/preloader/smart_collection.rb +63 -0
- data/lib/smart_collection/associations/smart_collection_association.rb +75 -0
- data/lib/smart_collection/builder/smart_collection_association.rb +26 -0
- data/lib/smart_collection/cache_manager/cache_store.rb +38 -0
- data/lib/smart_collection/cache_manager/table.rb +46 -0
- data/lib/smart_collection/cache_manager.rb +30 -0
- data/lib/smart_collection/config.rb +26 -0
- data/lib/smart_collection/mixin.rb +50 -0
- data/lib/smart_collection/reflection/smart_collection_reflection.rb +13 -0
- data/lib/smart_collection.rb +9 -0
- metadata +110 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 206884a5b82cfb9a32a8084b9e28602e0d06aef1
|
4
|
+
data.tar.gz: 4cfc1bd05e9b4e49d026e3388bb1409b9b92f1db
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e8f54ca5265ded99d18f0a41036fde1fb2052d6c1c26662482b409ecb7c9689bf14bd08f09fa9e10a7fa1cec331c979f88165e79d4fd1d7db0c52b7da5843599
|
7
|
+
data.tar.gz: de95c2aabb00d01f30fc5f60b826d2b8071d9ecd04e76a245466845a3d9f0a31a927bfc0db9d602ea12b100c8a81e916ad25044684e352c6423032c63ef461a7
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module SmartCollection
|
2
|
+
module Associations
|
3
|
+
module Preloader
|
4
|
+
class SmartCollectionCachedByTable < ActiveRecord::Associations::Preloader::HasManyThrough
|
5
|
+
def through_reflection
|
6
|
+
owners.first.class.reflect_on_association(:cached_items)
|
7
|
+
end
|
8
|
+
|
9
|
+
def source_reflection
|
10
|
+
association_name = reflection.options[:smart_collection].items_name.to_s.singularize.to_sym
|
11
|
+
through_reflection.klass.reflect_on_association(association_name)
|
12
|
+
end
|
13
|
+
|
14
|
+
def associated_records_by_owner preloader
|
15
|
+
owners.reject(&:cache_exists?).each(&:update_cache)
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
class SmartCollectionCachedByCacheStore < ActiveRecord::Associations::Preloader::CollectionAssociation
|
24
|
+
def associated_records_by_owner preloader
|
25
|
+
owners.reject(&:cache_exists?).each(&:update_cache)
|
26
|
+
loaded = reflection.options[:smart_collection].cache_manager.read_multi(owners)
|
27
|
+
records = reflection.options[:smart_collection].item_class.where(id: loaded.values.flatten.uniq).map{|x| [x.id, x]}.to_h
|
28
|
+
loaded.map do |owner, ids|
|
29
|
+
[owner, ids.map{|x| records[x]}]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def preload(preloader)
|
34
|
+
associated_records_by_owner(preloader).each do |owner, records|
|
35
|
+
association = owner.association(reflection.name)
|
36
|
+
association.loaded!
|
37
|
+
association.target.concat(records)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module ActiveRecordPreloaderPatch
|
45
|
+
def preloader_for(reflection, owners, rhs_klass)
|
46
|
+
if reflection.options[:smart_collection]
|
47
|
+
unless reflection.options[:smart_collection].cache_manager
|
48
|
+
raise RuntimeError, "Turn on cache to enable preloading."
|
49
|
+
end
|
50
|
+
case reflection.options[:smart_collection].cache_manager
|
51
|
+
when SmartCollection::CacheManager::CacheStore
|
52
|
+
SmartCollection::Associations::Preloader::SmartCollectionCachedByCacheStore
|
53
|
+
when SmartCollection::CacheManager::Table
|
54
|
+
SmartCollection::Associations::Preloader::SmartCollectionCachedByTable
|
55
|
+
end
|
56
|
+
else
|
57
|
+
super
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
ActiveRecord::Associations::Preloader.prepend ActiveRecordPreloaderPatch
|
63
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module SmartCollection
|
2
|
+
module Associations
|
3
|
+
class SmartCollectionAssociation < ::ActiveRecord::Associations::HasManyAssociation
|
4
|
+
|
5
|
+
class ScopeBuilder
|
6
|
+
def initialize rule, klass
|
7
|
+
@rule = rule
|
8
|
+
@klass = klass
|
9
|
+
@klass_hash = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def build
|
13
|
+
rule_to_bulk_queries @rule
|
14
|
+
bulk_load
|
15
|
+
rule_to_scope @rule
|
16
|
+
end
|
17
|
+
|
18
|
+
def bulk_load
|
19
|
+
@klass_hash = @klass_hash.map do |klass_name, ids|
|
20
|
+
[klass_name, Object.const_get(klass_name).where(id: ids).map{|x| [x.id, x]}.to_h]
|
21
|
+
end.to_h
|
22
|
+
end
|
23
|
+
|
24
|
+
def rule_to_bulk_queries rule
|
25
|
+
case
|
26
|
+
when arr = (rule['or'] || rule['and'])
|
27
|
+
arr.each{|x| rule_to_bulk_queries x}
|
28
|
+
when assoc = rule['association']
|
29
|
+
ids = @klass_hash[assoc['class_name']] ||= []
|
30
|
+
ids << assoc['id']
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def rule_to_scope rule
|
35
|
+
case
|
36
|
+
when ors = rule['or']
|
37
|
+
ors.map{|x| rule_to_scope x}.inject(:or)
|
38
|
+
when ands = rule['and']
|
39
|
+
ands.map{|x| rule_to_scope x}.inject(:merge)
|
40
|
+
when assoc = rule['association']
|
41
|
+
@klass_hash[assoc['class_name']][assoc['id']].association(assoc['source']).scope
|
42
|
+
when cond = rule['condition']
|
43
|
+
scope = @klass
|
44
|
+
scope = scope.joins(cond['joins'].to_sym) if cond['joins']
|
45
|
+
scope.where(cond['where'])
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def association_scope
|
51
|
+
if cache_manager = reflection.options[:smart_collection].cache_manager
|
52
|
+
unless cache_manager.cache_exists? owner
|
53
|
+
owner.update_cache
|
54
|
+
end
|
55
|
+
cached_scope
|
56
|
+
else
|
57
|
+
uncached_scope
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def skip_statement_cache?
|
62
|
+
true
|
63
|
+
end
|
64
|
+
|
65
|
+
def uncached_scope
|
66
|
+
ScopeBuilder.new(owner.rule, reflection.klass).build
|
67
|
+
end
|
68
|
+
|
69
|
+
def cached_scope
|
70
|
+
reflection.options[:smart_collection].cache_manager.read owner
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module SmartCollection
|
2
|
+
module Builder
|
3
|
+
class SmartCollectionAssociation < ::ActiveRecord::Associations::Builder::CollectionAssociation
|
4
|
+
def self.macro
|
5
|
+
:has_many
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.valid_options(options)
|
9
|
+
super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type, :index_errors, :smart_collection]
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.valid_dependent_options
|
13
|
+
[:destroy, :delete_all, :nullify, :restrict_with_error, :restrict_with_exception]
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.create_reflection(model, name, scope, options, extension = nil)
|
17
|
+
raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)
|
18
|
+
|
19
|
+
validate_options(options)
|
20
|
+
|
21
|
+
scope = build_scope(scope, extension)
|
22
|
+
Reflection::SmartCollectionReflection.new(name, scope, options, model)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module SmartCollection
|
2
|
+
class CacheManager
|
3
|
+
class CacheStore < CacheManager
|
4
|
+
|
5
|
+
def cache_store
|
6
|
+
@config.cache_config[:cache_store]
|
7
|
+
end
|
8
|
+
|
9
|
+
def cache_key owner
|
10
|
+
"_smart_collection_#{owner.class.name}_#{owner.id}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def update owner
|
14
|
+
association = owner.association(@config.items_name)
|
15
|
+
|
16
|
+
cache_store.write(cache_key(owner), Marshal.dump(association.uncached_scope.pluck(:id)))
|
17
|
+
owner.update(cache_expires_at: Time.now + expires_in)
|
18
|
+
end
|
19
|
+
|
20
|
+
def read owner
|
21
|
+
@config.item_class.where(id: Marshal.load(cache_store.read(cache_key owner)))
|
22
|
+
end
|
23
|
+
|
24
|
+
def read_multi owners
|
25
|
+
cache_keys = owners.map{|owner| cache_key owner}
|
26
|
+
loaded = cache_store.read_multi(*cache_keys)
|
27
|
+
owners.map.with_index do |owner, index|
|
28
|
+
[owner, Marshal.load(loaded[cache_keys[index]])]
|
29
|
+
end.to_h
|
30
|
+
end
|
31
|
+
|
32
|
+
def cache_exists? owner
|
33
|
+
!(owner.cache_expires_at.nil? || owner.cache_expires_at < Time.now) && cache_store.exist?(cache_key owner)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module SmartCollection
|
2
|
+
class CacheManager
|
3
|
+
class Table < CacheManager
|
4
|
+
|
5
|
+
def initialize model:, config:
|
6
|
+
super
|
7
|
+
|
8
|
+
define_cache_association_for model
|
9
|
+
end
|
10
|
+
|
11
|
+
def define_cache_association_for model
|
12
|
+
options = @config.raw_config
|
13
|
+
cached_item_model = nil
|
14
|
+
model.class_eval do
|
15
|
+
cached_item_model = Class.new ActiveRecord::Base do
|
16
|
+
self.table_name = 'smart_collection_cached_items'
|
17
|
+
belongs_to options[:items].to_s.singularize.to_sym, class_name: options[:class_name], foreign_key: :item_id
|
18
|
+
end
|
19
|
+
const_set("CachedItem", cached_item_model)
|
20
|
+
|
21
|
+
has_many :cached_items, class_name: cached_item_model.name, foreign_key: :collection_id
|
22
|
+
has_many "cached_#{options[:items]}".to_sym, class_name: options[:class_name], through: :cached_items, source: options[:items].to_s.singularize.to_sym
|
23
|
+
end
|
24
|
+
@cache_model = cached_item_model
|
25
|
+
end
|
26
|
+
|
27
|
+
def update owner
|
28
|
+
association = owner.association(@config.items_name)
|
29
|
+
|
30
|
+
@cache_model.where(collection_id: owner.id).delete_all
|
31
|
+
@cache_model.connection.execute "INSERT INTO #{@cache_model.table_name} (collection_id, item_id) #{association.uncached_scope.select(owner.id, :id).to_sql}"
|
32
|
+
owner.update(cache_expires_at: Time.now + expires_in)
|
33
|
+
end
|
34
|
+
|
35
|
+
def read owner
|
36
|
+
cache_association = owner.association("cached_#{@config.items_name}")
|
37
|
+
cache_association.scope
|
38
|
+
end
|
39
|
+
|
40
|
+
def cache_exists? owner
|
41
|
+
!(owner.cache_expires_at.nil? || owner.cache_expires_at < Time.now)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module SmartCollection
|
2
|
+
class CacheManager
|
3
|
+
|
4
|
+
def self.determine_class config
|
5
|
+
case
|
6
|
+
when config.dig(:cached_by, :table)
|
7
|
+
SmartCollection::CacheManager::Table
|
8
|
+
when config.dig(:cached_by, :cache_store)
|
9
|
+
SmartCollection::CacheManager::CacheStore
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize model:, config:
|
14
|
+
@model = model
|
15
|
+
@config = config
|
16
|
+
end
|
17
|
+
|
18
|
+
def update owner
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
def read owner
|
23
|
+
raise NotImplementedError
|
24
|
+
end
|
25
|
+
|
26
|
+
def expires_in
|
27
|
+
@config.cache_config[:expires_in] || 1.hour
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module SmartCollection
|
2
|
+
class Config
|
3
|
+
attr_accessor :cache_manager
|
4
|
+
attr_reader :raw_config
|
5
|
+
|
6
|
+
def initialize raw_config
|
7
|
+
@raw_config = raw_config
|
8
|
+
end
|
9
|
+
|
10
|
+
def items_name
|
11
|
+
@raw_config[:items]
|
12
|
+
end
|
13
|
+
|
14
|
+
def item_class_name
|
15
|
+
@raw_config[:item_class]
|
16
|
+
end
|
17
|
+
|
18
|
+
def item_class
|
19
|
+
item_class_name.constantize
|
20
|
+
end
|
21
|
+
|
22
|
+
def cache_config
|
23
|
+
@raw_config[:cached_by]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module SmartCollection
|
2
|
+
class Mixin < Module
|
3
|
+
module InstanceMethods
|
4
|
+
def update_cache
|
5
|
+
smart_collection_mixin.config.cache_manager.update self
|
6
|
+
end
|
7
|
+
|
8
|
+
def expire_cache
|
9
|
+
update_column(:cache_expires_at, nil)
|
10
|
+
end
|
11
|
+
|
12
|
+
def cache_exists?
|
13
|
+
smart_collection_mixin.config.cache_manager.cache_exists? self
|
14
|
+
end
|
15
|
+
|
16
|
+
def smart_collection_mixin
|
17
|
+
@__smart_collection_mixin ||= self.class.ancestors.find do |x|
|
18
|
+
x.instance_of? Mixin
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :config
|
24
|
+
|
25
|
+
def initialize items:, item_class: nil, cached_by: nil
|
26
|
+
@raw_config = {
|
27
|
+
items: items,
|
28
|
+
item_class: item_class,
|
29
|
+
cached_by: cached_by
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def included base
|
34
|
+
COLLECTIONS[base] = true
|
35
|
+
@config = config = SmartCollection::Config.new(@raw_config)
|
36
|
+
name = config.items_name
|
37
|
+
|
38
|
+
options = {smart_collection: config}
|
39
|
+
options[:class_name] = config.item_class_name if config.item_class_name
|
40
|
+
reflection = Builder::SmartCollectionAssociation.build(base, name, nil, options)
|
41
|
+
::ActiveRecord::Reflection.add_reflection base, name, reflection
|
42
|
+
base.include(InstanceMethods)
|
43
|
+
|
44
|
+
if cache_class = CacheManager.determine_class(@raw_config)
|
45
|
+
config.cache_manager = cache_class.new(model: base, config: config)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module SmartCollection
|
2
|
+
module Reflection
|
3
|
+
class SmartCollectionReflection < ::ActiveRecord::Reflection::HasManyReflection
|
4
|
+
def association_class
|
5
|
+
Associations::SmartCollectionAssociation
|
6
|
+
end
|
7
|
+
|
8
|
+
def check_eager_loadable!
|
9
|
+
raise RuntimeError, 'eager_load is not supported by now.'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: smart_collection
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- CicholGricenchos
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-01-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pry
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sqlite3
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: database_cleaner
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: ''
|
70
|
+
email:
|
71
|
+
- cichol@live.cn
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- lib/smart_collection.rb
|
77
|
+
- lib/smart_collection/associations/preloader/smart_collection.rb
|
78
|
+
- lib/smart_collection/associations/smart_collection_association.rb
|
79
|
+
- lib/smart_collection/builder/smart_collection_association.rb
|
80
|
+
- lib/smart_collection/cache_manager.rb
|
81
|
+
- lib/smart_collection/cache_manager/cache_store.rb
|
82
|
+
- lib/smart_collection/cache_manager/table.rb
|
83
|
+
- lib/smart_collection/config.rb
|
84
|
+
- lib/smart_collection/mixin.rb
|
85
|
+
- lib/smart_collection/reflection/smart_collection_reflection.rb
|
86
|
+
homepage: https://github.com/CicholGricenchos/smart_collection
|
87
|
+
licenses:
|
88
|
+
- MIT
|
89
|
+
metadata: {}
|
90
|
+
post_install_message:
|
91
|
+
rdoc_options: []
|
92
|
+
require_paths:
|
93
|
+
- lib
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubyforge_project:
|
106
|
+
rubygems_version: 2.5.2.1
|
107
|
+
signing_key:
|
108
|
+
specification_version: 4
|
109
|
+
summary: ''
|
110
|
+
test_files: []
|