hold 1.0.2 → 1.0.3
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/lib/hold.rb +16 -8
- data/lib/hold/error.rb +28 -0
- data/lib/hold/file/hash_repository.rb +49 -42
- data/lib/hold/in_memory.rb +11 -179
- data/lib/hold/in_memory/array_cell.rb +27 -0
- data/lib/hold/in_memory/cell.rb +30 -0
- data/lib/hold/in_memory/hash_repository.rb +28 -0
- data/lib/hold/in_memory/identity_set_repository.rb +49 -0
- data/lib/hold/in_memory/object_cell.rb +30 -0
- data/lib/hold/in_memory/set_repository.rb +28 -0
- data/lib/hold/interfaces.rb +17 -439
- data/lib/hold/interfaces/array_cell.rb +49 -0
- data/lib/hold/interfaces/cell.rb +61 -0
- data/lib/hold/interfaces/hash_repository.rb +63 -0
- data/lib/hold/interfaces/identity_set_repository.rb +118 -0
- data/lib/hold/interfaces/object_cell.rb +103 -0
- data/lib/hold/interfaces/set_repository.rb +37 -0
- data/lib/hold/sequel.rb +12 -11
- data/lib/hold/sequel/identity_set_repository.rb +1 -8
- data/lib/hold/sequel/polymorphic_repository.rb +105 -96
- data/lib/hold/sequel/property_mapper.rb +144 -119
- data/lib/hold/sequel/query_array_cell.rb +19 -14
- data/lib/hold/sequel/repository_observer.rb +25 -21
- data/lib/hold/sequel/with_polymorphic_type_column.rb +145 -99
- data/lib/hold/serialized.rb +2 -104
- data/lib/hold/serialized/hash_repository.rb +50 -0
- data/lib/hold/serialized/identity_set_repository.rb +58 -0
- data/lib/hold/version.rb +2 -1
- metadata +78 -10
@@ -1,21 +1,26 @@
|
|
1
|
-
module Hold
|
2
|
-
|
3
|
-
|
1
|
+
module Hold
|
2
|
+
module Sequel
|
3
|
+
# Query an array cell
|
4
|
+
class QueryArrayCell
|
5
|
+
include Hold::ArrayCell
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
-
|
7
|
+
def initialize(repo, *_query_args, &query_block)
|
8
|
+
@repo, @query_block = repo, query_block
|
9
|
+
end
|
8
10
|
|
9
|
-
|
10
|
-
|
11
|
-
|
11
|
+
def get(properties = nil)
|
12
|
+
@repo.query(properties, &@query_block).to_a
|
13
|
+
end
|
12
14
|
|
13
|
-
|
14
|
-
|
15
|
-
|
15
|
+
def slice(start, length, properties = nil)
|
16
|
+
@repo.query(properties, &@query_block).to_a(true)[start, length]
|
17
|
+
end
|
18
|
+
alias_method :get_slice, :slice
|
16
19
|
|
17
|
-
|
18
|
-
|
20
|
+
def length
|
21
|
+
Query.new(@repo, [], &@query_block).to_a(true).length
|
22
|
+
end
|
23
|
+
alias_method :get_length, :length
|
19
24
|
end
|
20
25
|
end
|
21
26
|
end
|
@@ -1,28 +1,32 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
|
11
|
-
|
12
|
-
|
1
|
+
module Hold
|
2
|
+
module Sequel
|
3
|
+
# If you want to observe events on a Hold::Sequel::IdentitySetRepository
|
4
|
+
# you need to implement this interface. Stubs supplied here to save you some
|
5
|
+
# boilerplate in case you only care about certain events.
|
6
|
+
#
|
7
|
+
# The callback methods correspond to their counterparts on
|
8
|
+
# Hold::Sequel::IdentityHashRepository but with an added argument
|
9
|
+
# passing the repository instance.
|
10
|
+
#
|
11
|
+
# TODO: generalise this to SetRepositories in general
|
12
|
+
module RepositoryObserver
|
13
|
+
def pre_insert(_repo, _entity)
|
14
|
+
end
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
+
def post_insert(_repo, _entity, _insert_rows, _insert_id)
|
17
|
+
end
|
16
18
|
|
17
|
-
|
18
|
-
|
19
|
+
def pre_update(_repo, _entity, _update_entity)
|
20
|
+
end
|
19
21
|
|
20
|
-
|
21
|
-
|
22
|
+
def post_update(_repo, _entity, _update_entity, _update_rows)
|
23
|
+
end
|
22
24
|
|
23
|
-
|
24
|
-
|
25
|
+
def pre_delete(_repo, _entity)
|
26
|
+
end
|
25
27
|
|
26
|
-
|
28
|
+
def post_delete(_repo, _entity)
|
29
|
+
end
|
30
|
+
end
|
27
31
|
end
|
28
32
|
end
|
@@ -1,117 +1,163 @@
|
|
1
|
-
module Hold
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
def restricted_to_types; @restricted_to_types ||= (superclass.restricted_to_types if superclass < IdentitySetRepository::WithPolymorphicTypeColumn); end
|
12
|
-
|
13
|
-
def supported_classes
|
14
|
-
if restricted_to_types
|
15
|
-
type_to_class_mapping.values_at(*restricted_to_types)
|
16
|
-
else
|
17
|
-
class_to_type_mapping.keys
|
18
|
-
end
|
19
|
-
end
|
1
|
+
module Hold
|
2
|
+
module Sequel
|
3
|
+
class IdentitySetRepository
|
4
|
+
# Subclass of Sequel::IdentitySetRepository which adds support for a
|
5
|
+
# polymorphic type column which is used to persist the class of the model.
|
6
|
+
class WithPolymorphicTypeColumn < IdentitySetRepository
|
7
|
+
class << self
|
8
|
+
def type_column
|
9
|
+
@type_column ||= superclass.type_column
|
10
|
+
end
|
20
11
|
|
21
|
-
|
12
|
+
def type_column_table
|
13
|
+
@type_column_table ||= superclass.type_column_table
|
14
|
+
end
|
22
15
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
16
|
+
def class_to_type_mapping
|
17
|
+
@class_to_type_mapping ||=
|
18
|
+
if superclass < WithPolymorphicTypeColumn
|
19
|
+
superclass.class_to_type_mapping
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def type_to_class_mapping
|
24
|
+
@type_to_class_mapping ||=
|
25
|
+
if superclass < WithPolymorphicTypeColumn
|
26
|
+
superclass.type_to_class_mapping
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def restricted_to_types
|
31
|
+
@restricted_to_types ||=
|
32
|
+
if superclass < WithPolymorphicTypeColumn
|
33
|
+
superclass.restricted_to_types
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def supported_classes
|
38
|
+
if restricted_to_types
|
39
|
+
type_to_class_mapping.values_at(*restricted_to_types)
|
40
|
+
else
|
41
|
+
class_to_type_mapping.keys
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def set_type_column(column, table, mapping = nil)
|
48
|
+
unless superclass == WithPolymorphicTypeColumn
|
49
|
+
fail 'set_type_column only on the topmost subclass'
|
50
|
+
end
|
33
51
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
52
|
+
table, mapping = tables.first.first, table if table.is_a?(Hash)
|
53
|
+
@type_column = column
|
54
|
+
@type_column_table = table
|
55
|
+
@class_to_type_mapping = mapping
|
56
|
+
@type_to_class_mapping = mapping.invert
|
57
|
+
end
|
58
|
+
|
59
|
+
def set_model_class(model_class)
|
60
|
+
@model_class = model_class
|
61
|
+
unless class_to_type_mapping
|
62
|
+
fail 'set_type_column before set_model_class'
|
63
|
+
end
|
64
|
+
|
65
|
+
klasses = class_to_type_mapping.keys
|
66
|
+
.select { |klass| klass <= model_class }
|
67
|
+
|
68
|
+
@restricted_to_types = if klasses.size < @class_to_type_mapping.size
|
69
|
+
class_to_type_mapping.values_at(*klasses)
|
70
|
+
end
|
71
|
+
end
|
40
72
|
end
|
41
|
-
end
|
42
|
-
end
|
43
73
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
74
|
+
def initialize(db)
|
75
|
+
super
|
76
|
+
@qualified_type_column =
|
77
|
+
::Sequel::SQL::QualifiedIdentifier.new(self.class.type_column_table,
|
78
|
+
self.class.type_column)
|
48
79
|
|
49
|
-
|
50
|
-
|
51
|
-
self.class.tables.each do |name, options|
|
52
|
-
@tables_restricting_type[name] = true if options[:restricts_type]
|
53
|
-
end
|
54
|
-
end
|
80
|
+
@aliased_type_column =
|
81
|
+
::Sequel::SQL::AliasedExpression.new(@qualified_type_column, :type)
|
55
82
|
|
56
|
-
|
57
|
-
|
58
|
-
|
83
|
+
@restricted_to_types = self.class.restricted_to_types
|
84
|
+
@tables_restricting_type = {}
|
85
|
+
self.class.tables.each do |name, options|
|
86
|
+
@tables_restricting_type[name] = true if options[:restricts_type]
|
87
|
+
end
|
88
|
+
end
|
59
89
|
|
60
|
-
|
61
|
-
|
62
|
-
|
90
|
+
def can_get_class?(model_class)
|
91
|
+
self.class.supported_classes.include?(model_class)
|
92
|
+
end
|
63
93
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
raise "WithPolymorphicTypeColumn: type column value #{type_value} not found in mapping"
|
68
|
-
klass.new(property_hash)
|
69
|
-
end
|
94
|
+
def can_set_class?(model_class)
|
95
|
+
self.class.supported_classes.include?(model_class)
|
96
|
+
end
|
70
97
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
98
|
+
def construct_entity(property_hash, row = nil)
|
99
|
+
type_value = row && row[:type] || (return super)
|
100
|
+
klass =
|
101
|
+
self.class.type_to_class_mapping[type_value] ||
|
102
|
+
(fail "WithPolymorphicTypeColumn: type column value #{type_value}" \
|
103
|
+
' not found in mapping')
|
104
|
+
klass.new(property_hash)
|
105
|
+
end
|
77
106
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
unless @restricted_to_types && @restricted_to_types.length == 1
|
85
|
-
aliased_columns << @aliased_type_column
|
86
|
-
tables << self.class.type_column_table unless tables.include?(self.class.type_column_table)
|
87
|
-
end
|
88
|
-
return columns_by_property, aliased_columns, tables
|
89
|
-
end
|
107
|
+
# This optimisation has to be turned off for polymorphic repositories,
|
108
|
+
# since even if we know the ID, we have to query the db to find out the
|
109
|
+
# appropriate class to construct the object as.
|
110
|
+
def can_construct_from_id_alone?(properties)
|
111
|
+
super && @restricted_to_types && @restricted_to_types.length == 1
|
112
|
+
end
|
90
113
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
114
|
+
# ensure we select the type column in addition to any columns for mapped
|
115
|
+
# properties, so we know which class to instantiate for each row.
|
116
|
+
#
|
117
|
+
# If we're restricted to only one class, we don't need to select the
|
118
|
+
# type column
|
119
|
+
def columns_aliases_and_tables_for_properties(properties)
|
120
|
+
columns_by_property, aliased_columns, tables = super
|
121
|
+
unless @restricted_to_types && @restricted_to_types.length == 1
|
122
|
+
aliased_columns << @aliased_type_column
|
123
|
+
unless tables.include?(self.class.type_column_table)
|
124
|
+
tables << self.class.type_column_table
|
125
|
+
end
|
126
|
+
end
|
127
|
+
[columns_by_property, aliased_columns, tables]
|
128
|
+
end
|
129
|
+
|
130
|
+
# Where 'restricted_to_types' has been set, ensure we add a filter to
|
131
|
+
# the where clause restricting to rows with the allowed class (or
|
132
|
+
# classes).
|
133
|
+
#
|
134
|
+
# Except, where one of the tables used is specified in this repo's
|
135
|
+
# config as :restricts_type => true, this is taken to mean that (inner)
|
136
|
+
# joining to this table effectively acts as this repo's
|
137
|
+
# restricted_to_types restriction. hence no additional where clause is
|
138
|
+
# needed in order to do this. Helps with Class Table Inheritance.
|
139
|
+
def dataset_to_select_tables(*tables)
|
140
|
+
if @restricted_to_types &&
|
141
|
+
!@tables_restricting_type.values_at(*tables).any?
|
142
|
+
super.filter(@qualified_type_column => @restricted_to_types)
|
143
|
+
else
|
144
|
+
super
|
145
|
+
end
|
146
|
+
end
|
105
147
|
|
106
|
-
|
148
|
+
private
|
107
149
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
150
|
+
def insert_row_for_entity(entity, table, id = nil)
|
151
|
+
row = super
|
152
|
+
if table == self.class.type_column_table
|
153
|
+
row[self.class.type_column] =
|
154
|
+
self.class .class_to_type_mapping[entity.class] ||
|
155
|
+
(fail "WithPolymorphicTypeColumn: class #{entity.class} not" \
|
156
|
+
' found in mapping')
|
157
|
+
end
|
158
|
+
row
|
113
159
|
end
|
114
|
-
row
|
115
160
|
end
|
161
|
+
end
|
116
162
|
end
|
117
163
|
end
|
data/lib/hold/serialized.rb
CHANGED
@@ -1,106 +1,4 @@
|
|
1
1
|
require 'hold/interfaces'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
module Serialized; end
|
6
|
-
|
7
|
-
# A repository which caches serialized versions of an entity in a string-based key/value cache.
|
8
|
-
#
|
9
|
-
# Wraps a string-based HashRepository, and requires a serializer responding to 'serialize' and
|
10
|
-
# 'deserialize'.
|
11
|
-
#
|
12
|
-
# May optionally have a 'key_prefix', which is a prefixed namespace added to the cache keys
|
13
|
-
# before getting/setting the serialized values in the underlying cache.
|
14
|
-
class Serialized::HashRepository
|
15
|
-
include Hold::HashRepository
|
16
|
-
|
17
|
-
attr_reader :cache, :serializer, :key_prefix
|
18
|
-
|
19
|
-
def initialize(cache, serializer, key_prefix=nil)
|
20
|
-
@cache = cache
|
21
|
-
@serializer = serializer
|
22
|
-
@key_prefix = key_prefix
|
23
|
-
end
|
24
|
-
|
25
|
-
def cache_key(key)
|
26
|
-
@key_prefix ? @key_prefix + key.to_s : key.to_s
|
27
|
-
end
|
28
|
-
|
29
|
-
def set_with_key(key, entity)
|
30
|
-
@cache.set_with_key(cache_key(key), @serializer.serialize(entity))
|
31
|
-
end
|
32
|
-
|
33
|
-
def get_with_key(key)
|
34
|
-
json = @cache.get_with_key(cache_key(key)) and @serializer.deserialize(json)
|
35
|
-
end
|
36
|
-
|
37
|
-
def get_many_with_keys(keys)
|
38
|
-
jsons = @cache.get_many_with_keys(*keys.map {|key| cache_key(key)})
|
39
|
-
jsons.map {|json| json && @serializer.deserialize(json)}
|
40
|
-
end
|
41
|
-
|
42
|
-
def has_key?(key)
|
43
|
-
@cache.has_key?(cache_key(key))
|
44
|
-
end
|
45
|
-
alias_method :key?, :has_key?
|
46
|
-
|
47
|
-
def clear_key(key)
|
48
|
-
@cache.clear_key(cache_key(key))
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
class Serialized::IdentitySetRepository
|
53
|
-
include Hold::IdentitySetRepository
|
54
|
-
|
55
|
-
attr_reader :cache, :serializer, :key_prefix
|
56
|
-
|
57
|
-
def initialize(cache, serializer, key_prefix=nil)
|
58
|
-
@cache = cache
|
59
|
-
@serializer = serializer
|
60
|
-
@key_prefix = key_prefix
|
61
|
-
end
|
62
|
-
|
63
|
-
def cache_key(key)
|
64
|
-
@key_prefix ? @key_prefix + key.to_s : key.to_s
|
65
|
-
end
|
66
|
-
|
67
|
-
def allocates_ids?
|
68
|
-
false
|
69
|
-
end
|
70
|
-
|
71
|
-
def store(object)
|
72
|
-
id = object.id or raise MissingIdentity
|
73
|
-
@cache.set_with_key(cache_key(id), @serializer.serialize(object))
|
74
|
-
object
|
75
|
-
end
|
76
|
-
|
77
|
-
def delete(object)
|
78
|
-
id = object.id or raise MissingIdentity
|
79
|
-
delete_id(id)
|
80
|
-
end
|
81
|
-
|
82
|
-
def contains?(object)
|
83
|
-
id = object.id or raise MissingIdentity
|
84
|
-
contains_id?(id)
|
85
|
-
end
|
86
|
-
|
87
|
-
def get_by_id(id)
|
88
|
-
json = @cache.get_with_key(cache_key(id))
|
89
|
-
if json
|
90
|
-
string_hash = @serializer.deserialize(json)
|
91
|
-
string_hash.inject({}) do |memo, (k,v)|
|
92
|
-
memo[k.to_sym] = v
|
93
|
-
memo
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
def delete_id(id)
|
99
|
-
@cache.clear_key(cache_key(id))
|
100
|
-
end
|
101
|
-
|
102
|
-
def contains_id?(id)
|
103
|
-
@cache.has_key?(cache_key(id))
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|
3
|
+
require 'hold/serialized/hash_repository'
|
4
|
+
require 'hold/serialized/identity_set_repository'
|