praxis-mapper 3.4.0 → 4.0
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/.travis.yml +2 -1
- data/CHANGELOG.md +20 -1
- data/lib/praxis-mapper.rb +7 -0
- data/lib/praxis-mapper/connection_factories/sequel.rb +66 -0
- data/lib/praxis-mapper/connection_factories/simple.rb +27 -0
- data/lib/praxis-mapper/connection_manager.rb +29 -50
- data/lib/praxis-mapper/identity_map.rb +37 -13
- data/lib/praxis-mapper/identity_map_extensions/persistence.rb +83 -0
- data/lib/praxis-mapper/query/base.rb +1 -1
- data/lib/praxis-mapper/query/sequel.rb +11 -0
- data/lib/praxis-mapper/sequel_compat.rb +99 -0
- data/lib/praxis-mapper/version.rb +1 -1
- data/praxis-mapper.gemspec +2 -0
- data/spec/factories/all.rb +32 -0
- data/spec/praxis-mapper/connection_factories/sequel_spec.rb +67 -0
- data/spec/praxis-mapper/connection_factories/simple_spec.rb +29 -0
- data/spec/praxis-mapper/connection_manager_spec.rb +49 -71
- data/spec/praxis-mapper/identity_map_extensions/persistence_spec.rb +122 -0
- data/spec/praxis-mapper/sequel_compat_spec.rb +106 -0
- data/spec/spec_helper.rb +19 -4
- data/spec/support/spec_sequel_models.rb +110 -0
- metadata +46 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 04a36d68c2385426a0b0aed2295c41299b0c3943
|
4
|
+
data.tar.gz: 5eca3e994f89a94f67a7c1f0d9ec3ed64c274793
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9604920ffc4719206ee8dd41cb2e3a642bb87f8efd64fa44c310d1e6db6f4a702a94c9965724bd98d27984336d745d06b131f766752b989ccfbeb127b754bf9e
|
7
|
+
data.tar.gz: 88a2a5c4c87f99701c955e4f1063e8d5169363f6250cf06b992b53f978581b6cc35cbd645c4d2c34229146e8ada75534ad5b338598038c870a2ace98f214b895
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,25 @@
|
|
1
1
|
# praxis-mapper changelog
|
2
2
|
|
3
|
-
##
|
3
|
+
## 4.0
|
4
|
+
|
5
|
+
* Optimization on handling repeated ids (especially noticeable when using subloads)
|
6
|
+
* Refactored `ConnectionManager` repository handling to improve integration with other connection-pooling (specifically Sequel's at present).
|
7
|
+
* Added two types of connection factory under `Praxis::Mapper::ConnectionFactories::`:
|
8
|
+
* `Simple`: Takes a `connection:` option to specify the raw object to return for all `checkout`. Also, preserves current behavior with proc-based uses when `ConnectionManager.repository` is given a block. This is the default factory type if one is not specified for the repository.
|
9
|
+
* `Sequel`: Takes `connection:` option to specify a `Sequel::Database`, or hash of options to pass to `Sequel.connect`.
|
10
|
+
* `IdentityMap#finalize!` now calls `ConnectionManager#release` to ensure any connections are returned to their respective pools, if applicable.
|
11
|
+
* Added `SequelCompat` module, which provides support for using `Sequel::Model` objects with an `IdentityMap` when included in model class.
|
12
|
+
* This overrides the association accessors on instances associated with an `IdentityMap` (see below for more on this) to query the map instead of database.
|
13
|
+
* See (spec/support/spec_sequel_models.rb) for example definition.
|
14
|
+
* Added prototype for write-path support to `IdentityMap` (for `Sequel::Model` models):
|
15
|
+
* `IdentityMap#attach(record)`: adds the record to the identity map, saving it to the database first if it does not have a value for its primary key (or other identities).
|
16
|
+
* `IdentityMap#detatch(record)`: removes the record from the identity map
|
17
|
+
* `IdentityMap#flush!(record_or_class=nil)`: depending on the value of `record_or_class` it:
|
18
|
+
* with no argument, or nil, given it saves all modified records in the identity map.
|
19
|
+
* with an instance of a model, it saves just that record.
|
20
|
+
* with a model class, it saves all modified records for that class in the identity map.
|
21
|
+
* `IdentityMap#remove`: calls `detatch` with the record, and then calls`record.delete` to delete it.
|
22
|
+
|
4
23
|
|
5
24
|
## 3.4.0
|
6
25
|
|
data/lib/praxis-mapper.rb
CHANGED
@@ -45,12 +45,19 @@ end
|
|
45
45
|
require 'praxis-mapper/finalizable'
|
46
46
|
require 'praxis-mapper/logging'
|
47
47
|
|
48
|
+
require 'praxis-mapper/identity_map_extensions/persistence'
|
48
49
|
require 'praxis-mapper/identity_map'
|
49
50
|
|
50
51
|
require 'praxis-mapper/model'
|
51
52
|
require 'praxis-mapper/query_statistics'
|
53
|
+
|
54
|
+
require 'praxis-mapper/sequel_compat'
|
55
|
+
|
52
56
|
require 'praxis-mapper/connection_manager'
|
53
57
|
|
58
|
+
require 'praxis-mapper/connection_factories/simple'
|
59
|
+
require 'praxis-mapper/connection_factories/sequel'
|
60
|
+
|
54
61
|
require 'praxis-mapper/resource'
|
55
62
|
|
56
63
|
require 'praxis-mapper/query/base'
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Praxis::Mapper
|
2
|
+
module ConnectionFactories
|
3
|
+
|
4
|
+
class Sequel
|
5
|
+
|
6
|
+
def initialize(connection:nil, **opts)
|
7
|
+
raise ArgumentError, 'May not provide both a connection and opts' if connection && !opts.empty?
|
8
|
+
|
9
|
+
if connection
|
10
|
+
@connection = connection
|
11
|
+
else
|
12
|
+
@connection = ::Sequel.connect(**opts)
|
13
|
+
end
|
14
|
+
|
15
|
+
# steal timeout values so we can replicate the same timeout behavior
|
16
|
+
@timeout = @connection.pool.instance_variable_get(:@timeout)
|
17
|
+
@sleep_time = @connection.pool.instance_variable_get(:@sleep_time)
|
18
|
+
|
19
|
+
# connections that we created explicitly
|
20
|
+
@owned_connections = Hash.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def checkout(connection_manager)
|
24
|
+
# copied from Sequel's ThreadedConnectionPool#hold
|
25
|
+
# to ensure consistent behavior
|
26
|
+
unless acquire(connection_manager.thread)
|
27
|
+
time = Time.now
|
28
|
+
timeout = time + @timeout
|
29
|
+
sleep_time = @sleep_time
|
30
|
+
sleep sleep_time
|
31
|
+
until acquire(connection_manager.thread)
|
32
|
+
raise(::Sequel::PoolTimeout) if Time.now > timeout
|
33
|
+
sleep sleep_time
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
@connection
|
38
|
+
end
|
39
|
+
|
40
|
+
def release(connection_manager, connection)
|
41
|
+
# ensure we only release connections we own, in case
|
42
|
+
# we've acquired a connection from Sequel that
|
43
|
+
# is likely still in use.
|
44
|
+
if (@owned_connections.delete(connection_manager.thread))
|
45
|
+
@connection.pool.send(:sync) do
|
46
|
+
@connection.pool.send(:release,connection_manager.thread)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def acquire(thread)
|
52
|
+
# check connection's pool to see if it already has a connection
|
53
|
+
# if so, re-use it. otherwise, acquire a new one and mark that we
|
54
|
+
# "own" it for future releasing.
|
55
|
+
if (owned = @connection.pool.send(:owned_connection, thread))
|
56
|
+
return true
|
57
|
+
else
|
58
|
+
conn = @connection.pool.send(:acquire, thread)
|
59
|
+
@owned_connections[thread] = conn
|
60
|
+
true
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Praxis::Mapper
|
2
|
+
module ConnectionFactories
|
3
|
+
class Simple
|
4
|
+
def initialize(connection: nil, &block)
|
5
|
+
@connection = connection if connection
|
6
|
+
if block
|
7
|
+
@checkout = block
|
8
|
+
end
|
9
|
+
|
10
|
+
if @connection && @checkout
|
11
|
+
raise ArgumentError, 'May not provide both a connection and block'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def checkout(connection_manager)
|
16
|
+
return @connection if @connection
|
17
|
+
|
18
|
+
@checkout.call
|
19
|
+
end
|
20
|
+
|
21
|
+
def release(connection_manager, connection)
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -1,61 +1,39 @@
|
|
1
1
|
module Praxis::Mapper
|
2
2
|
class ConnectionManager
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
connection_factory_class = Object.const_get(klass_name)
|
9
|
-
repositories[repository_name][:connection_factory] = connection_factory_class.new(data[:connection_opts])
|
4
|
+
@repositories = {}
|
5
|
+
class << self
|
6
|
+
attr_accessor :repositories
|
7
|
+
end
|
10
8
|
|
11
|
-
|
12
|
-
query_klass = Object.const_get(query_klass_name)
|
13
|
-
repositories[repository_name][:query] = query_klass
|
14
|
-
end
|
15
|
-
end
|
9
|
+
def self.setup(&block)
|
16
10
|
if block_given?
|
17
11
|
self.instance_eval(&block)
|
18
12
|
end
|
19
13
|
end
|
20
14
|
|
21
|
-
def self.repository(repository_name, data
|
22
|
-
return repositories[repository_name] if data.
|
15
|
+
def self.repository(repository_name, **data, &block)
|
16
|
+
return repositories[repository_name] if data.empty? && !block_given?
|
23
17
|
|
24
|
-
|
25
|
-
|
26
|
-
when String
|
27
|
-
query_klass_name = data[:query]
|
28
|
-
Object.const_get(query_klass_name) #FIXME: won't really work consistently
|
29
|
-
when Class
|
30
|
-
data[:query]
|
31
|
-
when Symbol
|
32
|
-
raise "symbol support is not implemented yet"
|
33
|
-
else
|
34
|
-
raise "unknown type for query: #{data[:query].inspect} has type #{data[:query].class}"
|
35
|
-
end
|
36
|
-
repositories[repository_name][:query] = query_klass
|
37
|
-
end
|
18
|
+
query = data[:query] || Praxis::Mapper::Query::Sql
|
19
|
+
factory_class = data[:factory] || ConnectionFactories::Simple
|
38
20
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
elsif data
|
43
|
-
klass_name = data.delete(:connection_factory)
|
44
|
-
connection_factory_class = Object.const_get(klass_name) #FIXME: won't really work consistently
|
45
|
-
repositories[repository_name][:connection_factory] = connection_factory_class.new(data[:connection_opts])
|
21
|
+
opts = data[:opts] || {}
|
22
|
+
if query.kind_of? String
|
23
|
+
query = query.constantize
|
46
24
|
end
|
47
|
-
end
|
48
|
-
|
49
25
|
|
50
|
-
|
51
|
-
|
52
|
-
hash[key] = {
|
53
|
-
:connection_factory => nil,
|
54
|
-
:query => Praxis::Mapper::Query::Sql
|
55
|
-
}
|
26
|
+
if factory_class.kind_of? String
|
27
|
+
factory_class = factory_class.constantize
|
56
28
|
end
|
29
|
+
|
30
|
+
repositories[repository_name] = {
|
31
|
+
query: query,
|
32
|
+
factory: factory_class.new(**opts, &block)
|
33
|
+
}
|
57
34
|
end
|
58
35
|
|
36
|
+
|
59
37
|
def repositories
|
60
38
|
self.class.repositories
|
61
39
|
end
|
@@ -66,26 +44,27 @@ module Praxis::Mapper
|
|
66
44
|
|
67
45
|
def initialize
|
68
46
|
@connections = {}
|
47
|
+
@thread = Thread.current
|
48
|
+
end
|
49
|
+
|
50
|
+
def thread
|
51
|
+
return @thread if @thread == Thread.current
|
52
|
+
raise 'threading violation in ConnectionManager. Calling Thread is different from Thread that owns this instance.'
|
69
53
|
end
|
70
54
|
|
71
55
|
def checkout(name)
|
72
56
|
connection = @connections[name]
|
73
57
|
return connection if connection
|
74
58
|
|
75
|
-
factory = repositories[name][:
|
76
|
-
connection =
|
77
|
-
factory.call
|
78
|
-
else
|
79
|
-
factory.checkout
|
80
|
-
end
|
59
|
+
factory = repositories[name][:factory]
|
60
|
+
connection = factory.checkout(self)
|
81
61
|
|
82
62
|
@connections[name] = connection
|
83
63
|
end
|
84
64
|
|
85
65
|
def release_one(name)
|
86
66
|
if (connection = @connections.delete(name))
|
87
|
-
|
88
|
-
repositories[name][:connection_factory].release(connection)
|
67
|
+
repositories[name][:factory].release(self, connection)
|
89
68
|
end
|
90
69
|
end
|
91
70
|
|
@@ -4,7 +4,8 @@
|
|
4
4
|
# The scope can be thought of as a set of named filters.
|
5
5
|
module Praxis::Mapper
|
6
6
|
class IdentityMap
|
7
|
-
|
7
|
+
include IdentityMapExtensions::Persistence
|
8
|
+
|
8
9
|
class UnloadedRecordException < StandardError; end;
|
9
10
|
class UnsupportedModel < StandardError; end;
|
10
11
|
class UnknownIdentity < StandardError; end;
|
@@ -69,6 +70,11 @@ module Praxis::Mapper
|
|
69
70
|
@connection_manager = ConnectionManager.new
|
70
71
|
@scope = scope
|
71
72
|
clear!
|
73
|
+
|
74
|
+
# Ensure we clean up open connections
|
75
|
+
ObjectSpace.define_finalizer(self) do
|
76
|
+
@connection_manager.release
|
77
|
+
end
|
72
78
|
end
|
73
79
|
|
74
80
|
def clear!
|
@@ -104,7 +110,9 @@ module Praxis::Mapper
|
|
104
110
|
end
|
105
111
|
|
106
112
|
# TODO: rework this so it's a hash with default values and simplify #index
|
107
|
-
@secondary_indexes =
|
113
|
+
@secondary_indexes = Hash.new do |hash, model|
|
114
|
+
hash[model] = Hash.new
|
115
|
+
end
|
108
116
|
end
|
109
117
|
|
110
118
|
|
@@ -184,9 +192,16 @@ module Praxis::Mapper
|
|
184
192
|
finalize_model!(model).any?
|
185
193
|
end
|
186
194
|
|
187
|
-
|
195
|
+
if did_something
|
196
|
+
finalize!
|
197
|
+
else
|
198
|
+
release
|
199
|
+
end
|
188
200
|
end
|
189
201
|
|
202
|
+
def release
|
203
|
+
@connection_manager.release
|
204
|
+
end
|
190
205
|
|
191
206
|
# don't doc. never ever use yourself!
|
192
207
|
# FIXME: make private and fix specs that break?
|
@@ -293,8 +308,6 @@ module Praxis::Mapper
|
|
293
308
|
|
294
309
|
|
295
310
|
def index(model, key, value)
|
296
|
-
@secondary_indexes[model] ||= Hash.new
|
297
|
-
|
298
311
|
unless @secondary_indexes[model].has_key? key
|
299
312
|
@secondary_indexes[model][key] ||= Hash.new
|
300
313
|
reindex!(model, key)
|
@@ -311,6 +324,7 @@ module Praxis::Mapper
|
|
311
324
|
else
|
312
325
|
row.send(key)
|
313
326
|
end
|
327
|
+
# FIXME: make this a set? or handle duplicates better
|
314
328
|
index(model, key, val) << row
|
315
329
|
end
|
316
330
|
end
|
@@ -325,7 +339,8 @@ module Praxis::Mapper
|
|
325
339
|
if values.size == 1
|
326
340
|
value = values[0]
|
327
341
|
if @row_keys[model].has_key?(key)
|
328
|
-
res =
|
342
|
+
res = @row_keys[model][key][value]
|
343
|
+
|
329
344
|
if res
|
330
345
|
[res]
|
331
346
|
else
|
@@ -337,7 +352,7 @@ module Praxis::Mapper
|
|
337
352
|
else
|
338
353
|
if @row_keys[model].has_key?(key)
|
339
354
|
values.collect do |value|
|
340
|
-
|
355
|
+
@row_keys[model][key][value]
|
341
356
|
end.compact
|
342
357
|
else
|
343
358
|
values.each_with_object(Array.new) do |value, results|
|
@@ -375,20 +390,19 @@ module Praxis::Mapper
|
|
375
390
|
end
|
376
391
|
end
|
377
392
|
|
378
|
-
|
379
393
|
def connection(name)
|
380
394
|
@connection_manager.checkout(name)
|
381
395
|
end
|
382
396
|
|
383
397
|
def extract_keys(field, records)
|
384
|
-
row_keys =
|
398
|
+
row_keys = Set.new
|
385
399
|
if field.kind_of?(Array) # composite identities
|
386
400
|
records.each do |record|
|
387
401
|
row_key = field.collect { |col| record.send(col) }
|
388
402
|
row_keys << row_key unless row_key.include?(nil)
|
389
403
|
end
|
390
404
|
else
|
391
|
-
row_keys.
|
405
|
+
row_keys.merge records.collect(&field).compact
|
392
406
|
end
|
393
407
|
row_keys
|
394
408
|
end
|
@@ -418,9 +432,9 @@ module Praxis::Mapper
|
|
418
432
|
key = tracked_association[:key]
|
419
433
|
primary_key = tracked_association[:primary_key] || :id
|
420
434
|
|
421
|
-
row_keys =
|
435
|
+
row_keys = Set.new
|
422
436
|
records.collect(&key).each do |keys|
|
423
|
-
row_keys.
|
437
|
+
row_keys.merge keys
|
424
438
|
end
|
425
439
|
|
426
440
|
row_keys.reject! do |row_key|
|
@@ -438,7 +452,7 @@ module Praxis::Mapper
|
|
438
452
|
|
439
453
|
|
440
454
|
def add_records(records)
|
441
|
-
return [] if records.empty?
|
455
|
+
return [] if records.empty?
|
442
456
|
|
443
457
|
to_stage = Hash.new do |hash,staged_model|
|
444
458
|
hash[staged_model] = Hash.new do |identities, identity_name|
|
@@ -497,6 +511,16 @@ module Praxis::Mapper
|
|
497
511
|
@row_keys[model][identity][key] = record
|
498
512
|
end
|
499
513
|
|
514
|
+
@secondary_indexes[model].each do |key, indexed_values|
|
515
|
+
val = if key.kind_of? Array
|
516
|
+
key.collect { |k| record.send(k) }
|
517
|
+
else
|
518
|
+
record.send(key)
|
519
|
+
end
|
520
|
+
|
521
|
+
indexed_values[val] << record
|
522
|
+
end
|
523
|
+
|
500
524
|
record.identity_map = self
|
501
525
|
@rows[model] << record
|
502
526
|
record
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Praxis::Mapper
|
2
|
+
module IdentityMapExtensions
|
3
|
+
module Persistence
|
4
|
+
|
5
|
+
def deindex(record)
|
6
|
+
model = record.class
|
7
|
+
|
8
|
+
# delete from full set of rows
|
9
|
+
rows_for(model).delete record
|
10
|
+
|
11
|
+
# remove record from identity indexes
|
12
|
+
@row_keys[model].each do |identity, index|
|
13
|
+
index.delete_if {|k,v| v == record }
|
14
|
+
end
|
15
|
+
|
16
|
+
# remove any secondary indexes
|
17
|
+
@secondary_indexes[model].each do |key, index|
|
18
|
+
index.each do |index_key, indexed_values|
|
19
|
+
indexed_values.delete record
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def reindex(record)
|
25
|
+
# fully remove the record from any indexes it may be part of
|
26
|
+
deindex(record)
|
27
|
+
|
28
|
+
# hack to update any indexes as applicable
|
29
|
+
add_record(record)
|
30
|
+
end
|
31
|
+
|
32
|
+
# attach record to the identity map.
|
33
|
+
# save the record if, and only if, we need to
|
34
|
+
def attach(record)
|
35
|
+
# save unless it has all identities populated
|
36
|
+
unless record.identities.all? { |identity, value| value }
|
37
|
+
record.save
|
38
|
+
end
|
39
|
+
|
40
|
+
# raise if still don't have full identities
|
41
|
+
unless record.identities.all? { |identity, value| value }
|
42
|
+
raise "can not attach #{record.inspect} without a full set of identities."
|
43
|
+
end
|
44
|
+
|
45
|
+
add_record(record)
|
46
|
+
|
47
|
+
# TODO: what to do with related records?
|
48
|
+
end
|
49
|
+
|
50
|
+
def flush!(object=nil)
|
51
|
+
if object.nil?
|
52
|
+
return @rows.keys.each { |klass| self.flush!(klass) }
|
53
|
+
end
|
54
|
+
|
55
|
+
case object
|
56
|
+
when Class
|
57
|
+
@rows[object].select(&:modified?).each do |record|
|
58
|
+
record.save
|
59
|
+
reindex(record)
|
60
|
+
end
|
61
|
+
when Sequel::Model
|
62
|
+
if object.modified?
|
63
|
+
object.save
|
64
|
+
reindex(object)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def remove(record)
|
70
|
+
detach(record)
|
71
|
+
|
72
|
+
record.delete
|
73
|
+
end
|
74
|
+
|
75
|
+
def detach(record)
|
76
|
+
record.identity_map = nil
|
77
|
+
deindex(record)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|