perpetuity 0.4.4 → 0.4.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +0 -1
- data/CHANGELOG.md +9 -0
- data/README.md +52 -23
- data/lib/perpetuity/data_injectable.rb +6 -4
- data/lib/perpetuity/mapper.rb +9 -9
- data/lib/perpetuity/mongodb.rb +50 -35
- data/lib/perpetuity/persisted_object.rb +7 -0
- data/lib/perpetuity/retrieval.rb +6 -34
- data/lib/perpetuity/serializer.rb +62 -6
- data/lib/perpetuity/version.rb +1 -1
- data/perpetuity.gemspec +1 -2
- data/spec/integration/associations_spec.rb +65 -0
- data/spec/integration/deletion_spec.rb +24 -0
- data/spec/integration/indexing_spec.rb +38 -0
- data/spec/integration/pagination_spec.rb +27 -0
- data/spec/integration/persistence_spec.rb +97 -0
- data/spec/integration/retrieval_spec.rb +114 -0
- data/spec/integration/serialization_spec.rb +60 -0
- data/spec/integration/update_spec.rb +30 -0
- data/spec/integration/validations_spec.rb +17 -0
- data/spec/perpetuity/mapper_spec.rb +3 -2
- data/spec/perpetuity/mongodb_spec.rb +9 -6
- data/spec/perpetuity/persisted_object_spec.rb +16 -0
- data/spec/perpetuity/retrieval_spec.rb +3 -1
- data/spec/perpetuity/serializer_spec.rb +31 -7
- data/spec/perpetuity_spec.rb +1 -427
- data/spec/spec_helper.rb +6 -0
- metadata +29 -22
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
## Version 0.4.5
|
2
|
+
|
3
|
+
- Move from the `mongo` gem from 10gen to the `moped` gem for talking to MongoDB. This resulted in performance gains of 30-80%, depending on query size.
|
4
|
+
- Make persisted objects marshalable.
|
5
|
+
- Previously, the `id` method was defined on individual objects that were either persisted to or retrieved from the DB. This made them unable to be marshaled with `Marshal.dump`.
|
6
|
+
- Now we extend the objects with `Perpetuity::PersistedObject` to keep them marshalable while still providing `id`. This keeps them marshalable (unmarshalled objects will still be extended with `Perpetuity::PersistedObject`).
|
7
|
+
- Provided a partial fix for a [bug](https://github.com/jgaskins/perpetuity/issues/23) that kept us from being able to persist hash attributes properly. See the first comments in the linked GitHub issue for an explanation of why it is only a partial fix.
|
8
|
+
- Stopped testing on MRI 1.9.2 with Travis CI. Moped requires 1.9.3 or higher.
|
9
|
+
|
1
10
|
## Version 0.4.4
|
2
11
|
|
3
12
|
- Automatically persist all referenced objects if they are not already persisted. Previously, referenced objects were required to be persisted before persisting the referencing object.
|
data/README.md
CHANGED
@@ -23,7 +23,14 @@ gem 'perpetuity'
|
|
23
23
|
The only currently supported persistence method is MongoDB. Other schemaless solutions can probably be implemented easily.
|
24
24
|
|
25
25
|
```ruby
|
26
|
-
mongodb = Perpetuity::MongoDB.new
|
26
|
+
mongodb = Perpetuity::MongoDB.new(
|
27
|
+
db: 'example_db', # Required
|
28
|
+
host: 'mongodb.example.com', # Default: 'localhost'
|
29
|
+
port: 27017, # Default: 27017
|
30
|
+
username: 'mongo', # If no username/password given, do not authenticate
|
31
|
+
password: 'password'
|
32
|
+
)
|
33
|
+
|
27
34
|
Perpetuity.configure do
|
28
35
|
data_source mongodb
|
29
36
|
end
|
@@ -35,7 +42,10 @@ Object mappers are generated by the following:
|
|
35
42
|
|
36
43
|
```ruby
|
37
44
|
Perpetuity.generate_mapper_for MyClass do
|
38
|
-
|
45
|
+
attribute :my_attribute
|
46
|
+
attribute :my_other_attribute
|
47
|
+
|
48
|
+
index :my_attribute
|
39
49
|
end
|
40
50
|
```
|
41
51
|
|
@@ -68,7 +78,7 @@ You can load all persisted objects of a particular class by sending `all` to the
|
|
68
78
|
Perpetuity[Article].all
|
69
79
|
```
|
70
80
|
|
71
|
-
You can load specific objects by calling the `find` method with an ID param on
|
81
|
+
You can load specific objects by calling the `find` method with an ID param on the mapper and passing in the criteria. You may also specify more general criteria using the `select` method with a block similar to `Enumerable#select`.
|
72
82
|
|
73
83
|
```ruby
|
74
84
|
article = Perpetuity[Article].find params[:id]
|
@@ -77,13 +87,15 @@ articles = Perpetuity[Article].select { |article| article.published_at < Time.no
|
|
77
87
|
comments = Perpetuity[Comment].select { |comment| comment.article_id.in articles.map(&:id) }
|
78
88
|
```
|
79
89
|
|
80
|
-
These methods will return a Perpetuity::Retrieval object, which will lazily retrieve the objects from the database. They will wait to hit the DB when you begin iterating over the objects so you can continue chaining methods.
|
90
|
+
These methods will return a Perpetuity::Retrieval object, which will lazily retrieve the objects from the database. They will wait to hit the DB when you begin iterating over the objects so you can continue chaining methods, similar to ActiveRecord.
|
81
91
|
|
82
92
|
```ruby
|
83
93
|
article_mapper = Perpetuity[Article]
|
84
94
|
articles = article_mapper.select { |article| article.published_at < Time.now }
|
85
|
-
|
86
|
-
|
95
|
+
.sort(:published_at)
|
96
|
+
.reverse
|
97
|
+
.page(2)
|
98
|
+
.per_page(10) # built-in pagination
|
87
99
|
|
88
100
|
articles.each do |article| # This is when the DB gets hit
|
89
101
|
# Display the pretty articles
|
@@ -100,37 +112,52 @@ Notice that we have to use a single `&` and surround each criterion with parenth
|
|
100
112
|
|
101
113
|
## Associations with Other Objects
|
102
114
|
|
103
|
-
The database can natively serialize some objects. For example, MongoDB can serialize `String`, `Numeric`, `Array`, `Hash`, `Time`, `nil`, `true`, `false`, and a few others.
|
115
|
+
The database can natively serialize some objects. For example, MongoDB can serialize `String`, `Numeric`, `Array`, `Hash`, `Time`, `nil`, `true`, `false`, and a few others. For other data types, you must determine whether you want those attributes embedded within the same document in the database or attached as a reference. For example, a `Post` could have `Comment`s, which would likely be embedded within the post object. But these comments could have an `author` attribute that references the `Person` that wrote the comment. Embedding the author in this case is not a good idea since it would be a duplicate of the `Person` that wrote it, which would then be out of sync if the original object is modified.
|
104
116
|
|
105
|
-
If
|
117
|
+
If an object references another type of object, the association is declared just as any other attribute. No special treatment is required. For embedded relationships, make sure you use the `embedded: true` option in the attribute.
|
106
118
|
|
107
119
|
```ruby
|
108
|
-
|
120
|
+
Perpetuity.generate_mapper_for Article do
|
121
|
+
attribute :title
|
122
|
+
attribute :body
|
123
|
+
attribute :author
|
124
|
+
attribute :comments, embedded: true
|
125
|
+
attribute :timestamp
|
109
126
|
end
|
110
127
|
|
111
|
-
|
112
|
-
|
128
|
+
Perpetuity.generate_mapper_for Comment do
|
129
|
+
attribute :body
|
130
|
+
attribute :author
|
131
|
+
attribute :timestamp
|
113
132
|
end
|
133
|
+
```
|
114
134
|
|
115
|
-
|
116
|
-
end
|
135
|
+
In this case, the article has an array of `Comment` objects, which the serializer knows that MongoDB cannot serialize. It will then tell the `Comment` mapper to serialize it and it stores that within the array.
|
117
136
|
|
118
|
-
|
119
|
-
|
120
|
-
|
137
|
+
If some of the comments aren't objects of class `Comment`, it will adapt and serialize them according to their class. This works very well for objects that can have attributes of various types, such as a `User` having a profile attribute that can be either a `UserProfile` or `AdminProfile` object. You don't need to declare anything different for this case, just store the appropriate type of object into the `User`'s `profile` attribute and the mapper will take care of the details.
|
138
|
+
|
139
|
+
If the associated object's class has a mapper defined, it will be used by the parent object's mapper for serialization. Otherwise, the object will be `Marshal.dump`ed. If the object cannot be marshaled, the object cannot be serialized and an exception will be raised.
|
140
|
+
|
141
|
+
When you load an object that has embedded associations, the embedded attributes are loaded immediately. For referenced associations, though, only the object itself will be loaded. All referenced objects must be loaded with the `load_association!` mapper call.
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
user_mapper = Perpetuity[User]
|
145
|
+
user = user_mapper.find(params[:id])
|
146
|
+
user_mapper.load_association! user, :profile
|
121
147
|
```
|
122
148
|
|
123
|
-
This
|
149
|
+
This loads up the user's profile and injects it into the profile attribute. All loading of referenced objects is explicit so that we don't load an entire object graph unnecessarily. This encourages (forces, really) you to think about all of the objects you'll be loading.
|
150
|
+
|
151
|
+
If you want to load a 1:N, N:1 or M:N association, Perpetuity handles that for you.
|
124
152
|
|
125
153
|
```ruby
|
126
154
|
article_mapper = Perpetuity[Article]
|
127
|
-
|
128
|
-
article_mapper.load_association!
|
129
|
-
|
155
|
+
articles = article_mapper.all.to_a
|
156
|
+
article_mapper.load_association! articles.first, :tags # 1:N
|
157
|
+
article_mapper.load_association! articles, :author # All author objects for these articles load in a single query - N:1
|
158
|
+
article_mapper.load_association! articles, :tags # M:N
|
130
159
|
```
|
131
160
|
|
132
|
-
All loading of associated objects is explicit so that we don't load an entire object graph unnecessarily.
|
133
|
-
|
134
161
|
## Customizing persistence
|
135
162
|
|
136
163
|
Setting the ID of a record to a custom value rather than using the DB default.
|
@@ -141,6 +168,8 @@ Perpetuity.generate_mapper_for Article do
|
|
141
168
|
end
|
142
169
|
```
|
143
170
|
|
171
|
+
The block passed to the `id` macro is evaluated in the context of the object being persisted. This allows you to use the object's private methods and instance variables if you need to.
|
172
|
+
|
144
173
|
## Indexing
|
145
174
|
|
146
175
|
Indexes are declared with the `index` method. The simplest way to create an index is just to pass the attribute to be indexed as a parameter:
|
@@ -185,4 +214,4 @@ Perpetuity[Article].reindex!
|
|
185
214
|
|
186
215
|
## Contributing
|
187
216
|
|
188
|
-
Right now, this code is pretty bare and there are possibly some design decisions that need some more refinement. You can help. If you have ideas to build on this, send some love in the form of pull requests or issues or tweets or e-mails and I'll do what I can for them.
|
217
|
+
Right now, this code is pretty bare and there are possibly some design decisions that need some more refinement. You can help. If you have ideas to build on this, send some love in the form of pull requests or issues or [tweets](http://twitter.com/jamie_gaskins) or e-mails and I'll do what I can for them.
|
@@ -1,8 +1,9 @@
|
|
1
|
+
require 'perpetuity/persisted_object'
|
2
|
+
|
1
3
|
module Perpetuity
|
2
4
|
module DataInjectable
|
3
5
|
def inject_attribute object, attribute, value
|
4
|
-
|
5
|
-
object.instance_variable_set(attribute, value)
|
6
|
+
object.instance_variable_set("@#{attribute}", value)
|
6
7
|
end
|
7
8
|
|
8
9
|
def inject_data object, data
|
@@ -13,9 +14,10 @@ module Perpetuity
|
|
13
14
|
end
|
14
15
|
|
15
16
|
def give_id_to object, *args
|
16
|
-
|
17
|
-
args.first
|
17
|
+
if args.any?
|
18
|
+
inject_attribute object, :id, args.first
|
18
19
|
end
|
20
|
+
object.extend PersistedObject
|
19
21
|
end
|
20
22
|
end
|
21
23
|
end
|
data/lib/perpetuity/mapper.rb
CHANGED
@@ -86,12 +86,12 @@ module Perpetuity
|
|
86
86
|
end
|
87
87
|
|
88
88
|
def select &block
|
89
|
-
query = data_source.
|
89
|
+
query = data_source.query(&block).to_db
|
90
90
|
retrieve query
|
91
91
|
end
|
92
92
|
|
93
93
|
def find id
|
94
|
-
|
94
|
+
select { |object| object.id == id }.first
|
95
95
|
end
|
96
96
|
|
97
97
|
def delete object
|
@@ -148,13 +148,7 @@ module Perpetuity
|
|
148
148
|
end
|
149
149
|
|
150
150
|
def serialize object
|
151
|
-
Serializer.new(self
|
152
|
-
end
|
153
|
-
|
154
|
-
private
|
155
|
-
|
156
|
-
def retrieve criteria={}
|
157
|
-
Perpetuity::Retrieval.new mapped_class, criteria, data_source
|
151
|
+
Serializer.new(self).serialize(object)
|
158
152
|
end
|
159
153
|
|
160
154
|
def self.mapped_class
|
@@ -164,6 +158,12 @@ module Perpetuity
|
|
164
158
|
def mapped_class
|
165
159
|
self.class.mapped_class
|
166
160
|
end
|
161
|
+
|
162
|
+
private
|
163
|
+
|
164
|
+
def retrieve criteria={}
|
165
|
+
Perpetuity::Retrieval.new self, criteria
|
166
|
+
end
|
167
167
|
end
|
168
168
|
end
|
169
169
|
|
data/lib/perpetuity/mongodb.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
|
-
require '
|
1
|
+
require 'moped'
|
2
2
|
require 'perpetuity/mongodb/query'
|
3
3
|
require 'perpetuity/mongodb/index'
|
4
|
+
require 'set'
|
4
5
|
|
5
6
|
module Perpetuity
|
6
7
|
class MongoDB
|
7
|
-
attr_accessor :
|
8
|
+
attr_accessor :host, :port, :db, :pool_size, :username, :password
|
8
9
|
|
9
10
|
def initialize options
|
10
11
|
@host = options.fetch(:host, 'localhost')
|
@@ -13,47 +14,54 @@ module Perpetuity
|
|
13
14
|
@pool_size = options.fetch(:pool_size, 5)
|
14
15
|
@username = options[:username]
|
15
16
|
@password = options[:password]
|
16
|
-
@
|
17
|
+
@session = nil
|
17
18
|
@indexes = Hash.new { |hash, key| hash[key] = active_indexes(key) }
|
18
19
|
end
|
19
20
|
|
21
|
+
def session
|
22
|
+
@session ||= Moped::Session.new(["#{host}:#{port}"])
|
23
|
+
end
|
24
|
+
|
20
25
|
def connect
|
21
|
-
|
22
|
-
|
26
|
+
session.login(@username, @password) if @username and @password
|
27
|
+
session
|
23
28
|
end
|
24
29
|
|
25
30
|
def connected?
|
26
|
-
!!@
|
31
|
+
!!@session
|
27
32
|
end
|
28
33
|
|
29
34
|
def database
|
30
35
|
connect unless connected?
|
31
|
-
|
36
|
+
session.use db
|
32
37
|
end
|
33
38
|
|
34
39
|
def collection klass
|
35
|
-
database
|
40
|
+
database[klass.to_s]
|
36
41
|
end
|
37
42
|
|
38
43
|
def insert klass, attributes
|
39
44
|
if attributes.has_key? :id
|
40
45
|
attributes[:_id] = attributes[:id]
|
41
46
|
attributes.delete :id
|
47
|
+
else
|
48
|
+
attributes[:_id] = Moped::BSON::ObjectId.new
|
42
49
|
end
|
43
50
|
|
44
51
|
collection(klass).insert attributes
|
52
|
+
attributes[:_id]
|
45
53
|
end
|
46
54
|
|
47
55
|
def count klass
|
48
|
-
collection(klass).count
|
56
|
+
collection(klass).find.count
|
49
57
|
end
|
50
58
|
|
51
59
|
def delete_all klass
|
52
|
-
|
60
|
+
collection(klass.to_s).find.remove_all
|
53
61
|
end
|
54
62
|
|
55
63
|
def first klass
|
56
|
-
document =
|
64
|
+
document = collection(klass.to_s).find.limit(1).first
|
57
65
|
document[:id] = document.delete("_id")
|
58
66
|
|
59
67
|
document
|
@@ -63,46 +71,48 @@ module Perpetuity
|
|
63
71
|
# MongoDB uses '_id' as its ID field.
|
64
72
|
if criteria.has_key?(:id)
|
65
73
|
if criteria[:id].is_a? String
|
66
|
-
criteria = { _id: (BSON::ObjectId
|
74
|
+
criteria = { _id: (Moped::BSON::ObjectId(criteria[:id].to_s) rescue criteria[:id]) }
|
67
75
|
else
|
68
76
|
criteria[:_id] = criteria.delete(:id)
|
69
77
|
end
|
70
78
|
end
|
71
79
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
80
|
+
query = collection(klass.to_s).find(criteria)
|
81
|
+
|
82
|
+
skipped = options[:page] ? (options[:page] - 1) * options[:limit] : 0
|
83
|
+
query = query.skip skipped
|
84
|
+
query = query.limit(options[:limit])
|
85
|
+
query = sort(query, options)
|
77
86
|
|
78
|
-
|
87
|
+
query.map do |document|
|
79
88
|
document[:id] = document.delete("_id")
|
80
89
|
document
|
81
90
|
end
|
82
91
|
end
|
83
92
|
|
84
|
-
def
|
85
|
-
return
|
86
|
-
|
93
|
+
def sort query, options
|
94
|
+
return query unless options[:attribute] &&
|
95
|
+
options[:direction]
|
87
96
|
|
97
|
+
sort_orders = { ascending: 1, descending: -1 }
|
88
98
|
sort_field = options[:attribute]
|
89
99
|
sort_direction = options[:direction]
|
90
|
-
sort_criteria =
|
91
|
-
|
100
|
+
sort_criteria = { sort_field => sort_orders[sort_direction] }
|
101
|
+
query.sort(sort_criteria)
|
92
102
|
end
|
93
103
|
|
94
104
|
def all klass
|
95
105
|
retrieve klass, {}, {}
|
96
106
|
end
|
97
107
|
|
98
|
-
def delete
|
99
|
-
id =
|
108
|
+
def delete object_or_id, klass=nil
|
109
|
+
id = object_or_id.is_a?(PersistedObject) ? object_or_id.id : object_or_id
|
100
110
|
klass ||= object.class
|
101
|
-
collection(klass.to_s).
|
111
|
+
collection(klass.to_s).find("_id" => id).remove
|
102
112
|
end
|
103
113
|
|
104
114
|
def update klass, id, new_data
|
105
|
-
collection(klass).
|
115
|
+
collection(klass).find({ _id: id }).update(new_data)
|
106
116
|
end
|
107
117
|
|
108
118
|
def can_serialize? value
|
@@ -110,7 +120,11 @@ module Perpetuity
|
|
110
120
|
end
|
111
121
|
|
112
122
|
def drop_collection to_be_dropped
|
113
|
-
collection(to_be_dropped).drop
|
123
|
+
collection(to_be_dropped.to_s).drop
|
124
|
+
end
|
125
|
+
|
126
|
+
def query &block
|
127
|
+
Query.new(&block)
|
114
128
|
end
|
115
129
|
|
116
130
|
def index klass, attribute, options={}
|
@@ -126,8 +140,8 @@ module Perpetuity
|
|
126
140
|
end
|
127
141
|
|
128
142
|
def active_indexes klass
|
129
|
-
indexes = collection(klass).
|
130
|
-
indexes.map do |
|
143
|
+
indexes = collection(klass).indexes.to_a
|
144
|
+
indexes.map do |index|
|
131
145
|
key = index['key'].keys.first
|
132
146
|
direction = index['key'][key]
|
133
147
|
unique = index['unique']
|
@@ -140,17 +154,18 @@ module Perpetuity
|
|
140
154
|
order = index.order == :ascending ? 1 : -1
|
141
155
|
unique = index.unique?
|
142
156
|
|
143
|
-
collection(index.collection).
|
157
|
+
collection(index.collection).indexes.create({attribute => order}, unique: unique)
|
144
158
|
index.activate!
|
145
159
|
end
|
146
160
|
|
147
161
|
def remove_index index
|
148
162
|
coll = collection(index.collection)
|
149
|
-
db_indexes = coll.
|
150
|
-
name =~
|
151
|
-
end
|
163
|
+
db_indexes = coll.indexes.select do |db_index|
|
164
|
+
db_index['name'] =~ /\A#{index.attribute}/
|
165
|
+
end.map { |index| index['key'] }
|
166
|
+
|
152
167
|
if db_indexes.any?
|
153
|
-
collection(index.collection).
|
168
|
+
collection(index.collection).indexes.drop db_indexes.first
|
154
169
|
end
|
155
170
|
end
|
156
171
|
|
data/lib/perpetuity/retrieval.rb
CHANGED
@@ -1,16 +1,16 @@
|
|
1
|
-
require 'perpetuity/data_injectable'
|
2
1
|
require 'perpetuity/reference'
|
2
|
+
require 'perpetuity/serializer'
|
3
3
|
|
4
4
|
module Perpetuity
|
5
5
|
class Retrieval
|
6
|
-
include DataInjectable
|
7
6
|
include Enumerable
|
8
7
|
attr_accessor :sort_attribute, :sort_direction, :result_limit, :result_page, :quantity_per_page
|
9
8
|
|
10
|
-
def initialize
|
11
|
-
@
|
9
|
+
def initialize mapper, criteria
|
10
|
+
@mapper = mapper
|
11
|
+
@class = mapper.mapped_class
|
12
12
|
@criteria = criteria
|
13
|
-
@data_source = data_source
|
13
|
+
@data_source = mapper.data_source
|
14
14
|
end
|
15
15
|
|
16
16
|
def sort attribute=:name
|
@@ -57,35 +57,7 @@ module Perpetuity
|
|
57
57
|
end
|
58
58
|
|
59
59
|
def unserialize(data)
|
60
|
-
|
61
|
-
Marshal.load(data)
|
62
|
-
elsif data.is_a? Array
|
63
|
-
data.map { |i| unserialize i }
|
64
|
-
elsif data.is_a? Hash
|
65
|
-
metadata = data.delete('__metadata__')
|
66
|
-
if metadata
|
67
|
-
klass = Object.const_get metadata['class']
|
68
|
-
id = metadata['id']
|
69
|
-
if id
|
70
|
-
object = Reference.new(klass, id)
|
71
|
-
else
|
72
|
-
object = klass.allocate
|
73
|
-
data.each do |attr, value|
|
74
|
-
inject_attribute object, attr, unserialize(value)
|
75
|
-
end
|
76
|
-
end
|
77
|
-
else
|
78
|
-
object = @class.allocate
|
79
|
-
data.each do |attr, value|
|
80
|
-
inject_attribute object, attr, unserialize(value)
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
give_id_to object
|
85
|
-
object
|
86
|
-
else
|
87
|
-
data
|
88
|
-
end
|
60
|
+
Serializer.new(@mapper).unserialize(data)
|
89
61
|
end
|
90
62
|
|
91
63
|
def [] index
|
@@ -1,10 +1,15 @@
|
|
1
|
+
require 'perpetuity/data_injectable'
|
2
|
+
|
1
3
|
module Perpetuity
|
2
4
|
class Serializer
|
5
|
+
include DataInjectable
|
6
|
+
|
3
7
|
attr_reader :mapper, :mapper_registry
|
4
8
|
|
5
|
-
def initialize(mapper
|
9
|
+
def initialize(mapper)
|
6
10
|
@mapper = mapper
|
7
|
-
@
|
11
|
+
@class = mapper.mapped_class
|
12
|
+
@mapper_registry = mapper.mapper_registry
|
8
13
|
end
|
9
14
|
|
10
15
|
def attribute_for object, attribute_name
|
@@ -22,9 +27,7 @@ module Perpetuity
|
|
22
27
|
elsif mapper_registry.has_mapper?(value.class)
|
23
28
|
serialize_with_foreign_mapper(value, attrib.embedded?)
|
24
29
|
else
|
25
|
-
|
26
|
-
Marshal.dump(value)
|
27
|
-
end
|
30
|
+
Marshal.dump(value)
|
28
31
|
end
|
29
32
|
|
30
33
|
[attrib.name.to_s, serialized_value]
|
@@ -33,6 +36,59 @@ module Perpetuity
|
|
33
36
|
Hash[attrs]
|
34
37
|
end
|
35
38
|
|
39
|
+
def unserialize data
|
40
|
+
if data.is_a? Array
|
41
|
+
unserialize_object_array data
|
42
|
+
else
|
43
|
+
object = unserialize_object(data)
|
44
|
+
give_id_to object
|
45
|
+
object
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def unserialize_object data, klass=@class
|
50
|
+
if data.is_a? Hash
|
51
|
+
object = klass.allocate
|
52
|
+
data.each do |attr, value|
|
53
|
+
inject_attribute object, attr, unserialize_attribute(value)
|
54
|
+
end
|
55
|
+
object
|
56
|
+
else
|
57
|
+
unserialize_attribute data
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def unserialize_object_array objects
|
62
|
+
objects.map do |data|
|
63
|
+
object = unserialize_object data
|
64
|
+
give_id_to object
|
65
|
+
object
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def unserialize_attribute data
|
70
|
+
if data.is_a?(String) && data.start_with?("\u0004") # if it's marshaled
|
71
|
+
Marshal.load(data)
|
72
|
+
elsif data.is_a? Array
|
73
|
+
data.map { |i| unserialize_attribute i }
|
74
|
+
elsif data.is_a? Hash
|
75
|
+
metadata = data.delete('__metadata__')
|
76
|
+
if metadata
|
77
|
+
klass = Object.const_get metadata['class']
|
78
|
+
id = metadata['id']
|
79
|
+
if id
|
80
|
+
object = Reference.new(klass, id)
|
81
|
+
else
|
82
|
+
object = unserialize_object(data, klass)
|
83
|
+
end
|
84
|
+
else
|
85
|
+
data
|
86
|
+
end
|
87
|
+
else
|
88
|
+
data
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
36
92
|
def serialize_with_foreign_mapper value, embedded = false
|
37
93
|
if embedded
|
38
94
|
value_mapper = mapper_registry[value.class]
|
@@ -67,7 +123,7 @@ module Perpetuity
|
|
67
123
|
end
|
68
124
|
|
69
125
|
def serialize_reference value
|
70
|
-
unless value.
|
126
|
+
unless value.is_a? PersistedObject
|
71
127
|
mapper_registry[value.class].insert value
|
72
128
|
end
|
73
129
|
{
|
data/lib/perpetuity/version.rb
CHANGED
data/perpetuity.gemspec
CHANGED
@@ -19,6 +19,5 @@ Gem::Specification.new do |s|
|
|
19
19
|
# specify any dependencies here; for example:
|
20
20
|
s.add_development_dependency "rake"
|
21
21
|
s.add_development_dependency "rspec", "~> 2.8.0"
|
22
|
-
s.add_runtime_dependency "
|
23
|
-
s.add_runtime_dependency "bson_ext" unless RUBY_PLATFORM == 'java'
|
22
|
+
s.add_runtime_dependency "moped"
|
24
23
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'support/test_classes'
|
3
|
+
|
4
|
+
describe 'associations with other objects' do
|
5
|
+
let(:user) { User.new }
|
6
|
+
let(:topic) { Topic.new }
|
7
|
+
let(:user_mapper) { Perpetuity[User] }
|
8
|
+
let(:topic_mapper) { Perpetuity[Topic] }
|
9
|
+
|
10
|
+
before do
|
11
|
+
user.name = 'Flump'
|
12
|
+
topic.creator = user
|
13
|
+
topic.title = 'Title'
|
14
|
+
|
15
|
+
user_mapper.insert user
|
16
|
+
topic_mapper.insert topic
|
17
|
+
end
|
18
|
+
|
19
|
+
describe 'referenced relationships' do
|
20
|
+
let(:creator) { topic_mapper.find(topic.id).creator }
|
21
|
+
subject { creator }
|
22
|
+
|
23
|
+
it { should be_a Perpetuity::Reference }
|
24
|
+
its(:klass) { should be User }
|
25
|
+
its(:id) { should be == user.id }
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'can retrieve a one-to-one association' do
|
29
|
+
retrieved_topic = topic_mapper.find(topic.id)
|
30
|
+
|
31
|
+
topic_mapper.load_association! retrieved_topic, :creator
|
32
|
+
retrieved_topic.creator.name.should eq 'Flump'
|
33
|
+
end
|
34
|
+
|
35
|
+
describe 'associations with many objects' do
|
36
|
+
let(:pragprogs) { [User.new('Dave'), User.new('Andy')] }
|
37
|
+
let(:cuke_authors) { [User.new('Matt'), User.new('Aslak')] }
|
38
|
+
let(:pragprog_book) { Book.new("PragProg #{Time.now.to_f}", pragprogs) }
|
39
|
+
let(:cuke_book) { Book.new("Cucumber Book #{Time.now.to_f}", cuke_authors) }
|
40
|
+
let(:book_mapper) { Perpetuity[Book] }
|
41
|
+
|
42
|
+
before do
|
43
|
+
pragprogs.each { |author| Perpetuity[User].insert author }
|
44
|
+
book_mapper.insert pragprog_book
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'can retrieve a one-to-many association' do
|
48
|
+
persisted_book = book_mapper.find(pragprog_book.id)
|
49
|
+
book_mapper.load_association! persisted_book, :authors
|
50
|
+
|
51
|
+
persisted_book.authors.first.name.should be == 'Dave'
|
52
|
+
persisted_book.authors.last.name.should be == 'Andy'
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'can retrieve a many-to-many association' do
|
56
|
+
cuke_authors.each { |author| Perpetuity[User].insert author }
|
57
|
+
book_mapper.insert cuke_book
|
58
|
+
book_ids = [pragprog_book, cuke_book].map(&:id)
|
59
|
+
|
60
|
+
books = book_mapper.select { |book| book.id.in book_ids }.to_a
|
61
|
+
book_mapper.load_association! books, :authors
|
62
|
+
books.map(&:authors).flatten.map(&:name).should include *%w(Dave Andy Matt Aslak)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|