sequel_dm 0.0.2
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.
- data/.gitignore +5 -0
- data/.travis.yml +3 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +51 -0
- data/LICENSE.txt +22 -0
- data/README.md +26 -0
- data/Rakefile +1 -0
- data/lib/sequel_dm/args_validator.rb +76 -0
- data/lib/sequel_dm/dao.rb +353 -0
- data/lib/sequel_dm/extensions/select_fields.rb +47 -0
- data/lib/sequel_dm/mapper.rb +54 -0
- data/lib/sequel_dm/mappings_dsl.rb +69 -0
- data/lib/sequel_dm/version.rb +3 -0
- data/lib/sequel_dm.rb +6 -0
- data/sequel_dm.gemspec +25 -0
- data/spec/sequel_dm/dao_spec.rb +21 -0
- data/spec/sequel_dm/mapper_spec.rb +63 -0
- data/spec/spec_helper.rb +15 -0
- metadata +139 -0
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
sequel_dm (0.0.2)
|
5
|
+
activesupport
|
6
|
+
sequel (~> 4.6)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
activesupport (4.0.2)
|
12
|
+
i18n (~> 0.6, >= 0.6.4)
|
13
|
+
minitest (~> 4.2)
|
14
|
+
multi_json (~> 1.3)
|
15
|
+
thread_safe (~> 0.1)
|
16
|
+
tzinfo (~> 0.3.37)
|
17
|
+
atomic (1.1.14)
|
18
|
+
columnize (0.3.6)
|
19
|
+
debugger (1.6.5)
|
20
|
+
columnize (>= 0.3.1)
|
21
|
+
debugger-linecache (~> 1.2.0)
|
22
|
+
debugger-ruby_core_source (~> 1.3.1)
|
23
|
+
debugger-linecache (1.2.0)
|
24
|
+
debugger-ruby_core_source (1.3.1)
|
25
|
+
diff-lcs (1.2.5)
|
26
|
+
i18n (0.6.9)
|
27
|
+
minitest (4.7.5)
|
28
|
+
multi_json (1.8.4)
|
29
|
+
rake (10.1.1)
|
30
|
+
rspec (2.14.1)
|
31
|
+
rspec-core (~> 2.14.0)
|
32
|
+
rspec-expectations (~> 2.14.0)
|
33
|
+
rspec-mocks (~> 2.14.0)
|
34
|
+
rspec-core (2.14.7)
|
35
|
+
rspec-expectations (2.14.4)
|
36
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
37
|
+
rspec-mocks (2.14.4)
|
38
|
+
sequel (4.6.0)
|
39
|
+
thread_safe (0.1.3)
|
40
|
+
atomic
|
41
|
+
tzinfo (0.3.38)
|
42
|
+
|
43
|
+
PLATFORMS
|
44
|
+
ruby
|
45
|
+
|
46
|
+
DEPENDENCIES
|
47
|
+
bundler (~> 1.3)
|
48
|
+
debugger
|
49
|
+
rake
|
50
|
+
rspec
|
51
|
+
sequel_dm!
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Albert Gazizov
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# SequelDM [](https://travis-ci.org/AlbertGazizov/sequel_dm) [](https://codeclimate.com/github/AlbertGazizov/sequel_dm)
|
2
|
+
|
3
|
+
|
4
|
+
|
5
|
+
SequelDM is a Sequel based Data Mapper pattern implementation
|
6
|
+
|
7
|
+
NOTE: This gem is not production ready, it needs a lot of work!
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
gem 'sequel_dm', github: 'AlbertGazizov/sequel_dm'
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
## Contributing
|
20
|
+
|
21
|
+
1. Fork it
|
22
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
23
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
24
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
25
|
+
5. Create new Pull Request
|
26
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# Helper class for arguments validation
|
2
|
+
module SequelDM::ArgsValidator
|
3
|
+
class << self
|
4
|
+
|
5
|
+
# Checks that specifid +obj+ is a symbol
|
6
|
+
# @param obj some object
|
7
|
+
# @param obj_name object's name, used to clarify error causer in exception
|
8
|
+
def is_symbol!(obj, obj_name)
|
9
|
+
unless obj.is_a?(Symbol)
|
10
|
+
raise ArgumentError, "#{obj_name} should be a Symbol"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Checks that specifid +obj+ is an Array
|
15
|
+
# @param obj some object
|
16
|
+
# @param obj_name object's name, used to clarify error causer in exception
|
17
|
+
def is_array!(obj, obj_name)
|
18
|
+
unless obj.is_a?(Array)
|
19
|
+
raise ArgumentError, "#{obj_name} should be an Array"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Checks that specifid +obj+ is a Hash
|
24
|
+
# @param obj some object
|
25
|
+
# @param obj_name object's name, used to clarify error causer in exception
|
26
|
+
def is_hash!(obj, obj_name)
|
27
|
+
unless obj.is_a?(Hash)
|
28
|
+
raise ArgumentError, "#{obj_name} should be a Hash"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Checks that specifid +obj+ is a Class
|
33
|
+
# @param obj some object
|
34
|
+
# @param obj_name object's name, used to clarify error causer in exception
|
35
|
+
def is_class!(obj, obj_name)
|
36
|
+
unless obj.is_a?(Class)
|
37
|
+
raise ArgumentError, "#{obj_name} should be a Class"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Checks that specifid +obj+ is a Proc
|
42
|
+
# @param obj some object
|
43
|
+
# @param obj_name object's name, used to clarify error causer in exception
|
44
|
+
def is_proc!(obj, obj_name)
|
45
|
+
unless obj.is_a?(Proc)
|
46
|
+
raise ArgumentError, "#{obj_name} should be a Proc"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Checks that specifid +obj+ is a symbol or Class
|
51
|
+
# @param obj some object
|
52
|
+
# @param obj_name object's name, used to clarify error causer in exception
|
53
|
+
def is_symbol_or_class!(obj, obj_name)
|
54
|
+
if !obj.is_a?(Symbol) && !obj.is_a?(Class)
|
55
|
+
raise ArgumentError, "#{obj_name} should be a Symbol or Class"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Checks that specifid +hash+ has a specified +key+
|
60
|
+
# @param hash some hash
|
61
|
+
# @param key hash's key
|
62
|
+
def has_key!(hash, key)
|
63
|
+
unless hash.has_key?(key)
|
64
|
+
raise ArgumentError, "#{hash} should has #{key} key"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Checks that specified +block+ is given
|
69
|
+
# @param block some block
|
70
|
+
def block_given!(block)
|
71
|
+
unless block
|
72
|
+
raise ArgumentError, "Block should be given"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,353 @@
|
|
1
|
+
require 'sequel_dm/extensions/select_fields'
|
2
|
+
|
3
|
+
SequelDM::DAO = Class.new(Sequel::Model)
|
4
|
+
module SequelDM
|
5
|
+
def self.DAO(source)
|
6
|
+
Class.new(SequelDM::DAO).set_dataset(source)
|
7
|
+
end
|
8
|
+
|
9
|
+
class DAO
|
10
|
+
class_attribute :mapper
|
11
|
+
|
12
|
+
dataset_module do
|
13
|
+
include SequelDM::Extensions::SelectFields
|
14
|
+
end
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def def_one_to_many(opts)
|
18
|
+
one_to_one = opts[:type] == :one_to_one
|
19
|
+
name = opts[:name]
|
20
|
+
model = self
|
21
|
+
key = (opts[:key] ||= opts.default_key)
|
22
|
+
km = opts[:key_method] ||= opts[:key]
|
23
|
+
cks = opts[:keys] = Array(key)
|
24
|
+
opts[:key_methods] = Array(opts[:key_method])
|
25
|
+
primary_key = (opts[:primary_key] ||= self.primary_key)
|
26
|
+
opts[:eager_loader_key] = primary_key unless opts.has_key?(:eager_loader_key)
|
27
|
+
cpks = opts[:primary_keys] = Array(primary_key)
|
28
|
+
pkc = opts[:primary_key_column] ||= primary_key
|
29
|
+
pkcs = opts[:primary_key_columns] ||= Array(pkc)
|
30
|
+
raise(Error, "mismatched number of keys: #{cks.inspect} vs #{cpks.inspect}") unless cks.length == cpks.length
|
31
|
+
uses_cks = opts[:uses_composite_keys] = cks.length > 1
|
32
|
+
slice_range = opts.slice_range
|
33
|
+
opts[:dataset] ||= proc do
|
34
|
+
opts.associated_dataset.where(opts.predicate_keys.zip(cpks.map{|k| send(k)}))
|
35
|
+
end
|
36
|
+
opts[:eager_loader] = proc do |eo|
|
37
|
+
h = eo[:id_map]
|
38
|
+
rows = eo[:rows]
|
39
|
+
reciprocal = opts.reciprocal
|
40
|
+
klass = opts.associated_class
|
41
|
+
filter_keys = opts.predicate_key
|
42
|
+
ds = model.eager_loading_dataset(opts, klass.where(filter_keys=>h.keys), nil, eo[:associations], eo)
|
43
|
+
assign_singular = true if one_to_one
|
44
|
+
case opts.eager_limit_strategy
|
45
|
+
when :distinct_on
|
46
|
+
ds = ds.distinct(*filter_keys).order_prepend(*filter_keys)
|
47
|
+
when :window_function
|
48
|
+
delete_rn = true
|
49
|
+
rn = ds.row_number_column
|
50
|
+
ds = apply_window_function_eager_limit_strategy(ds, opts)
|
51
|
+
when :ruby
|
52
|
+
assign_singular = false if one_to_one && slice_range
|
53
|
+
end
|
54
|
+
ds.all do |assoc_record|
|
55
|
+
assoc_record.values.delete(rn) if delete_rn
|
56
|
+
hash_key = uses_cks ? km.map{|k| assoc_record.send(k)} : assoc_record.send(km)
|
57
|
+
next unless objects = h[hash_key]
|
58
|
+
if assign_singular
|
59
|
+
objects.each do |object|
|
60
|
+
unless object.send(name)
|
61
|
+
# TODO: add persistance_associations update here
|
62
|
+
object.send("#{name}=", assoc_record)
|
63
|
+
assoc_record.send("#{reciprocal}=", object) if reciprocal
|
64
|
+
end
|
65
|
+
end
|
66
|
+
else
|
67
|
+
objects.each do |object|
|
68
|
+
add_to_associations_state(object, name, assoc_record)
|
69
|
+
object.send(name).push(assoc_record)
|
70
|
+
assoc_record.send("#{reciprocal}=", object) if reciprocal
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
if opts.eager_limit_strategy == :ruby
|
75
|
+
if one_to_one
|
76
|
+
if slice_range
|
77
|
+
rows.each{|o| o.associations[name] = o.associations[name][slice_range.begin]}
|
78
|
+
end
|
79
|
+
else
|
80
|
+
rows.each{|o| o.associations[name] = o.associations[name][slice_range] || []}
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
super
|
85
|
+
end
|
86
|
+
|
87
|
+
def set_dataset_row_proc(ds)
|
88
|
+
ds.row_proc = Proc.new do |raw|
|
89
|
+
raise StandardError, "Mapper should be specified" if !self.mapper
|
90
|
+
entity = self.mapper.to_entity(raw)
|
91
|
+
save_state(entity, raw)
|
92
|
+
entity
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def set_mapper(mapper)
|
97
|
+
SequelDM::ArgsValidator.is_class!(mapper, :mapper)
|
98
|
+
self.mapper = mapper
|
99
|
+
end
|
100
|
+
|
101
|
+
# Database methods
|
102
|
+
|
103
|
+
def insert(entity, root = nil)
|
104
|
+
raw = mapper.to_hash(entity, root)
|
105
|
+
key = dataset.insert(raw)
|
106
|
+
set_entity_primary_key(entity, raw, key)
|
107
|
+
save_state(entity, raw)
|
108
|
+
insert_associations(entity)
|
109
|
+
entity
|
110
|
+
end
|
111
|
+
|
112
|
+
def insert_all(entities, root = nil)
|
113
|
+
entities.each do |entity|
|
114
|
+
insert(entity, root)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def update(entity, root = nil)
|
119
|
+
raw = mapper.to_hash(entity, root)
|
120
|
+
raw = select_only_changed_values(entity, raw)
|
121
|
+
|
122
|
+
unless raw.empty?
|
123
|
+
update_state(entity, raw)
|
124
|
+
|
125
|
+
key_condition = prepare_key_condition_from_entity(entity)
|
126
|
+
dataset.where(key_condition).update(raw)
|
127
|
+
end
|
128
|
+
|
129
|
+
insert_or_update_associations(entity)
|
130
|
+
entity
|
131
|
+
end
|
132
|
+
|
133
|
+
def update_all(entities, root = nil)
|
134
|
+
entities.each do |entity|
|
135
|
+
update(entity, root)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def save(entity, root = nil)
|
140
|
+
if has_persistance_state?(entity)
|
141
|
+
update(entity, root)
|
142
|
+
else
|
143
|
+
insert(entity, root)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def save_all(entities, root = nil)
|
148
|
+
entities.each do |entity|
|
149
|
+
save(entity, root)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def delete(entity)
|
154
|
+
key_condition = prepare_key_condition_from_entity(entity)
|
155
|
+
dataset.where(key_condition).delete
|
156
|
+
delete_associations(entity)
|
157
|
+
end
|
158
|
+
|
159
|
+
# TODO: refactor
|
160
|
+
def delete_all(entities)
|
161
|
+
entity_ids = entities.map(&:id)
|
162
|
+
dataset.where(id: entity_ids).delete
|
163
|
+
unless association_reflections.empty?
|
164
|
+
association_reflections.each do |association, options|
|
165
|
+
association_dao = options[:class]
|
166
|
+
conditions = (options[:conditions] || {}).merge(options[:key] => entity_ids)
|
167
|
+
association_dao.where(conditions).delete
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
private
|
173
|
+
|
174
|
+
def select_only_changed_values(entity, hash)
|
175
|
+
changes = {}
|
176
|
+
return hash unless entity.instance_variable_defined?(:@persistance_state)
|
177
|
+
|
178
|
+
persistance_state = entity.instance_variable_get(:@persistance_state)
|
179
|
+
hash.each do |column, value|
|
180
|
+
previous_column_value = persistance_state[column]
|
181
|
+
if persistance_state.has_key?(column) && column_value_changed?(previous_column_value, value)
|
182
|
+
changes[column] = value
|
183
|
+
end
|
184
|
+
end
|
185
|
+
changes
|
186
|
+
end
|
187
|
+
|
188
|
+
def column_value_changed?(previous_value, new_value)
|
189
|
+
previous_value != new_value
|
190
|
+
end
|
191
|
+
|
192
|
+
def save_state(entity, raw)
|
193
|
+
if !entity.is_a?(Integer) && !entity.is_a?(Symbol)
|
194
|
+
entity.instance_variable_set(:@persistance_state, raw)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def update_state(entity, raw)
|
199
|
+
persistance_state = entity.instance_variable_get(:@persistance_state)
|
200
|
+
if persistance_state
|
201
|
+
persistance_state.merge!(raw)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def has_persistance_state?(entity)
|
206
|
+
!!entity.instance_variable_get(:@persistance_state)
|
207
|
+
end
|
208
|
+
|
209
|
+
def set_associations_state(entity, association_name, associations)
|
210
|
+
persistance_associations = entity.instance_variable_get(:@persistance_associations) || {}
|
211
|
+
persistance_associations[association_name] ||= []
|
212
|
+
persistance_associations[association_name] |= associations
|
213
|
+
entity.instance_variable_set(:@persistance_associations, persistance_associations)
|
214
|
+
end
|
215
|
+
|
216
|
+
def add_to_associations_state(entity, association_name, association)
|
217
|
+
persistance_associations = entity.instance_variable_get(:@persistance_associations) || {}
|
218
|
+
persistance_associations[association_name] ||= []
|
219
|
+
persistance_associations[association_name] << association
|
220
|
+
entity.instance_variable_set(:@persistance_associations, persistance_associations)
|
221
|
+
end
|
222
|
+
|
223
|
+
def prepare_key_condition_from_entity(entity)
|
224
|
+
key_condition = {}
|
225
|
+
if primary_key.is_a?(Array)
|
226
|
+
primary_key.each do |key_part|
|
227
|
+
key_part_value = entity.send(key_part)
|
228
|
+
raise ArgumentError, "entity's primary key can't be nil, got nil for #{key_part}" unless key_part_value
|
229
|
+
key_condition[key_part] = key_part_value
|
230
|
+
end
|
231
|
+
elsif primary_key.is_a?(Symbol)
|
232
|
+
key_value = entity.send(primary_key)
|
233
|
+
raise ArgumentError, "entity's primary key can't be nil, got nil for #{primary_key}" unless key_value
|
234
|
+
key_condition[primary_key] = key_value
|
235
|
+
else
|
236
|
+
raise StandardError, "primary key should be array or symbol"
|
237
|
+
end
|
238
|
+
key_condition
|
239
|
+
end
|
240
|
+
|
241
|
+
def set_entity_primary_key(entity, raw, key)
|
242
|
+
if key && !primary_key.is_a?(Array)
|
243
|
+
entity.instance_variable_set("@#{primary_key}", key)
|
244
|
+
raw[primary_key] = key
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def insert_associations(entity)
|
249
|
+
unless association_reflections.empty?
|
250
|
+
association_reflections.each do |association_name, options|
|
251
|
+
association_dao = options[:class]
|
252
|
+
if entity.respond_to?(association_name)
|
253
|
+
children = association_dao.insert_all(entity.send(association_name), entity)
|
254
|
+
set_associations_state(entity, association_name, children)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def insert_or_update_associations(entity)
|
261
|
+
unless association_reflections.empty?
|
262
|
+
association_reflections.each do |association_name, options|
|
263
|
+
association_dao = options[:class]
|
264
|
+
raise ArgumentError, "class option should be specified for #{association_name}" unless association_dao
|
265
|
+
|
266
|
+
delete_dissapeared_children(entity, association_name, options)
|
267
|
+
|
268
|
+
children = entity.send(association_name)
|
269
|
+
association_dao.save_all(children, entity)
|
270
|
+
set_associations_state(entity, association_name, children)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def delete_associations(entity)
|
276
|
+
unless association_reflections.empty?
|
277
|
+
association_reflections.each do |association, options|
|
278
|
+
if options[:delete]
|
279
|
+
association_dao = options[:class]
|
280
|
+
conditions = (options[:conditions] || {}).merge(options[:key] => entity.send(primary_key))
|
281
|
+
association_dao.where(conditions).delete
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
def delete_dissapeared_children(entity, association, options)
|
288
|
+
association_dao = options[:class]
|
289
|
+
unless options[:key]
|
290
|
+
raise ArgumentError, "key option should be specified for #{association}"
|
291
|
+
end
|
292
|
+
if options[:key].is_a?(Symbol)
|
293
|
+
conditions = (options[:conditions] || {}).merge(options[:key] => entity.send(primary_key))
|
294
|
+
elsif options[:key].is_a?(Array)
|
295
|
+
conditions = options[:key].inject(options[:conditions] || {}) { |result, key| result[key] = entity.send(key); result }
|
296
|
+
else
|
297
|
+
raise ArgumentError, "key should be symbol or array"
|
298
|
+
end
|
299
|
+
|
300
|
+
# get ids of removed children
|
301
|
+
association_objects = get_association_objects(entity, association)
|
302
|
+
dissapeared_objects = association_objects - entity.send(association)
|
303
|
+
|
304
|
+
scope_key = options[:scope_key] || association_dao.primary_key
|
305
|
+
if scope_key.is_a?(Symbol)
|
306
|
+
child_keys = { scope_key => [] }
|
307
|
+
dissapeared_objects.each do |child_object|
|
308
|
+
key = child_object.send(scope_key)
|
309
|
+
child_keys[scope_key] << key
|
310
|
+
end
|
311
|
+
|
312
|
+
if !child_keys[scope_key].empty?
|
313
|
+
association_dao.where(conditions).where(child_keys).delete
|
314
|
+
end
|
315
|
+
elsif scope_key.is_a?(Array)
|
316
|
+
child_keys = []
|
317
|
+
dissapeared_objects.each do |child_object|
|
318
|
+
child_keys << scope_key.inject({}) do |condition, key|
|
319
|
+
condition[key] = child_object.send(key)
|
320
|
+
condition
|
321
|
+
end
|
322
|
+
end
|
323
|
+
if !child_keys.empty?
|
324
|
+
child_keys.each { |keys| keys.merge!(conditions) }
|
325
|
+
association_dao.where(Sequel.|(*child_keys)).delete
|
326
|
+
end
|
327
|
+
elsif scope_key.is_a?(Proc)
|
328
|
+
child_keys = []
|
329
|
+
dissapeared_objects.each do |child_object|
|
330
|
+
child_keys << scope_key.call(child_object)
|
331
|
+
end
|
332
|
+
if !child_keys.empty?
|
333
|
+
child_keys.each { |keys| keys.merge!(conditions) }
|
334
|
+
association_dao.where(Sequel.|(*child_keys)).delete
|
335
|
+
end
|
336
|
+
else
|
337
|
+
raise StandardError, "scope key should be array or symbol"
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
def get_association_objects(entity, association)
|
342
|
+
persistance_associations = entity.instance_variable_get(:@persistance_associations)
|
343
|
+
if persistance_associations
|
344
|
+
persistance_associations[association] || []
|
345
|
+
else
|
346
|
+
[]
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module SequelDM
|
2
|
+
module Extensions
|
3
|
+
module SelectFields
|
4
|
+
def select_fields(fields)
|
5
|
+
if fields.empty?
|
6
|
+
if !model.association_reflections.empty?
|
7
|
+
eager(model.association_reflections.keys)
|
8
|
+
else
|
9
|
+
self
|
10
|
+
end
|
11
|
+
else
|
12
|
+
eager_associations = {}
|
13
|
+
fields.each do |association, columns|
|
14
|
+
next if association == :fields
|
15
|
+
if columns && !columns.is_a?(Array)
|
16
|
+
columns = get_columns_from_mapper(association)
|
17
|
+
end
|
18
|
+
if columns
|
19
|
+
table_name = model.association_reflections[association][:class].table_name
|
20
|
+
columns = columns.map { |column| :"#{table_name}__#{column}___#{column}" }
|
21
|
+
eager_associations[association] = proc{|ds| ds.select(*columns) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
if fields[:fields].is_a?(Array)
|
26
|
+
columns = fields[:fields]
|
27
|
+
else
|
28
|
+
columns = model.mapper.mappings.keys
|
29
|
+
end
|
30
|
+
columns = columns.map { |column| :"#{model.table_name}__#{column}___#{column}" }
|
31
|
+
eager(eager_associations).select(*columns)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def get_columns_from_mapper(association)
|
38
|
+
reflection = model.association_reflections[association]
|
39
|
+
raise ArgumentError, "association with name #{association} is not defined in dao" unless reflection
|
40
|
+
association_dao = reflection[:class]
|
41
|
+
raise ArgumentError, "association #{association} should have class option" unless association_dao
|
42
|
+
association_dao.mapper.mappings.keys
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'sequel_dm/args_validator'
|
2
|
+
require 'sequel_dm/mappings_dsl'
|
3
|
+
|
4
|
+
module SequelDM::Mapper
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
class_attribute :entity_class, :mappings
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def map(entity_class, &mappings_proc)
|
13
|
+
SequelDM::ArgsValidator.is_class!(entity_class, :entity_class)
|
14
|
+
self.entity_class = entity_class
|
15
|
+
self.mappings = SequelDM::MappingsDSL.new(&mappings_proc).mappings
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_entity(hash)
|
19
|
+
attributes = {}
|
20
|
+
entity = self.entity_class.new
|
21
|
+
hash.each do |key, value|
|
22
|
+
if mapping = self.mappings[key]
|
23
|
+
entity.instance_variable_set(:"@#{mapping.entity_field}", to_attribute(hash, value, mapping)) if mapping.set_field?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
entity
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_hash(entity, *args)
|
30
|
+
hash = {}
|
31
|
+
|
32
|
+
entity_mappings = self.mappings
|
33
|
+
entity_mappings.each do |column, mapping|
|
34
|
+
value = to_column(entity, mapping, *args)
|
35
|
+
hash[column] = value if mapping.set_column?
|
36
|
+
end
|
37
|
+
|
38
|
+
hash
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def to_attribute(hash, value, mapping)
|
44
|
+
mapping.load? ? mapping.load(hash) : value
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_column(entity, mapping, *args)
|
48
|
+
mapping.dump? ? mapping.dump(entity, *args) : entity.send(mapping.entity_field)
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
@@ -0,0 +1,69 @@
|
|
1
|
+
class SequelDM::MappingsDSL
|
2
|
+
attr_reader :mappings
|
3
|
+
def initialize(&dsl_block)
|
4
|
+
@mappings = {}
|
5
|
+
instance_exec(&dsl_block)
|
6
|
+
end
|
7
|
+
|
8
|
+
def column(column_name, options = {})
|
9
|
+
SequelDM::ArgsValidator.is_symbol!(column_name, :column_name)
|
10
|
+
SequelDM::ArgsValidator.is_hash!(options, :column_options)
|
11
|
+
SequelDM::ArgsValidator.is_symbol!(options[:to], :to) if options[:to]
|
12
|
+
SequelDM::ArgsValidator.is_proc!(options[:load], :load) if options[:load]
|
13
|
+
SequelDM::ArgsValidator.is_proc!(options[:dump], :dump) if options[:dump]
|
14
|
+
|
15
|
+
set_field = options[:set_field] == false ? false : true
|
16
|
+
set_column = options[:set_column] == false ? false : true
|
17
|
+
mappings[column_name] = Mapping.new(
|
18
|
+
column_name,
|
19
|
+
options[:to] || column_name,
|
20
|
+
options[:load],
|
21
|
+
options[:dump],
|
22
|
+
set_field,
|
23
|
+
set_column,
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
def columns(*column_names)
|
28
|
+
SequelDM::ArgsValidator.is_array!(column_names, :column_names)
|
29
|
+
column_names.each { |column_name| column(column_name) }
|
30
|
+
end
|
31
|
+
|
32
|
+
class Mapping
|
33
|
+
attr_accessor :column_name, :entity_field, :load, :dump
|
34
|
+
|
35
|
+
def initialize(column_name, entity_field, load = nil, dump = nil, set_field = true, set_column = true)
|
36
|
+
@column_name = column_name
|
37
|
+
@entity_field = entity_field
|
38
|
+
@load = load
|
39
|
+
@dump = dump
|
40
|
+
@set_field = set_field
|
41
|
+
@set_column = set_column
|
42
|
+
end
|
43
|
+
|
44
|
+
def set_field?
|
45
|
+
@set_field
|
46
|
+
end
|
47
|
+
|
48
|
+
def set_column?
|
49
|
+
@set_column
|
50
|
+
end
|
51
|
+
|
52
|
+
def load?
|
53
|
+
!!@load
|
54
|
+
end
|
55
|
+
|
56
|
+
def dump?
|
57
|
+
!!@dump
|
58
|
+
end
|
59
|
+
|
60
|
+
def load(value)
|
61
|
+
@load.call(value)
|
62
|
+
end
|
63
|
+
|
64
|
+
def dump(value, *args)
|
65
|
+
@dump.call(value, *args)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
data/lib/sequel_dm.rb
ADDED
data/sequel_dm.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'sequel_dm/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "sequel_dm"
|
8
|
+
spec.version = SequelDM::VERSION
|
9
|
+
spec.authors = ["Albert Gazizov", "Ruslan Gatiyatov"]
|
10
|
+
spec.email = ["deeper4k@gmail.com", "ruslan.gatiyatov@gmail.com"]
|
11
|
+
spec.description = %q{Sequel based Data Mapper implementation}
|
12
|
+
spec.summary = %q{Sequel based Data Mapper implementation}
|
13
|
+
spec.homepage = "http://github.com/deeper4k/sequel_dm"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(spec)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "activesupport"
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
23
|
+
spec.add_development_dependency "rake"
|
24
|
+
spec.add_dependency "sequel", "~> 4.6"
|
25
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'sequel_dm'
|
3
|
+
|
4
|
+
describe "SequelDM::DAO" do
|
5
|
+
class Entity
|
6
|
+
end
|
7
|
+
|
8
|
+
class Mapper
|
9
|
+
def self.to_entity(hash)
|
10
|
+
Entity.new
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe ".dataset.row_proc" do
|
15
|
+
it "should return entity" do
|
16
|
+
dao = Class.new(SequelDM::DAO(:items))
|
17
|
+
dao.mapper = Mapper
|
18
|
+
dao.dataset.row_proc.call({}).should be_instance_of(Entity)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SequelDM::Mapper do
|
4
|
+
module MapperTest
|
5
|
+
|
6
|
+
class Event
|
7
|
+
attr_accessor :name, :description, :when, :settings
|
8
|
+
|
9
|
+
def initialize(attrs = {})
|
10
|
+
@name = attrs[:name]
|
11
|
+
@description = attrs[:description]
|
12
|
+
@when = attrs[:when]
|
13
|
+
@settings = attrs[:settings ]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class EventMapper
|
18
|
+
include SequelDM::Mapper
|
19
|
+
|
20
|
+
map MapperTest::Event do
|
21
|
+
column :subject, to: :name
|
22
|
+
column :description
|
23
|
+
column :settings, load: ->(hash) { YAML.load(hash[:settings]) }, dump: ->(event) { YAML.dump(event.settings) }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe ".to_entity" do
|
29
|
+
it "should build Event instance from hash using mappings" do
|
30
|
+
tomorrow = Time.now + 24*60*60
|
31
|
+
event = MapperTest::EventMapper.to_entity({
|
32
|
+
subject: "Meet parents",
|
33
|
+
description: "I need to meet them",
|
34
|
+
when: tomorrow,
|
35
|
+
settings: "---\n:important: true\n",
|
36
|
+
occurrences_number: 2,
|
37
|
+
})
|
38
|
+
event.should be_instance_of(MapperTest::Event)
|
39
|
+
|
40
|
+
event.name.should == "Meet parents"
|
41
|
+
event.description.should == "I need to meet them"
|
42
|
+
event.when.should == nil # unlisted column
|
43
|
+
event.settings.should == { important: true }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe ".to_hash" do
|
48
|
+
it "should convert entity to hash" do
|
49
|
+
event = MapperTest::Event.new({
|
50
|
+
name: "Event",
|
51
|
+
description: "Description",
|
52
|
+
when: Time.now,
|
53
|
+
settings: { important: true },
|
54
|
+
})
|
55
|
+
MapperTest::EventMapper.to_hash(event).should == {
|
56
|
+
description: "Description",
|
57
|
+
settings: "---\n:important: true\n",
|
58
|
+
subject: "Event"
|
59
|
+
}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'debugger'
|
4
|
+
require 'sequel'
|
5
|
+
require 'sequel_dm'
|
6
|
+
|
7
|
+
RSpec.configure do |config|
|
8
|
+
config.color_enabled = true
|
9
|
+
|
10
|
+
db = Sequel.mock(:fetch=>{:id => 1, :x => 1}, :numrows=>1, :autoid=>proc{|sql| 10})
|
11
|
+
def db.schema(*) [[:id, {:primary_key=>true}]] end
|
12
|
+
def db.reset() sqls end
|
13
|
+
def db.supports_schema_parsing?() true end
|
14
|
+
Sequel::Model.db = DB = db
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sequel_dm
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.2
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Albert Gazizov
|
9
|
+
- Ruslan Gatiyatov
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2014-04-08 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
prerelease: false
|
17
|
+
name: activesupport
|
18
|
+
type: :runtime
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ! '>='
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: '0'
|
24
|
+
none: false
|
25
|
+
requirement: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
none: false
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
prerelease: false
|
33
|
+
name: bundler
|
34
|
+
type: :development
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ~>
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.3'
|
40
|
+
none: false
|
41
|
+
requirement: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '1.3'
|
46
|
+
none: false
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
prerelease: false
|
49
|
+
name: rake
|
50
|
+
type: :development
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
none: false
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
none: false
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
prerelease: false
|
65
|
+
name: sequel
|
66
|
+
type: :runtime
|
67
|
+
version_requirements: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ~>
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '4.6'
|
72
|
+
none: false
|
73
|
+
requirement: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '4.6'
|
78
|
+
none: false
|
79
|
+
description: Sequel based Data Mapper implementation
|
80
|
+
email:
|
81
|
+
- deeper4k@gmail.com
|
82
|
+
- ruslan.gatiyatov@gmail.com
|
83
|
+
executables: []
|
84
|
+
extensions: []
|
85
|
+
extra_rdoc_files: []
|
86
|
+
files:
|
87
|
+
- .gitignore
|
88
|
+
- .travis.yml
|
89
|
+
- Gemfile
|
90
|
+
- Gemfile.lock
|
91
|
+
- LICENSE.txt
|
92
|
+
- README.md
|
93
|
+
- Rakefile
|
94
|
+
- lib/sequel_dm.rb
|
95
|
+
- lib/sequel_dm/args_validator.rb
|
96
|
+
- lib/sequel_dm/dao.rb
|
97
|
+
- lib/sequel_dm/extensions/select_fields.rb
|
98
|
+
- lib/sequel_dm/mapper.rb
|
99
|
+
- lib/sequel_dm/mappings_dsl.rb
|
100
|
+
- lib/sequel_dm/version.rb
|
101
|
+
- sequel_dm.gemspec
|
102
|
+
- spec/sequel_dm/dao_spec.rb
|
103
|
+
- spec/sequel_dm/mapper_spec.rb
|
104
|
+
- spec/spec_helper.rb
|
105
|
+
homepage: http://github.com/deeper4k/sequel_dm
|
106
|
+
licenses:
|
107
|
+
- MIT
|
108
|
+
post_install_message:
|
109
|
+
rdoc_options: []
|
110
|
+
require_paths:
|
111
|
+
- lib
|
112
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ! '>='
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
segments:
|
117
|
+
- 0
|
118
|
+
hash: -3775447022657045354
|
119
|
+
version: '0'
|
120
|
+
none: false
|
121
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
segments:
|
126
|
+
- 0
|
127
|
+
hash: -3775447022657045354
|
128
|
+
version: '0'
|
129
|
+
none: false
|
130
|
+
requirements: []
|
131
|
+
rubyforge_project:
|
132
|
+
rubygems_version: 1.8.23
|
133
|
+
signing_key:
|
134
|
+
specification_version: 3
|
135
|
+
summary: Sequel based Data Mapper implementation
|
136
|
+
test_files:
|
137
|
+
- spec/sequel_dm/dao_spec.rb
|
138
|
+
- spec/sequel_dm/mapper_spec.rb
|
139
|
+
- spec/spec_helper.rb
|