langalex-couch_potato 0.1 → 0.1.1
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/README.textile +340 -0
- data/VERSION.yml +4 -0
- data/lib/couch_potato/ordering.rb +5 -9
- data/lib/couch_potato/persistence.rb +32 -7
- data/lib/couch_potato/persistence/belongs_to_property.rb +17 -3
- data/lib/couch_potato/persistence/callbacks.rb +9 -0
- data/lib/couch_potato/persistence/custom_view.rb +41 -0
- data/lib/couch_potato/persistence/dirty_attributes.rb +19 -0
- data/lib/couch_potato/persistence/external_collection.rb +31 -2
- data/lib/couch_potato/persistence/external_has_many_property.rb +4 -0
- data/lib/couch_potato/persistence/finder.rb +21 -65
- data/lib/couch_potato/persistence/inline_has_many_property.rb +4 -0
- data/lib/couch_potato/persistence/json.rb +1 -1
- data/lib/couch_potato/persistence/simple_property.rb +29 -1
- data/lib/couch_potato/persistence/view_query.rb +81 -0
- data/lib/couch_potato/versioning.rb +1 -1
- data/spec/attributes_spec.rb +35 -15
- data/spec/belongs_to_spec.rb +18 -0
- data/spec/callbacks_spec.rb +31 -12
- data/spec/create_spec.rb +5 -0
- data/spec/custom_view_spec.rb +44 -0
- data/spec/destroy_spec.rb +4 -0
- data/spec/dirty_attributes_spec.rb +82 -0
- data/spec/find_spec.rb +11 -3
- data/spec/finder_spec.rb +10 -0
- data/spec/has_many_spec.rb +64 -1
- data/spec/ordering_spec.rb +1 -0
- data/spec/property_spec.rb +4 -0
- data/spec/reload_spec.rb +4 -0
- data/spec/spec_helper.rb +2 -1
- data/spec/unit/external_collection_spec.rb +84 -0
- data/spec/unit/finder_spec.rb +10 -0
- data/spec/unit/view_query_spec.rb +10 -0
- data/spec/update_spec.rb +6 -0
- data/spec/versioning_spec.rb +1 -0
- metadata +21 -48
- data/CREDITS +0 -3
- data/init.rb +0 -5
data/README.textile
ADDED
@@ -0,0 +1,340 @@
|
|
1
|
+
h2. Couch Potato
|
2
|
+
|
3
|
+
... is a persistence layer written in ruby for CouchDB.
|
4
|
+
|
5
|
+
h3. Mission
|
6
|
+
|
7
|
+
The goal of Couch Potato is to create a migration path for users of ActiveRecord and other object relational mappers to port their applications to CouchDB. It therefore offers a basic set of the functionality provided by most ORMs and adds functionality unique to CouchDB on top.
|
8
|
+
|
9
|
+
h3. Core Features
|
10
|
+
|
11
|
+
Couch Potato is a work in progress so this list will hopefully grow over time.
|
12
|
+
|
13
|
+
* persisting objects by including the CouchPotato::Persistence module
|
14
|
+
* has_many/belongs_to relationships between persistant objects
|
15
|
+
* atomic write operations spanning multiple objects using bulk save queues
|
16
|
+
* extensive spec suite
|
17
|
+
* included versioning support via the CouchPotato::Versioning module
|
18
|
+
* included ordered lists support via the CouchPotato::Ordering module
|
19
|
+
|
20
|
+
h3. Installation
|
21
|
+
|
22
|
+
Couch Potato is hosted as a gem on github which you can install like this:
|
23
|
+
|
24
|
+
sudo gem source --add http://gems.github.com # if you haven't alread
|
25
|
+
sudo gem install langalex-couch_potato
|
26
|
+
|
27
|
+
h4. Using with your ruby application:
|
28
|
+
|
29
|
+
require 'rubygems'
|
30
|
+
gem 'couch_potato'
|
31
|
+
require 'couch_potato'
|
32
|
+
CouchPotato::Config.database_name = 'name of the db'
|
33
|
+
|
34
|
+
Alternatively you can download or clone the source repository and then require lib/couhc_potato.rb.
|
35
|
+
|
36
|
+
h4. Using with Rails
|
37
|
+
|
38
|
+
Add to your config/environment.rb:
|
39
|
+
|
40
|
+
config.gem 'langalex-couch_potato', :lib => 'couch_potato', :source => 'http://gems.github.com'
|
41
|
+
|
42
|
+
Then create a config/couchdb.yml:
|
43
|
+
|
44
|
+
development: development_db_name
|
45
|
+
test: test_db_name
|
46
|
+
production: production_db_name
|
47
|
+
|
48
|
+
Alternatively you can also install Couch Potato directly as a plugin.
|
49
|
+
|
50
|
+
h3. Introduction
|
51
|
+
|
52
|
+
This is a basic tutorial on how to use Couch Potato. If you want to know all the details feel free to read the specs.
|
53
|
+
|
54
|
+
h4. Save, load objects
|
55
|
+
|
56
|
+
First you need a class.
|
57
|
+
|
58
|
+
class User
|
59
|
+
end
|
60
|
+
|
61
|
+
To make instances of this class persistent include the persistence module:
|
62
|
+
|
63
|
+
class User
|
64
|
+
include CouchPotato::Persistence
|
65
|
+
end
|
66
|
+
|
67
|
+
If you want to store any properties you have to declare them:
|
68
|
+
|
69
|
+
class User
|
70
|
+
include CouchPotato::Persistence
|
71
|
+
|
72
|
+
property :name
|
73
|
+
end
|
74
|
+
|
75
|
+
Now you can save your objects:
|
76
|
+
|
77
|
+
user = User.new :name => 'joe'
|
78
|
+
user.save # or save!
|
79
|
+
|
80
|
+
Properties:
|
81
|
+
|
82
|
+
user.name # => 'joe'
|
83
|
+
user.name = {:first => ['joe', 'joey'], :last => 'doe', :middle => 'J'} # you can set any ruby object that responds_to :to_json (includes all core objects)
|
84
|
+
user._id # => "02097f33a0046123f1ebc0ebb6937269"
|
85
|
+
user._rev # => "2769180384"
|
86
|
+
user.created_at # => Fri Oct 24 19:05:54 +0200 2008
|
87
|
+
user.updated_at # => Fri Oct 24 19:05:54 +0200 2008
|
88
|
+
user.new_document? # => false, for compatibility new_record? will work as well
|
89
|
+
|
90
|
+
You can of course also retrieve your instance:
|
91
|
+
|
92
|
+
User.get "02097f33a0046123f1ebc0ebb6937269" # => <#User 0x3075>
|
93
|
+
|
94
|
+
h4. Object validations
|
95
|
+
|
96
|
+
Couch Potato uses the validatable library for vaidation (http://validatable.rubyforge.org/)\
|
97
|
+
|
98
|
+
class User
|
99
|
+
property :name
|
100
|
+
validates_presence_of :name
|
101
|
+
end
|
102
|
+
|
103
|
+
user = User.new
|
104
|
+
user.valid? # => false
|
105
|
+
user.errors.on(:name) # => [:name, 'can't be blank']
|
106
|
+
|
107
|
+
h4. Finding stuff
|
108
|
+
|
109
|
+
For running basic finds (e.g. creating/querying views) you can either use the CouchPotato::Persistence::Finder class:
|
110
|
+
|
111
|
+
joe = User.create! :first_name => 'joe', :position => 1
|
112
|
+
jane = User.create! :first_name => 'jane', :position => 2
|
113
|
+
Finder.new.find User, :first_name => 'joe' # => [joe]
|
114
|
+
Finder.new.find User, :first_name => ['joe', 'jane'] # => [joe, jane]
|
115
|
+
Finder.new.find User, :position => 1..2 # => [joe, jane]
|
116
|
+
|
117
|
+
You can also count:
|
118
|
+
|
119
|
+
user = User.create! :first_name => 'joe'
|
120
|
+
Finder.new.count User, :first_name => 'joe' # => 1
|
121
|
+
|
122
|
+
Or you can call first/all/count on a class persistent class which will call the Finder.find, e.g.:
|
123
|
+
|
124
|
+
User.first :login => 'alex'
|
125
|
+
User.all
|
126
|
+
User.count :activated => true
|
127
|
+
|
128
|
+
Be warned though that executing these finder methods will generate a new view for every new combination of class and attribute names it gets called with, so you really don't want to use this in a console on a production system.
|
129
|
+
|
130
|
+
Support for more sophisticated views will be added later.
|
131
|
+
|
132
|
+
h4. Associations
|
133
|
+
|
134
|
+
As of now has_many and belongs_to are supported. By default the associated objects are stored in separate documents linked via foreign keys just like in relational databases.
|
135
|
+
|
136
|
+
class User
|
137
|
+
has_many :addresses, :dependent => :destroy
|
138
|
+
end
|
139
|
+
|
140
|
+
class Address
|
141
|
+
belongs_to :user
|
142
|
+
property :street
|
143
|
+
end
|
144
|
+
|
145
|
+
user = User.new
|
146
|
+
user.addresses.build :street => 'potato way'
|
147
|
+
user.addresses.first # => <#Address 0x987>
|
148
|
+
user.addresses.create! # raises an exception as street is blank
|
149
|
+
user.addresses.first.user == user # => true
|
150
|
+
|
151
|
+
As CouchDB can not only store flat structures you also store associations inline:
|
152
|
+
|
153
|
+
class User
|
154
|
+
has_many :addresses, :stored => :inline
|
155
|
+
end
|
156
|
+
|
157
|
+
This will store the addresses of the user as an array within your CouchDB document.
|
158
|
+
|
159
|
+
You can also find stuff on associations (not the :stored => :inline):
|
160
|
+
|
161
|
+
user.addresses.all :street => 'potato way'
|
162
|
+
|
163
|
+
... returns only adresses belonging to this user instance and also matching the given conditions. You can call #count a well, #first and #last are not implemented here since those collide with the methods in Enumerable.
|
164
|
+
|
165
|
+
h4. callbacks
|
166
|
+
|
167
|
+
Couch Potato supports the usual lifecycle callbacks known from ActiveRecord:
|
168
|
+
|
169
|
+
class User
|
170
|
+
include CouchPotato::Persistence
|
171
|
+
|
172
|
+
before_create :do_something_before_create
|
173
|
+
after_update :do_something_else
|
174
|
+
end
|
175
|
+
|
176
|
+
This will call the method do_something_before_create before creating an object and do_something_else after updating one. Supported callbacks are: :before_validation_on_create, :before_validation_on_update, :before_validation_on_save, :before_create, :after_create, :before_update, :after_update, :before_save, :after_save, :before_destroy, :after_destroy
|
177
|
+
|
178
|
+
If you want to do any CouchDB update/create/delete operation in your callback methods...
|
179
|
+
|
180
|
+
class User
|
181
|
+
include CouchPotato::Persistence
|
182
|
+
has_many :comments
|
183
|
+
|
184
|
+
before_update :create_a_comment
|
185
|
+
|
186
|
+
private
|
187
|
+
def create_a_comment
|
188
|
+
comments.create :body => 'i was updated'
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
... and you want the entire operation including its hooks to be atomic you can do this:
|
193
|
+
|
194
|
+
class User
|
195
|
+
include CouchPotato::Persistence
|
196
|
+
has_many :comments
|
197
|
+
|
198
|
+
before_update :create_a_comment
|
199
|
+
|
200
|
+
private
|
201
|
+
def create_a_comment
|
202
|
+
bulk_save_queue << Comment.new(:body => 'i was updated')
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
h4. Custom Views
|
207
|
+
|
208
|
+
*This is still in very early in development and of limited use*
|
209
|
+
|
210
|
+
This is useful if you have a set of documents that has not been created with Couch Potato, for example doesn't have a ruby_class attribute but you still want to be able to load those documents as instances of a Couch Potato class.
|
211
|
+
|
212
|
+
Example: Assuming you have a document like this:
|
213
|
+
|
214
|
+
{name: "joe"}
|
215
|
+
|
216
|
+
And a class:
|
217
|
+
|
218
|
+
class User
|
219
|
+
include CouchPotato::Persistence
|
220
|
+
property :name
|
221
|
+
end
|
222
|
+
|
223
|
+
To be able to load that user all you have to do is this:
|
224
|
+
|
225
|
+
class User
|
226
|
+
...
|
227
|
+
|
228
|
+
view :everyone
|
229
|
+
end
|
230
|
+
|
231
|
+
User.everyone
|
232
|
+
|
233
|
+
This will load all documents in your database as instances of User and assign their _id and name attributes.
|
234
|
+
|
235
|
+
If you have larger structures and you only want to load some attributes you can customize the view:
|
236
|
+
|
237
|
+
{name: "joe", bio: "52 pages of text ...."}
|
238
|
+
|
239
|
+
class User
|
240
|
+
property :name
|
241
|
+
property :bio
|
242
|
+
|
243
|
+
view :everyone, :properties => [:name]
|
244
|
+
end
|
245
|
+
|
246
|
+
User.everyone.first.name # => "joe"
|
247
|
+
User.everyone.first.bio # => nil
|
248
|
+
|
249
|
+
|
250
|
+
h4. Versioning
|
251
|
+
|
252
|
+
Couch Potato supports versioning your objects, very similar to the popular acts_as_versioned plugin for ActiveRecord. To use it include the module:
|
253
|
+
|
254
|
+
class Document
|
255
|
+
include CouchPotato::Persistence
|
256
|
+
include CouchPotato::Versioning
|
257
|
+
end
|
258
|
+
|
259
|
+
After that your object will have a version that gets incremented on each save.
|
260
|
+
|
261
|
+
doc = Document.create
|
262
|
+
doc.version # => 1
|
263
|
+
doc.save
|
264
|
+
doc.version # => 2
|
265
|
+
|
266
|
+
You can access the older versions via the versions method.
|
267
|
+
|
268
|
+
doc.versions.first.version # => 1
|
269
|
+
|
270
|
+
When passing a version number the version method will only return that version:
|
271
|
+
|
272
|
+
doc.versions(1).version # => 1
|
273
|
+
|
274
|
+
You can set a condition for when to create a new version:
|
275
|
+
|
276
|
+
class Document
|
277
|
+
attr_accessor :update_version
|
278
|
+
include CouchPotato::Persistence
|
279
|
+
include CouchPotato::Versioning
|
280
|
+
|
281
|
+
set_version_condition lambda {|doc| doc.update_version}
|
282
|
+
end
|
283
|
+
|
284
|
+
doc = Document.create
|
285
|
+
doc.update_version = false
|
286
|
+
doc.version # => 1
|
287
|
+
doc.save
|
288
|
+
doc.version # => 1
|
289
|
+
doc.update_version = true
|
290
|
+
doc.save
|
291
|
+
doc.version # => 2
|
292
|
+
|
293
|
+
h4. Ordered Lists
|
294
|
+
|
295
|
+
Couch Potato supports ordered lists for has_many relationships (with the :stored => :separately option only), very similar to the popular acts_as_list plugin for ActiveRecord. To use it include the module:
|
296
|
+
|
297
|
+
class PersistenArray
|
298
|
+
include CouchPotato::Persistence
|
299
|
+
has_many :items
|
300
|
+
end
|
301
|
+
|
302
|
+
class Item
|
303
|
+
include CouchPotato::Ordering
|
304
|
+
belongs_to :persistent_array
|
305
|
+
set_ordering_scope :persistent_array_id
|
306
|
+
end
|
307
|
+
|
308
|
+
array = PersistenArray.new
|
309
|
+
item1 = array.items.create!
|
310
|
+
item1.position # => 1
|
311
|
+
item2 = array.items.create!
|
312
|
+
item2.position # => 2
|
313
|
+
|
314
|
+
You can move items up and down simply by changing the position:
|
315
|
+
|
316
|
+
item2.position = 1
|
317
|
+
item2.save!
|
318
|
+
item1.position # => 2
|
319
|
+
|
320
|
+
And you can insert new items at any position you want:
|
321
|
+
|
322
|
+
item3 = array.items.create! :position => 2
|
323
|
+
item1.position # => 3
|
324
|
+
|
325
|
+
And remove:
|
326
|
+
|
327
|
+
item3.destroy
|
328
|
+
item1.position # => 2
|
329
|
+
|
330
|
+
h3. Helping out
|
331
|
+
|
332
|
+
Please fix bugs, add more specs, implement new features by forking the github repo at http://github.com/langalex/couch_potato.
|
333
|
+
|
334
|
+
You can run all the specs by calling 'rake spec_unit' and 'rake spec_functional' in the root folder of Couch Potato. The specs require a running CouchDB instance at http://localhost:5984
|
335
|
+
|
336
|
+
I will only accept patches that are covered by specs - sorry.
|
337
|
+
|
338
|
+
h3. Contact
|
339
|
+
|
340
|
+
If you have any questions/suggestions etc. please contact me at alex at upstream-berlin.com or @langalex on twitter.
|
data/VERSION.yml
ADDED
@@ -71,15 +71,11 @@ module CouchPotato
|
|
71
71
|
end
|
72
72
|
|
73
73
|
module ExternalCollectionOrderedFindExtension
|
74
|
-
def
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
else
|
80
|
-
@items ||= CouchPotato::Persistence::Finder.new.find @item_class, @owner_id_attribute_name => owner_id
|
81
|
-
end
|
82
|
-
end
|
74
|
+
def items
|
75
|
+
if @item_class.property_names.include?(:position)
|
76
|
+
super @item_class, @owner_id_attribute_name => owner_id, :position => 1..CouchPotato::Ordering::MAX
|
77
|
+
else
|
78
|
+
super
|
83
79
|
end
|
84
80
|
end
|
85
81
|
end
|
@@ -8,6 +8,9 @@ require File.dirname(__FILE__) + '/persistence/callbacks'
|
|
8
8
|
require File.dirname(__FILE__) + '/persistence/json'
|
9
9
|
require File.dirname(__FILE__) + '/persistence/bulk_save_queue'
|
10
10
|
require File.dirname(__FILE__) + '/persistence/find'
|
11
|
+
require File.dirname(__FILE__) + '/persistence/dirty_attributes'
|
12
|
+
require File.dirname(__FILE__) + '/persistence/custom_view'
|
13
|
+
require File.dirname(__FILE__) + '/persistence/view_query'
|
11
14
|
|
12
15
|
module CouchPotato
|
13
16
|
module Persistence
|
@@ -17,7 +20,7 @@ module CouchPotato
|
|
17
20
|
|
18
21
|
def self.included(base)
|
19
22
|
base.send :extend, ClassMethods, Find
|
20
|
-
base.send :include, Callbacks, Properties, Validatable, Json
|
23
|
+
base.send :include, Callbacks, Properties, Validatable, Json, DirtyAttributes, CustomView
|
21
24
|
base.class_eval do
|
22
25
|
attr_accessor :_id, :_rev, :_attachments, :_deleted, :created_at, :updated_at
|
23
26
|
attr_reader :bulk_save_queue
|
@@ -38,6 +41,11 @@ module CouchPotato
|
|
38
41
|
end
|
39
42
|
end
|
40
43
|
|
44
|
+
def update_attributes(hash)
|
45
|
+
self.attributes = hash
|
46
|
+
save
|
47
|
+
end
|
48
|
+
|
41
49
|
def attributes
|
42
50
|
self.class.properties.inject({}) do |res, property|
|
43
51
|
property.serialize(res, self)
|
@@ -90,7 +98,7 @@ module CouchPotato
|
|
90
98
|
end
|
91
99
|
|
92
100
|
def ==(other)
|
93
|
-
other.class == self.class && self.
|
101
|
+
other.class == self.class && self.to_json == other.to_json
|
94
102
|
end
|
95
103
|
|
96
104
|
private
|
@@ -103,7 +111,7 @@ module CouchPotato
|
|
103
111
|
run_callbacks :before_create
|
104
112
|
self.created_at = Time.now
|
105
113
|
self.updated_at = Time.now
|
106
|
-
self._id =
|
114
|
+
self._id = generate_uuid
|
107
115
|
bulk_save_queue << self
|
108
116
|
save_dependent_objects
|
109
117
|
bulk_save_queue.save do |res|
|
@@ -114,6 +122,10 @@ module CouchPotato
|
|
114
122
|
true
|
115
123
|
end
|
116
124
|
|
125
|
+
def generate_uuid
|
126
|
+
self.class.server.next_uuid rescue Digest::MD5.hexdigest(rand(1000000000000).to_s) # only works with couchdb 0.9
|
127
|
+
end
|
128
|
+
|
117
129
|
def extract_rev(res)
|
118
130
|
res['new_revs'].select{|hash| hash['id'] == self.id}.first['rev']
|
119
131
|
end
|
@@ -172,15 +184,28 @@ module CouchPotato
|
|
172
184
|
def db(name = nil)
|
173
185
|
::CouchPotato::Persistence.Db(name)
|
174
186
|
end
|
187
|
+
|
175
188
|
end
|
176
189
|
|
177
190
|
def self.Db(database_name = nil)
|
191
|
+
@@__database ||= CouchRest.database(full_url_to_database(database_name))
|
192
|
+
end
|
193
|
+
|
194
|
+
def self.Server(database_name = nil)
|
195
|
+
@@_server ||= Db(database_name).server
|
196
|
+
end
|
197
|
+
|
198
|
+
def self.Db!(database_name = nil)
|
199
|
+
CouchRest.database!(full_url_to_database(database_name))
|
200
|
+
end
|
201
|
+
|
202
|
+
def self.full_url_to_database(database_name)
|
178
203
|
database_name ||= CouchPotato::Config.database_name || raise('No Database configured. Set CouchPotato::Config.database_name')
|
179
|
-
|
180
|
-
if
|
181
|
-
|
204
|
+
url = database_name
|
205
|
+
if url !~ /^http:\/\//
|
206
|
+
url = "http://localhost:5984/#{database_name}"
|
182
207
|
end
|
183
|
-
|
208
|
+
url
|
184
209
|
end
|
185
210
|
end
|
186
211
|
end
|