smart_collection 0.0.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 +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: []
|