hold 1.0.2 → 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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'
|