praxis-mapper 3.4.0 → 4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module Praxis::Mapper
|
4
|
+
module SequelCompat
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
attr_accessor :_resource
|
9
|
+
attr_accessor :_query
|
10
|
+
attr_accessor :identity_map
|
11
|
+
|
12
|
+
@repository_name = :default
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
|
17
|
+
def identities
|
18
|
+
[primary_key]
|
19
|
+
end
|
20
|
+
|
21
|
+
def finalized?
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
def associations
|
26
|
+
orig = self.association_reflections.clone
|
27
|
+
|
28
|
+
orig.each do |k,v|
|
29
|
+
v[:model] = v.associated_class
|
30
|
+
if v.respond_to?(:primary_key)
|
31
|
+
v[:primary_key] = v.primary_key
|
32
|
+
else
|
33
|
+
# FIXME: figure out exactly what to do here.
|
34
|
+
# not super critical, as we can't track these associations
|
35
|
+
# directly, but it would be nice to traverse these
|
36
|
+
# properly.
|
37
|
+
v[:primary_key] = :unsupported
|
38
|
+
end
|
39
|
+
end
|
40
|
+
orig
|
41
|
+
end
|
42
|
+
|
43
|
+
def repository_name(name=nil)
|
44
|
+
return @repository_name if name.nil?
|
45
|
+
|
46
|
+
@repository_name = name
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
def _load_associated_objects(opts, dynamic_opts=OPTS)
|
53
|
+
return super if self.identity_map.nil?
|
54
|
+
target = opts.associated_class
|
55
|
+
key = opts[:key]
|
56
|
+
|
57
|
+
case opts[:type]
|
58
|
+
when :many_to_one
|
59
|
+
val = if key.kind_of?(Array)
|
60
|
+
@values.values_at(*key)
|
61
|
+
else
|
62
|
+
@values[key]
|
63
|
+
end
|
64
|
+
return nil if val.nil?
|
65
|
+
self.identity_map.get(target, target.primary_key => val)
|
66
|
+
when :one_to_many
|
67
|
+
self.identity_map.all(target, key => [pk] )
|
68
|
+
when :many_to_many
|
69
|
+
# OPTIMIZE: cache this result
|
70
|
+
join_model = opts[:join_model].constantize
|
71
|
+
|
72
|
+
left_key = opts[:left_key]
|
73
|
+
right_key = opts[:right_key]
|
74
|
+
|
75
|
+
right_values = self.identity_map.
|
76
|
+
all(join_model, left_key => Array(values[primary_key])).
|
77
|
+
collect(&right_key)
|
78
|
+
|
79
|
+
self.identity_map.all(target, target.primary_key => right_values )
|
80
|
+
else
|
81
|
+
raise "#{opts[:type]} is not currently supported"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
def identities
|
87
|
+
self.class.identities.each_with_object(Hash.new) do |identity, hash|
|
88
|
+
case identity
|
89
|
+
when Symbol
|
90
|
+
hash[identity] = values[identity].freeze
|
91
|
+
else
|
92
|
+
hash[identity] = values.values_at(*identity).collect(&:freeze)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
data/praxis-mapper.gemspec
CHANGED
@@ -34,4 +34,6 @@ Gem::Specification.new do |spec|
|
|
34
34
|
spec.add_development_dependency(%q<pry-byebug>, ["~> 1"])
|
35
35
|
spec.add_development_dependency(%q<pry-stack_explorer>, ["~> 0"])
|
36
36
|
spec.add_development_dependency(%q<fuubar>, ["~> 1"])
|
37
|
+
spec.add_development_dependency('sqlite3')
|
38
|
+
spec.add_development_dependency('factory_girl')
|
37
39
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
|
3
|
+
to_create { |i| i.save }
|
4
|
+
|
5
|
+
factory :user, class: UserModel, aliases: [:author] do
|
6
|
+
name { /[:name:]/.gen }
|
7
|
+
email { /[:email:]/.gen }
|
8
|
+
end
|
9
|
+
|
10
|
+
factory :post, class: PostModel do
|
11
|
+
title { /\w+/.gen }
|
12
|
+
body { /\w+/.gen }
|
13
|
+
author
|
14
|
+
end
|
15
|
+
|
16
|
+
factory :comment, class: CommentModel do
|
17
|
+
author
|
18
|
+
post
|
19
|
+
end
|
20
|
+
|
21
|
+
factory :composite, class: CompositeIdSequelModel do
|
22
|
+
id { /\w+/.gen }
|
23
|
+
type { /\w+/.gen }
|
24
|
+
|
25
|
+
name { /\w+/.gen }
|
26
|
+
end
|
27
|
+
|
28
|
+
factory :other, class: OtherSequelModel do
|
29
|
+
composite
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Praxis::Mapper::ConnectionFactories::Sequel do
|
4
|
+
|
5
|
+
let(:database) { Sequel.mock}
|
6
|
+
let(:thread) { Thread.current }
|
7
|
+
let(:connection_manager) { double('Praxis::Mapper::ConnectionManager', thread: thread) }
|
8
|
+
|
9
|
+
subject(:factory) { described_class.new(connection:database) }
|
10
|
+
|
11
|
+
context 'checkout' do
|
12
|
+
it 'returns the connection' do
|
13
|
+
factory.checkout(connection_manager).should be database
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'allocates a connection to the thread of the manager' do
|
17
|
+
database.pool.allocated.should_not have_key(thread)
|
18
|
+
factory.checkout(connection_manager)
|
19
|
+
database.pool.allocated.should have_key(thread)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'does not acquire a new connection if the thread already has one' do
|
23
|
+
connection = double('connection')
|
24
|
+
database.pool.allocated[thread] = connection
|
25
|
+
|
26
|
+
factory.checkout(connection_manager)
|
27
|
+
database.pool.allocated[thread].should be connection
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'release' do
|
33
|
+
before do
|
34
|
+
factory.checkout(connection_manager)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'releases the connection' do
|
38
|
+
database.pool.allocated.should have_key(thread)
|
39
|
+
factory.release(connection_manager, database)
|
40
|
+
database.pool.allocated.should_not have_key(thread)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'across multiple threads' do
|
45
|
+
before do
|
46
|
+
factory.checkout(connection_manager)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'acquires a connection in a separate thread' do
|
50
|
+
database.pool.allocated.should have_key(thread)
|
51
|
+
|
52
|
+
thread_2 = Thread.new do
|
53
|
+
connection_manager_2 = Praxis::Mapper::ConnectionManager.new
|
54
|
+
database.pool.allocated.should_not have_key(Thread.current)
|
55
|
+
database.pool.allocated.should have_key(thread)
|
56
|
+
|
57
|
+
factory.checkout(connection_manager_2)
|
58
|
+
database.pool.allocated.should have_key(Thread.current)
|
59
|
+
database.pool.allocated.should have_key(thread)
|
60
|
+
end
|
61
|
+
|
62
|
+
thread_2.join
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Praxis::Mapper::ConnectionFactories::Simple do
|
4
|
+
|
5
|
+
let(:connection) { double("connection") }
|
6
|
+
|
7
|
+
let(:connection_manager) { double('Praxis::Mapper::ConnectionManager') }
|
8
|
+
|
9
|
+
context 'with a raw connection' do
|
10
|
+
subject(:factory) { described_class.new(connection:connection) }
|
11
|
+
|
12
|
+
it 'returns the connection on checkout' do
|
13
|
+
factory.checkout(connection_manager).should be connection
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'with a proc' do
|
19
|
+
let(:block) { Proc.new { connection } }
|
20
|
+
|
21
|
+
subject(:factory) { described_class.new(&block) }
|
22
|
+
|
23
|
+
it 'calls the block on checkout' do
|
24
|
+
block.should_receive(:call).and_call_original
|
25
|
+
factory.checkout(connection_manager).should be connection
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -1,117 +1,95 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
2
|
|
3
3
|
class DummyFactory
|
4
|
+
attr_reader :opts
|
4
5
|
def initialize(opts)
|
5
6
|
@opts = opts
|
6
7
|
end
|
7
8
|
|
8
|
-
def
|
9
|
-
@opts
|
9
|
+
def checkout(connection_manager)
|
10
10
|
end
|
11
11
|
|
12
|
-
def
|
13
|
-
end
|
14
|
-
|
15
|
-
def release(connection)
|
12
|
+
def release(connection_manager, connection)
|
16
13
|
end
|
17
14
|
|
18
15
|
end
|
19
16
|
|
20
|
-
|
21
17
|
describe Praxis::Mapper::ConnectionManager do
|
22
18
|
let(:mock_connection) { double("connection") }
|
23
19
|
|
24
20
|
let(:default_hash) { Hash.new }
|
25
21
|
let(:factory_opts) { {:foo => "bar"} }
|
26
|
-
let(:config) { {:dummy => {:connection_factory => "DummyFactory", :connection_opts => factory_opts}} }
|
27
22
|
|
23
|
+
let(:dummy_connection) { double("dummy connection")}
|
28
24
|
let(:dummy_factory_mock) { double("dummy_factory")}
|
29
25
|
|
30
|
-
subject { Praxis::Mapper::ConnectionManager }
|
31
|
-
|
26
|
+
subject(:connection_manager) { Praxis::Mapper::ConnectionManager }
|
27
|
+
|
32
28
|
before do
|
33
|
-
|
29
|
+
opts = factory_opts
|
30
|
+
block = Proc.new { mock_connection }
|
31
|
+
|
32
|
+
Praxis::Mapper::ConnectionManager.setup do
|
33
|
+
repository :foo, &block
|
34
|
+
repository :bar, :factory => "DummyFactory", :opts => opts
|
35
|
+
end
|
34
36
|
end
|
35
37
|
|
36
|
-
it '
|
37
|
-
|
38
|
+
it 'supports proc-based repostories' do
|
39
|
+
subject.repository(:foo)[:factory].should be_kind_of(Praxis::Mapper::ConnectionFactories::Simple)
|
40
|
+
end
|
38
41
|
|
39
|
-
|
40
|
-
repository[:
|
42
|
+
it 'supports config-based repositories' do
|
43
|
+
subject.repository(:bar)[:factory].should be_kind_of(DummyFactory)
|
44
|
+
subject.repository(:bar)[:factory].opts.should == factory_opts
|
41
45
|
end
|
42
46
|
|
43
|
-
context
|
44
|
-
let(:dummy_connection) { double("dummy connection")}
|
45
|
-
|
46
|
-
before do
|
47
|
-
opts = factory_opts
|
48
|
-
|
49
|
-
block = Proc.new { mock_connection }
|
50
|
-
Praxis::Mapper::ConnectionManager.setup do
|
51
|
-
repository :foo, &block
|
52
|
-
repository :bar, :connection_factory => "DummyFactory", :connection_opts => opts
|
53
|
-
end
|
54
|
-
end
|
47
|
+
context 'getting connections' do
|
55
48
|
|
56
|
-
|
57
|
-
subject.repository(:foo)[:connection_factory].should be_kind_of(Proc)
|
58
|
-
end
|
49
|
+
subject { Praxis::Mapper::ConnectionManager.new }
|
59
50
|
|
60
|
-
it '
|
61
|
-
subject.
|
62
|
-
subject.repository(:bar)[:connection_factory].opts.should == factory_opts
|
51
|
+
it 'gets connections from proc-based repositories' do
|
52
|
+
subject.checkout(:foo).should == mock_connection
|
63
53
|
end
|
64
54
|
|
65
|
-
|
66
|
-
|
67
|
-
subject
|
68
|
-
|
69
|
-
it 'gets connections from proc-based repositories' do
|
70
|
-
subject.checkout(:foo).should == mock_connection
|
71
|
-
end
|
55
|
+
it 'gets connections from config-based repositories' do
|
56
|
+
DummyFactory.any_instance.should_receive(:checkout).with(subject).and_return(dummy_connection)
|
57
|
+
subject.checkout(:bar).should == dummy_connection
|
58
|
+
end
|
72
59
|
|
73
|
-
|
74
|
-
DummyFactory.any_instance.should_receive(:checkout).and_return(dummy_connection)
|
75
|
-
subject.checkout(:bar).should == dummy_connection
|
76
|
-
end
|
60
|
+
end
|
77
61
|
|
78
|
-
|
62
|
+
context 'releasing connections' do
|
63
|
+
subject { Praxis::Mapper::ConnectionManager.new }
|
79
64
|
|
80
|
-
|
81
|
-
subject
|
65
|
+
it 'releases connections from config-based repositories' do
|
66
|
+
DummyFactory.any_instance.should_receive(:checkout).with(subject).exactly(2).times.and_return(dummy_connection)
|
67
|
+
DummyFactory.any_instance.should_receive(:release).with(subject, dummy_connection).and_return(true)
|
82
68
|
|
83
|
-
|
69
|
+
subject.checkout(:bar)
|
70
|
+
subject.checkout(:bar)
|
84
71
|
|
85
|
-
|
86
|
-
DummyFactory.any_instance.should_receive(:release).with(dummy_connection).and_return(true)
|
72
|
+
subject.release(:bar)
|
87
73
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
subject.release(:bar)
|
92
|
-
|
93
|
-
subject.checkout(:bar)
|
94
|
-
end
|
74
|
+
subject.checkout(:bar)
|
75
|
+
end
|
95
76
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
77
|
+
it 'releases connections from proc-based repositories' do
|
78
|
+
subject.checkout(:foo)
|
79
|
+
subject.release(:foo)
|
80
|
+
end
|
100
81
|
|
101
|
-
|
102
|
-
|
103
|
-
|
82
|
+
it 'releases all connections' do
|
83
|
+
DummyFactory.any_instance.should_receive(:checkout).with(subject).exactly(1).times.and_return(dummy_connection)
|
84
|
+
DummyFactory.any_instance.should_receive(:release).with(subject, dummy_connection).and_return(true)
|
104
85
|
|
105
|
-
|
86
|
+
subject.checkout(:bar)
|
106
87
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
subject.release
|
111
|
-
end
|
88
|
+
subject.should_not_receive(:release_one).with(:foo).and_call_original
|
89
|
+
subject.should_receive(:release_one).with(:bar).and_call_original
|
112
90
|
|
91
|
+
subject.release
|
113
92
|
end
|
114
93
|
|
115
|
-
|
116
94
|
end
|
117
95
|
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Praxis::Mapper::IdentityMapExtensions::Persistence do
|
4
|
+
|
5
|
+
let(:user) { UserModel.create }
|
6
|
+
let(:post) { PostModel.create(title: 'title', author: user) }
|
7
|
+
|
8
|
+
before do
|
9
|
+
identity_map.add_record(post)
|
10
|
+
identity_map.add_record(user)
|
11
|
+
identity_map.reindex!(PostModel, :author_id)
|
12
|
+
end
|
13
|
+
|
14
|
+
subject(:identity_map) { Praxis::Mapper::IdentityMap.new }
|
15
|
+
|
16
|
+
context '#attach(record)' do
|
17
|
+
context 'for a record that is missing an identity' do
|
18
|
+
let(:post) { PostModel.new(title: 'title') }
|
19
|
+
|
20
|
+
it 'saves the record before adding it ' do
|
21
|
+
post.should_receive(:save).and_call_original
|
22
|
+
identity_map.should_receive(:add_record).with(post).and_call_original
|
23
|
+
|
24
|
+
identity_map.attach(post)
|
25
|
+
|
26
|
+
identity_map.get(PostModel, id: post.id).should be post
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'for a record that is not missing an identity' do
|
31
|
+
let(:post) { PostModel.create(title: 'title') }
|
32
|
+
|
33
|
+
it 'does not save the record before adding it' do
|
34
|
+
post.should_not_receive(:save).and_call_original
|
35
|
+
identity_map.should_receive(:add_record).with(post).and_call_original
|
36
|
+
|
37
|
+
identity_map.attach(post)
|
38
|
+
|
39
|
+
identity_map.get(PostModel, id: post.id).should be post
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context '#flush!(object=nil)' do
|
45
|
+
|
46
|
+
context 'with a single record' do
|
47
|
+
it 'saves the changes' do
|
48
|
+
post.title = 'something else'
|
49
|
+
|
50
|
+
identity_map.flush!(post)
|
51
|
+
post.modified?.should be false
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'for an entire model class' do
|
56
|
+
it 'flushes every changed record' do
|
57
|
+
post.title = 'something else'
|
58
|
+
|
59
|
+
identity_map.flush!(PostModel)
|
60
|
+
post.modified?.should be false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'with no object' do
|
65
|
+
it 'flushes every class' do
|
66
|
+
identity_map.should_receive(:flush!).with(no_args).and_call_original
|
67
|
+
identity_map.should_receive(:flush!).with(PostModel)
|
68
|
+
identity_map.should_receive(:flush!).with(UserModel)
|
69
|
+
|
70
|
+
|
71
|
+
identity_map.flush!
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context '#remove(record)' do
|
77
|
+
it 'detaches and deletes the record' do
|
78
|
+
identity_map.should_receive(:detach).with(post).and_call_original
|
79
|
+
post.should_receive(:delete)
|
80
|
+
|
81
|
+
identity_map.remove(post)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context '#detach(record)' do
|
86
|
+
|
87
|
+
it 'unsets record identity_map and deindexes the record' do
|
88
|
+
identity_map.should_receive(:deindex).with(post)
|
89
|
+
identity_map.detach(post)
|
90
|
+
post.identity_map.should be nil
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
context '#deindex(record)' do
|
95
|
+
let!(:original_id) { post.id }
|
96
|
+
|
97
|
+
before do
|
98
|
+
post.id = 100
|
99
|
+
|
100
|
+
# build secondary index and ensure it's populated correctly
|
101
|
+
identity_map.all(PostModel, title: [post.title]).should include post
|
102
|
+
|
103
|
+
identity_map.deindex(post)
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'cleans up all-rows index' do
|
107
|
+
identity_map.all(PostModel).should be_empty
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'cleans up identity indexes' do
|
111
|
+
identity_map.all(PostModel, id: [original_id]).should be_empty
|
112
|
+
identity_map.all(PostModel, id: [post.id]).should be_empty
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'cleans up secondary indexes' do
|
116
|
+
identity_map.all(PostModel, title: [post.title]).should_not include post
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
end
|