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
@@ -367,16 +367,9 @@ module Hold::Sequel
|
|
367
367
|
end
|
368
368
|
|
369
369
|
def get_by_property(property, value, options={})
|
370
|
-
|
371
|
-
properties_to_fetch[property] = true
|
372
|
-
query(options[:properties]) do |dataset, property_columns|
|
373
|
-
filter = mapper(property).make_filter(value, property_columns[property])
|
374
|
-
dataset.filter(filter)
|
375
|
-
end.single_result
|
370
|
+
get_many_by_property(property, value, options).first
|
376
371
|
end
|
377
372
|
|
378
|
-
|
379
|
-
|
380
373
|
def contains_id?(id)
|
381
374
|
dataset = dataset_to_select_tables(@main_table)
|
382
375
|
id_filter = @identity_mapper.make_filter(id, [@tables_id_columns[@main_table]])
|
@@ -1,121 +1,130 @@
|
|
1
|
-
module Hold
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
1
|
+
module Hold
|
2
|
+
module Sequel
|
3
|
+
# Polymorphic repository
|
4
|
+
class PolymorphicRepository
|
5
|
+
include Hold::IdentitySetRepository
|
6
|
+
|
7
|
+
attr_reader :db, :table, :type_column, :id_column,
|
8
|
+
:type_to_model_class_mapping, :repos_for_model_classes,
|
9
|
+
:model_class_to_type_mapping
|
10
|
+
|
11
|
+
def initialize(db, options = {})
|
12
|
+
@db = db
|
13
|
+
@table = options[:table] || :base
|
14
|
+
@type_column = options[:type_column] || :type
|
15
|
+
@id_column = options[:id_column] || :id
|
16
|
+
@type_to_model_class_mapping = options[:mapping]
|
17
|
+
@model_class_to_type_mapping = @type_to_model_class_mapping.invert
|
18
|
+
|
19
|
+
@repos_for_model_classes = options[:repos] || {}
|
20
|
+
@dataset = @db[@table].select(Sequel.as(@type_column, :_type),
|
21
|
+
Sequel.as(@id_column, :_id))
|
22
|
+
end
|
19
23
|
|
20
|
-
|
21
|
-
|
22
|
-
|
24
|
+
def can_get_class?(model_class)
|
25
|
+
@model_class_to_type_mapping.key?(model_class)
|
26
|
+
end
|
23
27
|
|
24
|
-
|
25
|
-
|
26
|
-
|
28
|
+
def can_set_class?(model_class)
|
29
|
+
@model_class_to_type_mapping.key?(model_class)
|
30
|
+
end
|
27
31
|
|
28
|
-
|
29
|
-
|
30
|
-
|
32
|
+
def get_repo_dependencies_from(repo_set)
|
33
|
+
@type_to_model_class_mapping.each do |_, model_class|
|
34
|
+
@repos_for_model_classes[model_class] ||=
|
35
|
+
repo_set.repo_for_model_class(model_class)
|
36
|
+
end
|
31
37
|
end
|
32
|
-
end
|
33
38
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
+
def type_to_repo_mapping
|
40
|
+
@type_to_repo_mapping ||=
|
41
|
+
begin
|
42
|
+
@type_to_repo_mapping.each_with_obeject({}) do |(t, m), hash|
|
43
|
+
hash[t] = @repos_for_model_classes[m]
|
44
|
+
end
|
45
|
+
end
|
39
46
|
end
|
40
|
-
end
|
41
47
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
48
|
+
def construct_entity(property_hash, _row = nil)
|
49
|
+
type = property_hash[:_type] || (fail 'missing _type in result row')
|
50
|
+
@type_to_model_class_mapping[type].new(property_hash)
|
51
|
+
end
|
46
52
|
|
47
|
-
|
48
|
-
|
49
|
-
|
53
|
+
def transaction(*p, &b)
|
54
|
+
@db.transaction(*p, &b)
|
55
|
+
end
|
50
56
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
57
|
+
# - Takes multiple result rows with type and id column
|
58
|
+
# - Groups the IDs by type and does a separate get_many_by_ids query on
|
59
|
+
# the relevant repo
|
60
|
+
# - Combines the results from the separate queries putting them into the
|
61
|
+
# order of the IDs from the original rows (or in the order of the ids
|
62
|
+
# given, where they are given)
|
63
|
+
def load_from_rows(rows, options = {}, ids = nil)
|
64
|
+
ids ||= rows.map { |row| row[:_id] }
|
65
|
+
ids_by_type = Hash.new { |h, k| h[k] = [] }
|
66
|
+
rows.each { |row| ids_by_type[row[:_type]] << row[:_id] }
|
67
|
+
results_by_id = {}
|
68
|
+
ids_by_type.each do |type, type_ids|
|
69
|
+
repo = type_to_repo_mapping[type] ||
|
70
|
+
(fail "PolymorphicRepository: no repo found for type #{type}")
|
71
|
+
repo.get_many_by_ids(type_ids, options)
|
72
|
+
.each_with_index { |res, i| results_by_id[type_ids[i]] = res }
|
64
73
|
end
|
74
|
+
results_by_id.values_at(*ids)
|
65
75
|
end
|
66
|
-
results_by_id.values_at(*ids)
|
67
|
-
end
|
68
76
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
def get_with_dataset(options={}, &b)
|
75
|
-
dataset = @dataset
|
76
|
-
dataset = yield @dataset if block_given?
|
77
|
-
row = dataset.limit(1).first and load_from_row(row, options)
|
78
|
-
end
|
77
|
+
def load_from_row(row, options = {})
|
78
|
+
repo =
|
79
|
+
type_to_repo_mapping[row[:_type]] ||
|
80
|
+
(fail "PolymorphicRepository: no repo found for type #{row[:_type]}")
|
79
81
|
|
80
|
-
|
81
|
-
|
82
|
-
end
|
83
|
-
|
84
|
-
def get_many_by_ids(ids, options={})
|
85
|
-
rows = @dataset.filter(@id_column => ids).all
|
86
|
-
load_from_rows(rows, options, ids)
|
87
|
-
end
|
82
|
+
repo.get_by_id(row[:_id], options)
|
83
|
+
end
|
88
84
|
|
89
|
-
|
90
|
-
|
91
|
-
|
85
|
+
def get_with_dataset(options = {})
|
86
|
+
dataset = @dataset
|
87
|
+
dataset = yield @dataset if block_given?
|
88
|
+
(row = dataset.limit(1).first) && load_from_row(row, options)
|
89
|
+
end
|
92
90
|
|
91
|
+
def get_by_id(id, options = {})
|
92
|
+
get_with_dataset(options) { |ds| ds.filter(@id_column => id) }
|
93
|
+
end
|
93
94
|
|
95
|
+
def get_many_by_ids(ids, options = {})
|
96
|
+
rows = @dataset.filter(@id_column => ids).all
|
97
|
+
load_from_rows(rows, options, ids)
|
98
|
+
end
|
94
99
|
|
100
|
+
def contains_id?(id)
|
101
|
+
!@dataset.filter(@id_column => id).select(1).limit(1).single_value.nil?
|
102
|
+
end
|
95
103
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
104
|
+
def store(object)
|
105
|
+
repo = @repos_for_model_classes[object.class] || (fail StdError)
|
106
|
+
repo.store(id, object)
|
107
|
+
end
|
100
108
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
109
|
+
def store_new(object)
|
110
|
+
repo = @repos_for_model_classes[object.class] || (fail StdError)
|
111
|
+
repo.store_new(id, object)
|
112
|
+
end
|
105
113
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
114
|
+
def update(entity, update_entity)
|
115
|
+
repo = @repos_for_model_classes[entity.class] || (fail StdError)
|
116
|
+
repo.update(entity, update_entity)
|
117
|
+
end
|
110
118
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
119
|
+
def update_by_id(id, update_entity)
|
120
|
+
repo = @repos_for_model_classes[update_entity.class] || (fail StdError)
|
121
|
+
repo.update_by_id(id, update_entity)
|
122
|
+
end
|
115
123
|
|
116
|
-
|
117
|
-
|
118
|
-
|
124
|
+
def delete(object)
|
125
|
+
repo = @repos_for_model_classes[object.class] || (fail StdError)
|
126
|
+
repo.delete(object)
|
127
|
+
end
|
119
128
|
end
|
120
129
|
end
|
121
130
|
end
|
@@ -1,138 +1,163 @@
|
|
1
|
-
module Hold
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
1
|
+
module Hold
|
2
|
+
module Sequel
|
3
|
+
# Abstract superclass.
|
4
|
+
#
|
5
|
+
# Responsibility of a PropertyMapper is to map data for a particular
|
6
|
+
# property of a model class, between the instances of that model class, and
|
7
|
+
# the database
|
8
|
+
class PropertyMapper
|
9
|
+
def self.setter_dependencies_for(_options = {})
|
10
|
+
{}
|
11
|
+
end
|
7
12
|
|
8
|
-
|
13
|
+
attr_reader :repository, :property_name, :property
|
9
14
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
15
|
+
# If you pass a block, it will be instance_evalled, allowing you to create
|
16
|
+
# one-off custom property mappers by overriding bits of this
|
17
|
+
# implementation in the block.
|
18
|
+
def initialize(repo, property_name, _options = nil, &block)
|
19
|
+
@repository = repo
|
20
|
+
@property_name = property_name
|
21
|
+
instance_eval(&block) if block
|
22
|
+
end
|
17
23
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
24
|
+
# columns: column names to include in a SELECT in order to select this
|
25
|
+
# property. These should be qualified with the relevant table name but not
|
26
|
+
# aliased.
|
27
|
+
#
|
28
|
+
# aliases: the above columns, aliased for use in the SELECT clause. be
|
29
|
+
# alias should something unique which the mapper can later use to retreive
|
30
|
+
# from a result row.
|
31
|
+
#
|
32
|
+
# Any tables which need to be present in the FROM clause in order to
|
33
|
+
# select the columns. Relevant joins will be constructed by the parent
|
34
|
+
# repo.
|
35
|
+
#
|
36
|
+
# A 'preferred_table' hint may be passed by the repo to indicate that it'd
|
37
|
+
# prefer you load the column off a particular table; at present this is
|
38
|
+
# only used by the IdentityMapper
|
39
|
+
def columns_aliases_and_tables_for_select(_preferred_table = nil)
|
40
|
+
[[], [], []]
|
41
|
+
end
|
32
42
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
+
# Obtains the value of this property from a sequel result row and/or
|
44
|
+
# identity value.
|
45
|
+
#
|
46
|
+
# where the mapper has columns_aliases_and_tables_for_select, it will get
|
47
|
+
# passed a result row object here which contains the sql values for these
|
48
|
+
# columns (amongst others potentially)
|
49
|
+
#
|
50
|
+
# Where the identity value is available it will also be passed.
|
51
|
+
#
|
52
|
+
# One or other of id, row must always be passed.
|
53
|
+
def load_value(_row = nil, _id = nil, _properties = nil)
|
54
|
+
end
|
43
55
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
56
|
+
# called inside the INSERT transaction for insertion of the given entity.
|
57
|
+
#
|
58
|
+
# this is called first thing before insert rows are built (via
|
59
|
+
# build_insert_row) for each table of the repo.
|
60
|
+
def pre_insert(_entity)
|
61
|
+
end
|
50
62
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
63
|
+
# called inside the UPDATE transaction for insertion of the given entity.
|
64
|
+
#
|
65
|
+
# this is called first thing before update rows are built (via
|
66
|
+
# build_update_row) for each table of the repo.
|
67
|
+
#
|
68
|
+
# anything returned from pre_update will be passed to post_update's
|
69
|
+
# data_from_pre_update arg if the update succeeds.
|
70
|
+
def pre_update(_entity, _update_entity)
|
71
|
+
end
|
60
72
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
73
|
+
# called inside the DELETE transaction for a given entity.
|
74
|
+
#
|
75
|
+
# this is called first thing before rows are deleted for each table of the
|
76
|
+
# repo.
|
77
|
+
def pre_delete(_entity)
|
78
|
+
end
|
66
79
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
80
|
+
# called inside the DELETE transaction for a given entity.
|
81
|
+
#
|
82
|
+
# this is called last thing after rows are deleted for each table of the
|
83
|
+
# repo.
|
84
|
+
def post_delete(_entity)
|
85
|
+
end
|
72
86
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
87
|
+
# gets this property off the entity, and sets associated keys on a sequel
|
88
|
+
# row hash for insertion into the given table. May be passed an ID if an
|
89
|
+
# last_insert_id id value for the entity was previously obtained from an
|
90
|
+
# ID sequence on insertion into another table as part of the same combined
|
91
|
+
# entity store_new.
|
92
|
+
#
|
93
|
+
# this is called inside the transaction which wraps the insert, so this is
|
94
|
+
# effectively your pre-insert hook and you can safely do other things
|
95
|
+
# inside it in the knowledge they'll be rolled back in the event of a
|
96
|
+
# subsequent problem.
|
97
|
+
def build_insert_row(_entity, _table, _row, _id = nil)
|
98
|
+
end
|
83
99
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
100
|
+
# gets this property off the update_entity, and sets associated keys on a
|
101
|
+
# sequel row hash for update of the given table for the given entity.
|
102
|
+
#
|
103
|
+
# as with build_update_row, this is done inside the update transaction,
|
104
|
+
# it's effectively your pre-update hook.
|
105
|
+
def build_update_row(_update_entity, _table, _row)
|
106
|
+
end
|
91
107
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
108
|
+
# used to make a sequel filter condition setting relevant columns equal to
|
109
|
+
# values equivalent to the given property value. May raise if mapper
|
110
|
+
# doesn't support this
|
111
|
+
def make_filter(_value, _columns_mapped_to)
|
112
|
+
fail Hold::UnsupportedOperation
|
113
|
+
end
|
97
114
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
115
|
+
# As for make_filter but takes multiple possible values and does a column
|
116
|
+
# IN (1,2,3,4) type thing.
|
117
|
+
def make_multi_filter(_values, _columns_mapped_to)
|
118
|
+
fail Hold::UnsupportedOperation
|
119
|
+
end
|
102
120
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
rows
|
109
|
-
|
110
|
-
|
121
|
+
# like load_value, but works in a batched fashion, allowing a batched
|
122
|
+
# loading strategy to be used for associated objects.
|
123
|
+
# takes a block and yields the loaded values one at a time to it together
|
124
|
+
# with their index
|
125
|
+
def load_values(rows = nil, ids = nil, properties = nil)
|
126
|
+
if rows
|
127
|
+
rows.each_with_index do |row, i|
|
128
|
+
yield load_value(row, ids && ids[i], properties), i
|
129
|
+
end
|
130
|
+
else
|
131
|
+
ids.each_with_index do |id, i|
|
132
|
+
yield load_value(nil, id, properties), i
|
133
|
+
end
|
134
|
+
end
|
111
135
|
end
|
112
|
-
end
|
113
136
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
137
|
+
# called after rows built via build_insert_row have successfully been used
|
138
|
+
# in a INSERT for the entity passed. Should update the entity property,
|
139
|
+
# where appropriate, with any default values which were supplied by the
|
140
|
+
# repository (via default_for) on insert, and should do any additional
|
141
|
+
# work in order to save any values which are not mapped to columns on one
|
142
|
+
# of the repo's own :tables
|
143
|
+
#
|
144
|
+
# Is also passed the last_insert_id resulting from any insert, to help
|
145
|
+
# fill out any autoincrement primary key column.
|
146
|
+
#
|
147
|
+
# is executed inside the same transaction as the INSERT
|
148
|
+
def post_insert(_entity, _rows, _last_insert_id = nil)
|
149
|
+
end
|
126
150
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
151
|
+
# called after rows built via build_update_row have successfully been used
|
152
|
+
# in a UPDATE for the id and update_entity passed. Should update the
|
153
|
+
# entity property, where appropriate, with any default values which were
|
154
|
+
# supplied by the repository (via default_for) on update, and should do
|
155
|
+
# any additional work in order to save any values which are not mapped to
|
156
|
+
# columns on one of the repo's own :tables
|
157
|
+
#
|
158
|
+
# is executed inside the same transaction as the UPDATE
|
159
|
+
def post_update(_entity, _update_entity, _rows, _data_from_pre_update)
|
160
|
+
end
|
135
161
|
end
|
136
|
-
|
137
162
|
end
|
138
163
|
end
|