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 CHANGED
@@ -1,6 +1,5 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.9.2
4
3
  - 1.9.3
5
4
  - rbx-19mode
6
5
  - jruby-19mode
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 host: 'mongodb.example.com', db: 'example_db'
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
- # individual mapper configuration goes here
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 that class's mapper class and passing in the criteria. You may also specify more general criteria using the `select` method with a block similar to `Enumerable#select`.
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
- articles = articles.sort(:published_at).reverse
86
- articles = articles.page(2).per_page(10) # built-in pagination
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. If an object references another type of object (such as an article referencing its author, a `User` object), the association is declared just as any other attribute. No special treatment is required.
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 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.
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
- class User
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
- class Article
112
- attr_accessor :author
128
+ Perpetuity.generate_mapper_for Comment do
129
+ attribute :body
130
+ attribute :author
131
+ attribute :timestamp
113
132
  end
133
+ ```
114
134
 
115
- Perpetuity.generate_mapper_for User do
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
- Perpetuity.generate_mapper_for Article do
119
- attribute :author
120
- end
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 allows you to write the following:
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
- article = article_mapper.first
128
- article_mapper.load_association! article, :author
129
- user = article.author
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
- attribute = "@#{attribute}" unless attribute[0] == '@'
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
- object.define_singleton_method :id do
17
- args.first || object.instance_variable_get(:@id)
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
@@ -86,12 +86,12 @@ module Perpetuity
86
86
  end
87
87
 
88
88
  def select &block
89
- query = data_source.class::Query.new(&block).to_db
89
+ query = data_source.query(&block).to_db
90
90
  retrieve query
91
91
  end
92
92
 
93
93
  def find id
94
- retrieve(id: id).first
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, mapper_registry).serialize(object)
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
 
@@ -1,10 +1,11 @@
1
- require 'mongo'
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 :connection, :host, :port, :db, :pool_size, :username, :password
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
- @connection = nil
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
- database.authenticate(@username, @password) if @username and @password
22
- @connection ||= Mongo::MongoClient.new @host, @port, pool_size: @pool_size
26
+ session.login(@username, @password) if @username and @password
27
+ session
23
28
  end
24
29
 
25
30
  def connected?
26
- !!@connection
31
+ !!@session
27
32
  end
28
33
 
29
34
  def database
30
35
  connect unless connected?
31
- @connection.db(@db)
36
+ session.use db
32
37
  end
33
38
 
34
39
  def collection klass
35
- database.collection(klass.to_s)
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
- database.collection(klass.to_s).remove
60
+ collection(klass.to_s).find.remove_all
53
61
  end
54
62
 
55
63
  def first klass
56
- document = database.collection(klass.to_s).find_one
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.from_string(criteria[:id].to_s) rescue criteria[:id]) }
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
- other_options = { limit: options[:limit] }
73
- if options[:page]
74
- other_options = other_options.merge skip: (options[:page] - 1) * options[:limit]
75
- end
76
- cursor = database.collection(klass.to_s).find(criteria, other_options)
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
- sort_cursor(cursor, options).map do |document|
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 sort_cursor cursor, options
85
- return cursor unless options.has_key?(:attribute) &&
86
- options.has_key?(:direction)
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 = [[sort_field, sort_direction]]
91
- cursor.sort(sort_criteria)
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 object, klass=nil
99
- id = object.respond_to?(:id) ? object.id : object
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).remove "_id" => id
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).update({ _id: id }, new_data)
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).index_information
130
- indexes.map do |name, index|
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).create_index [[attribute, order]], unique: unique
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.index_information.select do |name, info|
150
- name =~ /#{index.attribute}/
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).drop_index db_indexes.first.first
168
+ collection(index.collection).indexes.drop db_indexes.first
154
169
  end
155
170
  end
156
171
 
@@ -0,0 +1,7 @@
1
+ module Perpetuity
2
+ module PersistedObject
3
+ def id
4
+ @id
5
+ end
6
+ end
7
+ end
@@ -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 klass, criteria, data_source = Perpetuity.configuration.data_source
11
- @class = klass
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
- if data.is_a?(String) && data.start_with?("\u0004") # if it's marshaled
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, mapper_registry)
9
+ def initialize(mapper)
6
10
  @mapper = mapper
7
- @mapper_registry = mapper_registry
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
- if attrib.embedded?
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.respond_to? :id
126
+ unless value.is_a? PersistedObject
71
127
  mapper_registry[value.class].insert value
72
128
  end
73
129
  {
@@ -1,3 +1,3 @@
1
1
  module Perpetuity
2
- VERSION = "0.4.4"
2
+ VERSION = "0.4.5"
3
3
  end
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 "mongo", ">= 1.8.0"
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