danielharan-mongo_mapper 0.6.5
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 +10 -0
- data/LICENSE +20 -0
- data/README.rdoc +53 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/bin/mmconsole +60 -0
- data/lib/mongo_mapper.rb +134 -0
- data/lib/mongo_mapper/associations.rb +183 -0
- data/lib/mongo_mapper/associations/base.rb +110 -0
- data/lib/mongo_mapper/associations/belongs_to_polymorphic_proxy.rb +34 -0
- data/lib/mongo_mapper/associations/belongs_to_proxy.rb +22 -0
- data/lib/mongo_mapper/associations/many_documents_as_proxy.rb +25 -0
- data/lib/mongo_mapper/associations/many_documents_proxy.rb +127 -0
- data/lib/mongo_mapper/associations/many_embedded_polymorphic_proxy.rb +33 -0
- data/lib/mongo_mapper/associations/many_embedded_proxy.rb +53 -0
- data/lib/mongo_mapper/associations/many_polymorphic_proxy.rb +11 -0
- data/lib/mongo_mapper/associations/many_proxy.rb +6 -0
- data/lib/mongo_mapper/associations/proxy.rb +80 -0
- data/lib/mongo_mapper/callbacks.rb +109 -0
- data/lib/mongo_mapper/dirty.rb +136 -0
- data/lib/mongo_mapper/document.rb +481 -0
- data/lib/mongo_mapper/dynamic_finder.rb +35 -0
- data/lib/mongo_mapper/embedded_document.rb +386 -0
- data/lib/mongo_mapper/finder_options.rb +133 -0
- data/lib/mongo_mapper/key.rb +36 -0
- data/lib/mongo_mapper/observing.rb +50 -0
- data/lib/mongo_mapper/pagination.rb +53 -0
- data/lib/mongo_mapper/rails_compatibility/document.rb +15 -0
- data/lib/mongo_mapper/rails_compatibility/embedded_document.rb +27 -0
- data/lib/mongo_mapper/serialization.rb +54 -0
- data/lib/mongo_mapper/serializers/json_serializer.rb +92 -0
- data/lib/mongo_mapper/support.rb +193 -0
- data/lib/mongo_mapper/validations.rb +41 -0
- data/mongo_mapper.gemspec +171 -0
- data/specs.watchr +32 -0
- data/test/NOTE_ON_TESTING +1 -0
- data/test/functional/associations/test_belongs_to_polymorphic_proxy.rb +55 -0
- data/test/functional/associations/test_belongs_to_proxy.rb +48 -0
- data/test/functional/associations/test_many_documents_as_proxy.rb +246 -0
- data/test/functional/associations/test_many_embedded_polymorphic_proxy.rb +156 -0
- data/test/functional/associations/test_many_embedded_proxy.rb +196 -0
- data/test/functional/associations/test_many_polymorphic_proxy.rb +339 -0
- data/test/functional/associations/test_many_proxy.rb +384 -0
- data/test/functional/test_associations.rb +44 -0
- data/test/functional/test_binary.rb +18 -0
- data/test/functional/test_callbacks.rb +85 -0
- data/test/functional/test_dirty.rb +159 -0
- data/test/functional/test_document.rb +1180 -0
- data/test/functional/test_embedded_document.rb +125 -0
- data/test/functional/test_logger.rb +20 -0
- data/test/functional/test_pagination.rb +95 -0
- data/test/functional/test_rails_compatibility.rb +25 -0
- data/test/functional/test_string_id_compatibility.rb +72 -0
- data/test/functional/test_validations.rb +369 -0
- data/test/models.rb +271 -0
- data/test/support/custom_matchers.rb +55 -0
- data/test/support/timing.rb +16 -0
- data/test/test_helper.rb +27 -0
- data/test/unit/serializers/test_json_serializer.rb +189 -0
- data/test/unit/test_association_base.rb +166 -0
- data/test/unit/test_document.rb +204 -0
- data/test/unit/test_dynamic_finder.rb +125 -0
- data/test/unit/test_embedded_document.rb +718 -0
- data/test/unit/test_finder_options.rb +296 -0
- data/test/unit/test_key.rb +172 -0
- data/test/unit/test_mongo_mapper.rb +65 -0
- data/test/unit/test_observing.rb +101 -0
- data/test/unit/test_pagination.rb +113 -0
- data/test/unit/test_rails_compatibility.rb +49 -0
- data/test/unit/test_serializations.rb +52 -0
- data/test/unit/test_support.rb +342 -0
- data/test/unit/test_time_zones.rb +40 -0
- data/test/unit/test_validations.rb +503 -0
- metadata +233 -0
@@ -0,0 +1,109 @@
|
|
1
|
+
module MongoMapper
|
2
|
+
# This module is mixed into the Document module to provide call-backs before
|
3
|
+
# and after the following events:
|
4
|
+
#
|
5
|
+
# * save
|
6
|
+
# * create
|
7
|
+
# * update
|
8
|
+
# * validation
|
9
|
+
# ** every validation
|
10
|
+
# ** validation when created
|
11
|
+
# ** validation when updated
|
12
|
+
# * destruction
|
13
|
+
#
|
14
|
+
# @see ActiveSupport::Callbacks
|
15
|
+
module Callbacks
|
16
|
+
def self.included(model) #:nodoc:
|
17
|
+
model.class_eval do
|
18
|
+
extend Observable
|
19
|
+
include ActiveSupport::Callbacks
|
20
|
+
|
21
|
+
callbacks = %w(
|
22
|
+
before_save
|
23
|
+
after_save
|
24
|
+
before_create
|
25
|
+
after_create
|
26
|
+
before_update
|
27
|
+
after_update
|
28
|
+
before_validation
|
29
|
+
after_validation
|
30
|
+
before_validation_on_create
|
31
|
+
after_validation_on_create
|
32
|
+
before_validation_on_update
|
33
|
+
after_validation_on_update
|
34
|
+
before_destroy
|
35
|
+
after_destroy
|
36
|
+
)
|
37
|
+
|
38
|
+
define_callbacks(*callbacks)
|
39
|
+
|
40
|
+
callbacks.each do |callback|
|
41
|
+
define_method(callback.to_sym) {}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def valid? #:nodoc:
|
47
|
+
return false if callback(:before_validation) == false
|
48
|
+
result = new? ? callback(:before_validation_on_create) : callback(:before_validation_on_update)
|
49
|
+
return false if false == result
|
50
|
+
|
51
|
+
result = super
|
52
|
+
callback(:after_validation)
|
53
|
+
|
54
|
+
new? ? callback(:after_validation_on_create) : callback(:after_validation_on_update)
|
55
|
+
return result
|
56
|
+
end
|
57
|
+
|
58
|
+
# Here we override the +destroy+ method to allow for the +before_destroy+
|
59
|
+
# and +after_destroy+ call-backs. Note that the +destroy+ call is aborted
|
60
|
+
# if the +before_destroy+ call-back returns +false+.
|
61
|
+
#
|
62
|
+
# @return the result of calling +destroy+ on the document
|
63
|
+
def destroy #:nodoc:
|
64
|
+
return false if callback(:before_destroy) == false
|
65
|
+
result = super
|
66
|
+
callback(:after_destroy)
|
67
|
+
result
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
def callback(method)
|
72
|
+
result = run_callbacks(method) { |result, object| false == result }
|
73
|
+
|
74
|
+
if result != false && respond_to?(method)
|
75
|
+
result = send(method)
|
76
|
+
end
|
77
|
+
|
78
|
+
notify(method)
|
79
|
+
return result
|
80
|
+
end
|
81
|
+
|
82
|
+
def notify(method) #:nodoc:
|
83
|
+
self.class.changed
|
84
|
+
self.class.notify_observers(method, self)
|
85
|
+
end
|
86
|
+
|
87
|
+
def create_or_update #:nodoc:
|
88
|
+
return false if callback(:before_save) == false
|
89
|
+
if result = super
|
90
|
+
callback(:after_save)
|
91
|
+
end
|
92
|
+
result
|
93
|
+
end
|
94
|
+
|
95
|
+
def create #:nodoc:
|
96
|
+
return false if callback(:before_create) == false
|
97
|
+
result = super
|
98
|
+
callback(:after_create)
|
99
|
+
result
|
100
|
+
end
|
101
|
+
|
102
|
+
def update(*args) #:nodoc:
|
103
|
+
return false if callback(:before_update) == false
|
104
|
+
result = super
|
105
|
+
callback(:after_update)
|
106
|
+
result
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module MongoMapper
|
2
|
+
module Dirty
|
3
|
+
DIRTY_SUFFIXES = ['_changed?', '_change', '_will_change!', '_was']
|
4
|
+
|
5
|
+
def method_missing(method, *args, &block)
|
6
|
+
if method.to_s =~ /(_changed\?|_change|_will_change!|_was)$/
|
7
|
+
method_suffix = $1
|
8
|
+
key = method.to_s.gsub(method_suffix, '')
|
9
|
+
|
10
|
+
if key_names.include?(key)
|
11
|
+
case method_suffix
|
12
|
+
when '_changed?'
|
13
|
+
key_changed?(key)
|
14
|
+
when '_change'
|
15
|
+
key_change(key)
|
16
|
+
when '_will_change!'
|
17
|
+
key_will_change!(key)
|
18
|
+
when '_was'
|
19
|
+
key_was(key)
|
20
|
+
end
|
21
|
+
else
|
22
|
+
super
|
23
|
+
end
|
24
|
+
else
|
25
|
+
super
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def changed?
|
30
|
+
!changed_keys.empty?
|
31
|
+
end
|
32
|
+
|
33
|
+
# List of keys with unsaved changes.
|
34
|
+
# person.changed # => []
|
35
|
+
# person.name = 'bob'
|
36
|
+
# person.changed # => ['name']
|
37
|
+
def changed
|
38
|
+
changed_keys.keys
|
39
|
+
end
|
40
|
+
|
41
|
+
# Map of changed attrs => [original value, new value].
|
42
|
+
# person.changes # => {}
|
43
|
+
# person.name = 'bob'
|
44
|
+
# person.changes # => { 'name' => ['bill', 'bob'] }
|
45
|
+
def changes
|
46
|
+
changed.inject({}) { |h, attribute| h[attribute] = key_change(attribute); h }
|
47
|
+
end
|
48
|
+
|
49
|
+
def initialize(attrs={})
|
50
|
+
super(attrs)
|
51
|
+
changed_keys.clear unless new?
|
52
|
+
end
|
53
|
+
|
54
|
+
# Attempts to +save+ the record and clears changed keys if successful.
|
55
|
+
def save(*args)
|
56
|
+
if status = super
|
57
|
+
changed_keys.clear
|
58
|
+
end
|
59
|
+
status
|
60
|
+
end
|
61
|
+
|
62
|
+
# Attempts to <tt>save!</tt> the record and clears changed keys if successful.
|
63
|
+
def save!(*args)
|
64
|
+
status = super
|
65
|
+
changed_keys.clear
|
66
|
+
status
|
67
|
+
end
|
68
|
+
|
69
|
+
# <tt>reload</tt> the record and clears changed keys.
|
70
|
+
# def reload_with_dirty(*args) #:nodoc:
|
71
|
+
# record = reload_without_dirty(*args)
|
72
|
+
# changed_keys.clear
|
73
|
+
# record
|
74
|
+
# end
|
75
|
+
|
76
|
+
private
|
77
|
+
def clone_key_value(attribute_name)
|
78
|
+
value = send(:read_attribute, attribute_name)
|
79
|
+
value.duplicable? ? value.clone : value
|
80
|
+
rescue TypeError, NoMethodError
|
81
|
+
value
|
82
|
+
end
|
83
|
+
|
84
|
+
# Map of change <tt>attr => original value</tt>.
|
85
|
+
def changed_keys
|
86
|
+
@changed_keys ||= {}
|
87
|
+
end
|
88
|
+
|
89
|
+
# Handle <tt>*_changed?</tt> for +method_missing+.
|
90
|
+
def key_changed?(attribute)
|
91
|
+
changed_keys.include?(attribute)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Handle <tt>*_change</tt> for +method_missing+.
|
95
|
+
def key_change(attribute)
|
96
|
+
[changed_keys[attribute], __send__(attribute)] if key_changed?(attribute)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Handle <tt>*_was</tt> for +method_missing+.
|
100
|
+
def key_was(attribute)
|
101
|
+
key_changed?(attribute) ? changed_keys[attribute] : __send__(attribute)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Handle <tt>*_will_change!</tt> for +method_missing+.
|
105
|
+
def key_will_change!(attribute)
|
106
|
+
changed_keys[attribute] = clone_key_value(attribute)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Wrap write_attribute to remember original key value.
|
110
|
+
def write_attribute(attribute, value)
|
111
|
+
attribute = attribute.to_s
|
112
|
+
|
113
|
+
# The key already has an unsaved change.
|
114
|
+
if changed_keys.include?(attribute)
|
115
|
+
old = changed_keys[attribute]
|
116
|
+
changed_keys.delete(attribute) unless value_changed?(attribute, old, value)
|
117
|
+
else
|
118
|
+
old = clone_key_value(attribute)
|
119
|
+
changed_keys[attribute] = old if value_changed?(attribute, old, value)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Carry on.
|
123
|
+
super(attribute, value)
|
124
|
+
end
|
125
|
+
|
126
|
+
def value_changed?(key_name, old, value)
|
127
|
+
key = _keys[key_name]
|
128
|
+
|
129
|
+
if key.number? && value.blank?
|
130
|
+
value = nil
|
131
|
+
end
|
132
|
+
|
133
|
+
old != value
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,481 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module MongoMapper
|
4
|
+
module Document
|
5
|
+
def self.included(model)
|
6
|
+
model.class_eval do
|
7
|
+
include EmbeddedDocument
|
8
|
+
include InstanceMethods
|
9
|
+
include Observing
|
10
|
+
include Callbacks
|
11
|
+
include Dirty
|
12
|
+
include RailsCompatibility::Document
|
13
|
+
extend Validations::Macros
|
14
|
+
extend ClassMethods
|
15
|
+
extend Finders
|
16
|
+
|
17
|
+
def self.per_page
|
18
|
+
25
|
19
|
+
end unless respond_to?(:per_page)
|
20
|
+
end
|
21
|
+
|
22
|
+
descendants << model
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.descendants
|
26
|
+
@descendants ||= Set.new
|
27
|
+
end
|
28
|
+
|
29
|
+
module ClassMethods
|
30
|
+
def key(*args)
|
31
|
+
key = super
|
32
|
+
create_indexes_for(key)
|
33
|
+
key
|
34
|
+
end
|
35
|
+
|
36
|
+
def ensure_index(name_or_array, options={})
|
37
|
+
keys_to_index = if name_or_array.is_a?(Array)
|
38
|
+
name_or_array.map { |pair| [pair[0], pair[1]] }
|
39
|
+
else
|
40
|
+
name_or_array
|
41
|
+
end
|
42
|
+
|
43
|
+
MongoMapper.ensure_index(self, keys_to_index, options)
|
44
|
+
end
|
45
|
+
|
46
|
+
# @overload find(:first, options)
|
47
|
+
# @see Document.first
|
48
|
+
#
|
49
|
+
# @overload find(:last, options)
|
50
|
+
# @see Document.last
|
51
|
+
#
|
52
|
+
# @overload find(:all, options)
|
53
|
+
# @see Document.all
|
54
|
+
#
|
55
|
+
# @overload find(ids, options)
|
56
|
+
#
|
57
|
+
# @raise DocumentNotFound raised when no ID or arguments are provided
|
58
|
+
def find!(*args)
|
59
|
+
options = args.extract_options!
|
60
|
+
case args.first
|
61
|
+
when :first then first(options)
|
62
|
+
when :last then last(options)
|
63
|
+
when :all then find_every(options)
|
64
|
+
when Array then find_some(args, options)
|
65
|
+
else
|
66
|
+
case args.size
|
67
|
+
when 0
|
68
|
+
raise DocumentNotFound, "Couldn't find without an ID"
|
69
|
+
when 1
|
70
|
+
find_one!(options.merge({:_id => args[0]}))
|
71
|
+
else
|
72
|
+
find_some(args, options)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def find(*args)
|
78
|
+
find!(*args)
|
79
|
+
rescue DocumentNotFound
|
80
|
+
nil
|
81
|
+
end
|
82
|
+
|
83
|
+
def paginate(options)
|
84
|
+
per_page = options.delete(:per_page) || self.per_page
|
85
|
+
page = options.delete(:page)
|
86
|
+
total_entries = count(options)
|
87
|
+
pagination = Pagination::PaginationProxy.new(total_entries, page, per_page)
|
88
|
+
|
89
|
+
options.merge!(:limit => pagination.limit, :skip => pagination.skip)
|
90
|
+
pagination.subject = find_every(options)
|
91
|
+
pagination
|
92
|
+
end
|
93
|
+
|
94
|
+
# @param [Hash] options any conditions understood by
|
95
|
+
# FinderOptions.to_mongo_criteria
|
96
|
+
#
|
97
|
+
# @return the first document in the ordered collection as described by
|
98
|
+
# +options+
|
99
|
+
#
|
100
|
+
# @see FinderOptions
|
101
|
+
def first(options={})
|
102
|
+
find_one(options)
|
103
|
+
end
|
104
|
+
|
105
|
+
# @param [Hash] options any conditions understood by
|
106
|
+
# FinderOptions.to_mongo_criteria
|
107
|
+
# @option [String] :order this *mandatory* option describes how to
|
108
|
+
# identify the ordering of the documents in your collection. Note that
|
109
|
+
# the *last* document in this collection will be selected.
|
110
|
+
#
|
111
|
+
# @return the last document in the ordered collection as described by
|
112
|
+
# +options+
|
113
|
+
#
|
114
|
+
# @raise Exception when no <tt>:order</tt> option has been defined
|
115
|
+
def last(options={})
|
116
|
+
raise ':order option must be provided when using last' if options[:order].blank?
|
117
|
+
find_one(options.merge(:order => invert_order_clause(options[:order])))
|
118
|
+
end
|
119
|
+
|
120
|
+
# @param [Hash] options any conditions understood by
|
121
|
+
# FinderOptions.to_mongo_criteria
|
122
|
+
#
|
123
|
+
# @return [Array] all documents in your collection that match the
|
124
|
+
# provided conditions
|
125
|
+
#
|
126
|
+
# @see FinderOptions
|
127
|
+
def all(options={})
|
128
|
+
find_every(options)
|
129
|
+
end
|
130
|
+
|
131
|
+
def find_by_id(id)
|
132
|
+
find_one(:_id => id)
|
133
|
+
end
|
134
|
+
|
135
|
+
def count(options={})
|
136
|
+
collection.find(to_criteria(options)).count
|
137
|
+
end
|
138
|
+
|
139
|
+
def exists?(options={})
|
140
|
+
!count(options).zero?
|
141
|
+
end
|
142
|
+
|
143
|
+
# @overload create(doc_attributes)
|
144
|
+
# Create a single new document
|
145
|
+
# @param [Hash] doc_attributes key/value pairs to create a new
|
146
|
+
# document
|
147
|
+
#
|
148
|
+
# @overload create(docs_attributes)
|
149
|
+
# Create many new documents
|
150
|
+
# @param [Array<Hash>] provide many Hashes of key/value pairs to create
|
151
|
+
# multiple documents
|
152
|
+
#
|
153
|
+
# @example Creating a single document
|
154
|
+
# MyModel.create({ :foo => "bar" })
|
155
|
+
#
|
156
|
+
# @example Creating multiple documents
|
157
|
+
# MyModel.create([{ :foo => "bar" }, { :foo => "baz" })
|
158
|
+
#
|
159
|
+
# @return [Boolean] when a document is successfully created, +true+ will
|
160
|
+
# be returned. If a document fails to create, +false+ will be returned.
|
161
|
+
def create(*docs)
|
162
|
+
initialize_each(*docs) { |doc| doc.save }
|
163
|
+
end
|
164
|
+
|
165
|
+
# @see Document.create
|
166
|
+
#
|
167
|
+
# @raise [DocumentNotValid] raised if a document fails to create
|
168
|
+
def create!(*docs)
|
169
|
+
initialize_each(*docs) { |doc| doc.save! }
|
170
|
+
end
|
171
|
+
|
172
|
+
# @overload update(id, attributes)
|
173
|
+
# Update a single document
|
174
|
+
# @param id the ID of the document you wish to update
|
175
|
+
# @param [Hash] attributes the key to update on the document with a new
|
176
|
+
# value
|
177
|
+
#
|
178
|
+
# @overload update(ids_and_attributes)
|
179
|
+
# Update multiple documents
|
180
|
+
# @param [Hash] ids_and_attributes each key is the ID of some document
|
181
|
+
# you wish to update. The value each key points toward are those
|
182
|
+
# applied to the target document
|
183
|
+
#
|
184
|
+
# @example Updating single document
|
185
|
+
# Person.update(1, {:foo => 'bar'})
|
186
|
+
#
|
187
|
+
# @example Updating multiple documents at once:
|
188
|
+
# Person.update({'1' => {:foo => 'bar'}, '2' => {:baz => 'wick'}})
|
189
|
+
def update(*args)
|
190
|
+
if args.length == 1
|
191
|
+
update_multiple(args[0])
|
192
|
+
else
|
193
|
+
id, attributes = args
|
194
|
+
update_single(id, attributes)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Removes ("deletes") one or many documents from the collection. Note
|
199
|
+
# that this will bypass any +destroy+ hooks defined by your class.
|
200
|
+
#
|
201
|
+
# @param [Array] ids the ID or IDs of the records you wish to delete
|
202
|
+
def delete(*ids)
|
203
|
+
collection.remove(to_criteria(:_id => ids.flatten))
|
204
|
+
end
|
205
|
+
|
206
|
+
def delete_all(options={})
|
207
|
+
collection.remove(to_criteria(options))
|
208
|
+
end
|
209
|
+
|
210
|
+
# Iterates over each document found by the provided IDs and calls their
|
211
|
+
# +destroy+ method. This has the advantage of processing your document's
|
212
|
+
# +destroy+ call-backs.
|
213
|
+
#
|
214
|
+
# @overload destroy(id)
|
215
|
+
# Destroy a single document by ID
|
216
|
+
# @param id the ID of the document to destroy
|
217
|
+
#
|
218
|
+
# @overload destroy(ids)
|
219
|
+
# Destroy many documents by their IDs
|
220
|
+
# @param [Array] the IDs of each document you wish to destroy
|
221
|
+
#
|
222
|
+
# @example Destroying a single document
|
223
|
+
# Person.destroy("34")
|
224
|
+
#
|
225
|
+
# @example Destroying multiple documents
|
226
|
+
# Person.destroy("34", "45", ..., "54")
|
227
|
+
#
|
228
|
+
# # OR...
|
229
|
+
#
|
230
|
+
# Person.destroy(["34", "45", ..., "54"])
|
231
|
+
def destroy(*ids)
|
232
|
+
find_some(ids.flatten).each(&:destroy)
|
233
|
+
end
|
234
|
+
|
235
|
+
def destroy_all(options={})
|
236
|
+
all(options).each(&:destroy)
|
237
|
+
end
|
238
|
+
|
239
|
+
# @overload connection()
|
240
|
+
# @return [Mongo::Connection] the connection used by your document class
|
241
|
+
#
|
242
|
+
# @overload connection(mongo_connection)
|
243
|
+
# @param [Mongo::Connection] mongo_connection a new connection for your
|
244
|
+
# document class to use
|
245
|
+
# @return [Mongo::Connection] a new Mongo::Connection for yoru document
|
246
|
+
# class
|
247
|
+
def connection(mongo_connection=nil)
|
248
|
+
if mongo_connection.nil?
|
249
|
+
@connection ||= MongoMapper.connection
|
250
|
+
else
|
251
|
+
@connection = mongo_connection
|
252
|
+
end
|
253
|
+
@connection
|
254
|
+
end
|
255
|
+
|
256
|
+
# Changes the database name from the default to whatever you want
|
257
|
+
#
|
258
|
+
# @param [#to_s] name the new database name to use.
|
259
|
+
def set_database_name(name)
|
260
|
+
@database_name = name
|
261
|
+
end
|
262
|
+
|
263
|
+
# Returns the database name
|
264
|
+
#
|
265
|
+
# @return [String] the database name
|
266
|
+
def database_name
|
267
|
+
@database_name
|
268
|
+
end
|
269
|
+
|
270
|
+
# Returns the database the document should use. Defaults to
|
271
|
+
# MongoMapper.database if other database is not set.
|
272
|
+
#
|
273
|
+
# @return [Mongo::DB] the mongo database instance
|
274
|
+
def database
|
275
|
+
if database_name.nil?
|
276
|
+
MongoMapper.database
|
277
|
+
else
|
278
|
+
connection.db(database_name)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
# Changes the collection name from the default to whatever you want
|
283
|
+
#
|
284
|
+
# @param [#to_s] name the new collection name to use.
|
285
|
+
def set_collection_name(name)
|
286
|
+
@collection_name = name
|
287
|
+
end
|
288
|
+
|
289
|
+
# Returns the collection name, if not set, defaults to class name tableized
|
290
|
+
#
|
291
|
+
# @return [String] the collection name, if not set, defaults to class
|
292
|
+
# name tableized
|
293
|
+
def collection_name
|
294
|
+
@collection_name ||= self.to_s.tableize.gsub(/\//, '.')
|
295
|
+
end
|
296
|
+
|
297
|
+
# @return the Mongo Ruby driver +collection+ object
|
298
|
+
def collection
|
299
|
+
database.collection(collection_name)
|
300
|
+
end
|
301
|
+
|
302
|
+
# Defines a +created_at+ and +updated_at+ attribute (with a +Time+
|
303
|
+
# value) on your document. These attributes are updated by an
|
304
|
+
# injected +update_timestamps+ +before_save+ hook.
|
305
|
+
def timestamps!
|
306
|
+
key :created_at, Time
|
307
|
+
key :updated_at, Time
|
308
|
+
class_eval { before_save :update_timestamps }
|
309
|
+
end
|
310
|
+
|
311
|
+
def single_collection_inherited?
|
312
|
+
keys.has_key?('_type') && single_collection_inherited_superclass?
|
313
|
+
end
|
314
|
+
|
315
|
+
def single_collection_inherited_superclass?
|
316
|
+
superclass.respond_to?(:keys) && superclass.keys.has_key?('_type')
|
317
|
+
end
|
318
|
+
|
319
|
+
protected
|
320
|
+
def method_missing(method, *args)
|
321
|
+
finder = DynamicFinder.new(method)
|
322
|
+
|
323
|
+
if finder.found?
|
324
|
+
meta_def(finder.method) { |*args| dynamic_find(finder, args) }
|
325
|
+
send(finder.method, *args)
|
326
|
+
else
|
327
|
+
super
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
private
|
332
|
+
def create_indexes_for(key)
|
333
|
+
ensure_index key.name if key.options[:index]
|
334
|
+
end
|
335
|
+
|
336
|
+
def initialize_each(*docs)
|
337
|
+
instances = []
|
338
|
+
docs = [{}] if docs.blank?
|
339
|
+
docs.flatten.each do |attrs|
|
340
|
+
doc = initialize_doc(attrs)
|
341
|
+
yield(doc)
|
342
|
+
instances << doc
|
343
|
+
end
|
344
|
+
instances.size == 1 ? instances[0] : instances
|
345
|
+
end
|
346
|
+
|
347
|
+
def find_every(options)
|
348
|
+
criteria, options = to_finder_options(options)
|
349
|
+
collection.find(criteria, options).to_a.map do |doc|
|
350
|
+
initialize_doc(doc)
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
def find_some(ids, options={})
|
355
|
+
ids = ids.flatten.compact.uniq
|
356
|
+
documents = find_every(options.merge(:_id => ids))
|
357
|
+
|
358
|
+
if ids.size == documents.size
|
359
|
+
documents
|
360
|
+
else
|
361
|
+
raise DocumentNotFound, "Couldn't find all of the ids (#{ids.to_sentence}). Found #{documents.size}, but was expecting #{ids.size}"
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
def find_one(options={})
|
366
|
+
criteria, options = to_finder_options(options)
|
367
|
+
if doc = collection.find_one(criteria, options)
|
368
|
+
initialize_doc(doc)
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
def find_one!(options={})
|
373
|
+
find_one(options) || raise(DocumentNotFound, "Document match #{options.inspect} does not exist in #{collection.name} collection")
|
374
|
+
end
|
375
|
+
|
376
|
+
def invert_order_clause(order)
|
377
|
+
order.split(',').map do |order_segment|
|
378
|
+
if order_segment =~ /\sasc/i
|
379
|
+
order_segment.sub /\sasc/i, ' desc'
|
380
|
+
elsif order_segment =~ /\sdesc/i
|
381
|
+
order_segment.sub /\sdesc/i, ' asc'
|
382
|
+
else
|
383
|
+
"#{order_segment.strip} desc"
|
384
|
+
end
|
385
|
+
end.join(',')
|
386
|
+
end
|
387
|
+
|
388
|
+
def update_single(id, attrs)
|
389
|
+
if id.blank? || attrs.blank? || !attrs.is_a?(Hash)
|
390
|
+
raise ArgumentError, "Updating a single document requires an id and a hash of attributes"
|
391
|
+
end
|
392
|
+
|
393
|
+
doc = find(id)
|
394
|
+
doc.update_attributes(attrs)
|
395
|
+
doc
|
396
|
+
end
|
397
|
+
|
398
|
+
def update_multiple(docs)
|
399
|
+
unless docs.is_a?(Hash)
|
400
|
+
raise ArgumentError, "Updating multiple documents takes 1 argument and it must be hash"
|
401
|
+
end
|
402
|
+
|
403
|
+
instances = []
|
404
|
+
docs.each_pair { |id, attrs| instances << update(id, attrs) }
|
405
|
+
instances
|
406
|
+
end
|
407
|
+
|
408
|
+
def to_criteria(options={})
|
409
|
+
FinderOptions.new(self, options).criteria
|
410
|
+
end
|
411
|
+
|
412
|
+
def to_finder_options(options={})
|
413
|
+
FinderOptions.new(self, options).to_a
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
module InstanceMethods
|
418
|
+
def collection
|
419
|
+
self.class.collection
|
420
|
+
end
|
421
|
+
|
422
|
+
def new?
|
423
|
+
read_attribute('_id').blank? || using_custom_id?
|
424
|
+
end
|
425
|
+
|
426
|
+
def save(perform_validations=true)
|
427
|
+
!perform_validations || valid? ? create_or_update : false
|
428
|
+
end
|
429
|
+
|
430
|
+
def save!
|
431
|
+
save || raise(DocumentNotValid.new(self))
|
432
|
+
end
|
433
|
+
|
434
|
+
def destroy
|
435
|
+
return false if frozen?
|
436
|
+
self.class.delete(_id) unless new?
|
437
|
+
freeze
|
438
|
+
end
|
439
|
+
|
440
|
+
def reload
|
441
|
+
self.class.find(_id)
|
442
|
+
end
|
443
|
+
|
444
|
+
private
|
445
|
+
def create_or_update
|
446
|
+
result = new? ? create : update
|
447
|
+
result != false
|
448
|
+
end
|
449
|
+
|
450
|
+
def create
|
451
|
+
assign_id
|
452
|
+
save_to_collection
|
453
|
+
end
|
454
|
+
|
455
|
+
def assign_id
|
456
|
+
if read_attribute(:_id).blank?
|
457
|
+
write_attribute :_id, Mongo::ObjectID.new
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
def update
|
462
|
+
save_to_collection
|
463
|
+
end
|
464
|
+
|
465
|
+
def save_to_collection
|
466
|
+
clear_custom_id_flag
|
467
|
+
collection.save(to_mongo)
|
468
|
+
end
|
469
|
+
|
470
|
+
def update_timestamps
|
471
|
+
now = Time.now.utc
|
472
|
+
write_attribute('created_at', now) if new?
|
473
|
+
write_attribute('updated_at', now)
|
474
|
+
end
|
475
|
+
|
476
|
+
def clear_custom_id_flag
|
477
|
+
@using_custom_id = nil
|
478
|
+
end
|
479
|
+
end
|
480
|
+
end # Document
|
481
|
+
end # MongoMapper
|