ohm_util 0.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.md ADDED
@@ -0,0 +1,570 @@
1
+ Ohm ॐ
2
+ =====
3
+
4
+ Object-hash mapping library for Redis.
5
+
6
+ Description
7
+ -----------
8
+
9
+ Ohm is a library for storing objects in [Redis][redis], a persistent key-value
10
+ database. It has very good performance.
11
+
12
+ Community
13
+ ---------
14
+
15
+ Join the mailing list: [http://groups.google.com/group/ohm-ruby](http://groups.google.com/group/ohm-ruby)
16
+
17
+ Meet us on IRC: [#ohm](irc://chat.freenode.net/#ohm) on [freenode.net](http://freenode.net/)
18
+
19
+ Related projects
20
+ ----------------
21
+
22
+ These are libraries in other languages that were inspired by Ohm.
23
+
24
+ * [Ohm](https://github.com/soveran/ohm-crystal) for Crystal, created by soveran
25
+ * [JOhm](https://github.com/xetorthio/johm) for Java, created by xetorthio
26
+ * [Lohm](https://github.com/slact/lua-ohm) for Lua, created by slact
27
+ * [ohm.lua](https://github.com/amakawa/ohm.lua) for Lua, created by amakawa
28
+ * [Nohm](https://github.com/maritz/nohm) for Node.js, created by maritz
29
+ * [Redisco](https://github.com/iamteem/redisco) for Python, created by iamteem
30
+ * [redis3m](https://github.com/luca3m/redis3m) for C++, created by luca3m
31
+ * [Ohmoc](https://github.com/seppo0010/ohmoc) for Objective-C, created by seppo0010
32
+ * [Sohm](https://github.com/xxuejie/sohm.lua) for Lua, compatible with Twemproxy
33
+
34
+ Articles and Presentations
35
+ --------------------------
36
+
37
+ * [Simplicity](http://files.soveran.com/simplicity)
38
+ * [How to Redis](http://www.paperplanes.de/2009/10/30/how_to_redis.html)
39
+ * [Redis and Ohm](http://carlopecchia.eu/blog/2010/04/30/redis-and-ohm-part1/)
40
+ * [Ohm (Redis ORM)](http://blog.s21g.com/articles/1717) (Japanese)
41
+ * [Redis and Ohm](http://www.slideshare.net/awksedgreep/redis-and-ohm)
42
+ * [Ruby off Rails](http://www.slideshare.net/cyx.ucron/ruby-off-rails)
43
+ * [Data modeling with Redis and Ohm](http://www.sitepoint.com/semi-relational-data-modeling-redis-ohm/)
44
+
45
+ Getting started
46
+ ---------------
47
+
48
+ Install [Redis][redis]. On most platforms it's as easy as grabbing the sources,
49
+ running make and then putting the `redis-server` binary in the PATH.
50
+
51
+ Once you have it installed, you can execute `redis-server` and it will
52
+ run on `localhost:6379` by default. Check the `redis.conf` file that comes
53
+ with the sources if you want to change some settings.
54
+
55
+ If you don't have Ohm, try this:
56
+
57
+ $ [sudo] gem install ohm
58
+
59
+ Or you can grab the code from [http://github.com/soveran/ohm][ohm].
60
+
61
+ Now, in an irb session you can test the Redis adapter directly:
62
+
63
+ >> require "ohm"
64
+ => true
65
+ >> Ohm.redis.call "SET", "Foo", "Bar"
66
+ => "OK"
67
+ >> Ohm.redis.call "GET", "Foo"
68
+ => "Bar"
69
+
70
+ ## Connecting to a Redis database
71
+
72
+ Ohm uses a lightweight Redis client called [Redic][redic]. To connect
73
+ to a Redis database, you will need to set an instance of `Redic`, with
74
+ an URL of the form `redis://:<passwd>@<host>:<port>/<db>`, through the
75
+ `Ohm.redis=` method, e.g.
76
+
77
+ ```ruby
78
+ require "ohm"
79
+
80
+ Ohm.redis = Redic.new("redis://127.0.0.1:6379")
81
+
82
+ Ohm.redis.call "SET", "Foo", "Bar"
83
+
84
+ Ohm.redis.call "GET", "Foo"
85
+ # => "Bar"
86
+ ```
87
+
88
+ Ohm defaults to a Redic connection to "redis://127.0.0.1:6379". The
89
+ example above could be rewritten as:
90
+
91
+ ```ruby
92
+ require "ohm"
93
+
94
+ Ohm.redis.call "SET", "Foo", "Bar"
95
+
96
+ Ohm.redis.call "GET", "Foo"
97
+ # => "Bar"
98
+ ```
99
+
100
+ All Ohm models inherit the same connection settings from `Ohm.redis`.
101
+ For cases where certain models need to connect to different databases,
102
+ they simple have to override that, i.e.
103
+
104
+ ```ruby
105
+ require "ohm"
106
+
107
+ Ohm.redis = Redic.new(ENV["REDIS_URL1"])
108
+
109
+ class User < Ohm::Model
110
+ end
111
+
112
+ User.redis = Redic.new(ENV["REDIS_URL2"])
113
+ ```
114
+
115
+ Models
116
+ ------
117
+
118
+ Ohm's purpose in life is to map objects to a key value datastore. It
119
+ doesn't need migrations or external schema definitions. Take a look at
120
+ the example below:
121
+
122
+ ### Example
123
+
124
+ ```ruby
125
+ class Event < Ohm::Model
126
+ attribute :name
127
+ reference :venue, :Venue
128
+ set :participants, :Person
129
+ counter :votes
130
+
131
+ index :name
132
+ end
133
+
134
+ class Venue < Ohm::Model
135
+ attribute :name
136
+ collection :events, :Event
137
+ end
138
+
139
+ class Person < Ohm::Model
140
+ attribute :name
141
+ end
142
+ ```
143
+
144
+ All models have the `id` attribute built in, you don't need to declare it.
145
+
146
+ This is how you interact with IDs:
147
+
148
+ ```ruby
149
+ event = Event.create :name => "Ohm Worldwide Conference 2031"
150
+ event.id
151
+ # => 1
152
+
153
+ # Find an event by id
154
+ event == Event[1]
155
+ # => true
156
+
157
+ # Update an event
158
+ event.update :name => "Ohm Worldwide Conference 2032"
159
+ # => #<Event:0x007fb4c35e2458 @attributes={:name=>"Ohm Worldwide Conference"}, @_memo={}, @id="1">
160
+
161
+ # Trying to find a non existent event
162
+ Event[2]
163
+ # => nil
164
+
165
+ # Finding all the events
166
+ Event.all.to_a
167
+ # => [<Event:1 name='Ohm Worldwide Conference 2032'>]
168
+ ```
169
+
170
+ This example shows some basic features, like attribute declarations and
171
+ querying. Keep reading to find out what you can do with models.
172
+
173
+ Attribute types
174
+ ---------------
175
+
176
+ Ohm::Model provides 4 attribute types:
177
+
178
+ * `Ohm::Model.attribute`,
179
+ * `Ohm::Model.set`
180
+ * `Ohm::Model.list`
181
+ * `Ohm::Model.counter`
182
+
183
+ and 2 meta types:
184
+
185
+ * `Ohm::Model.reference`
186
+ * `Ohm::Model.collection`.
187
+
188
+ ### attribute
189
+
190
+ An `attribute` is just any value that can be stored as a string. In the
191
+ example above, we used this field to store the event's `name`. You can
192
+ use it to store numbers, but be aware that Redis will return a string
193
+ when you retrieve the value.
194
+
195
+ ### set
196
+
197
+ A `set` in Redis is an unordered list, with an external behavior similar
198
+ to that of Ruby arrays, but optimized for faster membership lookups.
199
+ It's used internally by Ohm to keep track of the instances of each model
200
+ and for generating and maintaining indexes.
201
+
202
+ ### list
203
+
204
+ A `list` is like an array in Ruby. It's perfectly suited for queues
205
+ and for keeping elements in order.
206
+
207
+ ### counter
208
+
209
+ A `counter` is like a regular attribute, but the direct manipulation
210
+ of the value is not allowed. You can retrieve, increase or decrease
211
+ the value, but you can not assign it. In the example above, we used a
212
+ counter attribute for tracking votes. As the `increment` and `decrement`
213
+ operations are atomic, you can rest assured a vote won't be counted twice.
214
+
215
+ ### reference
216
+
217
+ It's a special kind of attribute that references another model.
218
+ Internally, Ohm will keep a pointer to the model (its ID), but you get
219
+ accessors that give you real instances. You can think of it as the model
220
+ containing the foreign key to another model.
221
+
222
+ ### collection
223
+
224
+ Provides an accessor to search for all models that `reference` the current model.
225
+
226
+ Tracked keys
227
+ ------------
228
+
229
+ Besides the provided attribute types, it is possible to instruct
230
+ Ohm to track arbitrary keys and tie them to the object's lifecycle.
231
+
232
+ For example:
233
+
234
+ ```ruby
235
+ class Log < Ohm::Model
236
+ track :text
237
+
238
+ def append(msg)
239
+ redis.call("APPEND", key[:text], msg)
240
+ end
241
+
242
+ def tail(n = 100)
243
+ redis.call("GETRANGE", key[:text], -(n), -1)
244
+ end
245
+ end
246
+
247
+ log = Log.create
248
+ log.append("hello\n")
249
+
250
+ assert_equal "hello\n", log.tail
251
+
252
+ log.append("world\n")
253
+
254
+ assert_equal "world\n", log.tail(6)
255
+ ```
256
+
257
+ When the `log` object is deleted, the `:text` key will be deleted
258
+ too. Note that the key is scoped to that particular instance of
259
+ `Log`, so if `log.id` is `42` then the key will be `Log:42:text`.
260
+
261
+ Persistence strategy
262
+ --------------------
263
+
264
+ The attributes declared with `attribute` are only persisted after
265
+ calling `save`.
266
+
267
+ Operations on attributes of type `list`, `set` and `counter` are
268
+ possible only after the object is created (when it has an assigned
269
+ `id`). Any operation on these kinds of attributes is performed
270
+ immediately. This design yields better performance than buffering
271
+ the operations and waiting for a call to `save`.
272
+
273
+ For most use cases, this pattern doesn't represent a problem.
274
+ If you are saving the object, this will suffice:
275
+
276
+ ```ruby
277
+ if event.save
278
+ event.comments.add(Comment.create(body: "Wonderful event!"))
279
+ end
280
+ ```
281
+
282
+ Working with Sets
283
+ -----------------
284
+
285
+ Given the following model declaration:
286
+
287
+ ```ruby
288
+ class Event < Ohm::Model
289
+ attribute :name
290
+ set :attendees, :Person
291
+ end
292
+ ```
293
+
294
+ You can add instances of `Person` to the set of attendees with the
295
+ `add` method:
296
+
297
+ ```ruby
298
+ event.attendees.add(Person.create(name: "Albert"))
299
+
300
+ # And now...
301
+ event.attendees.each do |person|
302
+ # ...do what you want with this person.
303
+ end
304
+ ```
305
+
306
+ ## Sorting
307
+
308
+ Since `attendees` is a `Ohm::Model::Set`, it exposes two sorting
309
+ methods: `Ohm::Model::Collection#sort` returns the elements
310
+ ordered by `id`, and `Ohm::Model::Collection#sort_by` receives
311
+ a parameter with an attribute name, which will determine the sorting
312
+ order. Both methods receive an options hash which is explained below:
313
+
314
+ ### :order
315
+
316
+ Order direction and strategy. You can pass in any of the following:
317
+
318
+ 1. ASC
319
+ 2. ASC ALPHA (or ALPHA ASC)
320
+ 3. DESC
321
+ 4. DESC ALPHA (or ALPHA DESC)
322
+
323
+ It defaults to `ASC`.
324
+
325
+ __Important Note:__ Starting with Redis 2.6, `ASC` and `DESC` only
326
+ work with integers or floating point data types. If you need to sort
327
+ by an alphanumeric field, add the `ALPHA` keyword.
328
+
329
+ ### :limit
330
+
331
+ The offset and limit from which we should start with. Note that
332
+ this is 0-indexed. It defaults to `0`.
333
+
334
+ Example:
335
+
336
+ `limit: [0, 10]` will get the first 10 entries starting from offset 0.
337
+
338
+ ### :by
339
+
340
+ Key or Hash key with which to sort by. An important distinction with
341
+ using `Ohm::Model::Collection#sort` and
342
+ `Ohm::Model::Collection#sort_by` is that `sort_by` automatically
343
+ converts the passed argument with the assumption that it is a hash key
344
+ and it's within the current model you are sorting.
345
+
346
+ ```ruby
347
+ Post.all.sort_by(:title) # SORT Post:all BY Post:*->title
348
+ Post.all.sort(by: :title) # SORT Post:all BY title
349
+ ```
350
+
351
+ __Tip:__ Unless you absolutely know what you're doing, use `sort`
352
+ when you want to sort your models by their `id`, and use `sort_by`
353
+ otherwise.
354
+
355
+ ### :get
356
+
357
+ A key pattern to return, e.g. `Post:*->title`. As is the case with
358
+ the `:by` option, using `Ohm::Model::Collection#sort` and
359
+ `Ohm::Model::Collection#sort_by` has distinct differences in
360
+ that `sort_by` does much of the hand-coding for you.
361
+
362
+ ```ruby
363
+ Post.all.sort_by(:title, get: :title)
364
+ # SORT Post:all BY Post:*->title GET Post:*->title
365
+
366
+ Post.all.sort(by: :title, get: :title)
367
+ # SORT Post:all BY title GET title
368
+ ```
369
+
370
+
371
+ Associations
372
+ ------------
373
+
374
+ Ohm lets you declare `references` and `collections` to represent associations.
375
+
376
+ ```ruby
377
+ class Post < Ohm::Model
378
+ attribute :title
379
+ attribute :body
380
+ collection :comments, :Comment
381
+ end
382
+
383
+ class Comment < Ohm::Model
384
+ attribute :body
385
+ reference :post, :Post
386
+ end
387
+ ```
388
+
389
+ After this, every time you refer to `post.comments` you will be talking
390
+ about instances of the model `Comment`. If you want to get a list of IDs
391
+ you can use `post.comments.ids`.
392
+
393
+ ### References explained
394
+
395
+ Doing a `Ohm::Model.reference` is actually just a shortcut for
396
+ the following:
397
+
398
+ ```ruby
399
+ # Redefining our model above
400
+ class Comment < Ohm::Model
401
+ attribute :body
402
+ attribute :post_id
403
+ index :post_id
404
+
405
+ def post=(post)
406
+ self.post_id = post.id
407
+ end
408
+
409
+ def post
410
+ Post[post_id]
411
+ end
412
+ end
413
+ ```
414
+
415
+ The only difference with the actual implementation is that the model
416
+ is memoized.
417
+
418
+ The net effect here is we can conveniently set and retrieve `Post` objects,
419
+ and also search comments using the `post_id` index.
420
+
421
+ ```ruby
422
+ Comment.find(post_id: 1)
423
+ ```
424
+
425
+ ### Collections explained
426
+
427
+ The reason a `Ohm::Model.reference` and a
428
+ `Ohm::Model.collection` go hand in hand, is that a collection is
429
+ just a macro that defines a finder for you, and we know that to find a model
430
+ by a field requires an `Ohm::Model.index` to be defined for the field
431
+ you want to search.
432
+
433
+ ```ruby
434
+ # Redefining our post above
435
+ class Post < Ohm::Model
436
+ attribute :title
437
+ attribute :body
438
+
439
+ def comments
440
+ Comment.find(post_id: self.id)
441
+ end
442
+ end
443
+ ```
444
+
445
+ The only "magic" happening is with the inference of the `index` that was used
446
+ in the other model. The following all produce the same effect:
447
+
448
+ ```ruby
449
+ # easiest, with the basic assumption that the index is `:post_id`
450
+ collection :comments, :Comment
451
+
452
+ # we can explicitly declare this as follows too:
453
+ collection :comments, :Comment, :post
454
+
455
+ # finally, we can use the default argument for the third parameter which
456
+ # is `to_reference`.
457
+ collection :comments, :Comment, to_reference
458
+
459
+ # exploring `to_reference` reveals a very interesting and simple concept:
460
+ Post.to_reference == :post
461
+ # => true
462
+ ```
463
+
464
+ Modules
465
+ -------
466
+
467
+ If your models are defined inside a module, you will have to define
468
+ the references and collections as in the following example:
469
+
470
+ ```ruby
471
+ module SomeNamespace
472
+ class Foo < Ohm::Model
473
+ attribute :name
474
+ end
475
+
476
+ class Bar < Ohm::Model
477
+ reference :foo, 'SomeNamespace::Foo'
478
+ end
479
+ end
480
+ ```
481
+
482
+ Indices
483
+ -------
484
+
485
+ An `Ohm::Model.index` is a set that's handled automatically by Ohm. For
486
+ any index declared, Ohm maintains different sets of objects IDs for quick
487
+ lookups.
488
+
489
+ In the `Event` example, the index on the name attribute will
490
+ allow for searches like `Event.find(name: "some value")`.
491
+
492
+ Note that the methods `Ohm::Model::Set#find` and
493
+ `Ohm::Model::Set#except` need a corresponding index in order to work.
494
+
495
+ ### Finding records
496
+
497
+ You can find a collection of records with the `find` method:
498
+
499
+ ```ruby
500
+ # This returns a collection of users with the username "Albert"
501
+ User.find(username: "Albert")
502
+ ```
503
+
504
+ ### Filtering results
505
+
506
+ ```ruby
507
+ # Find all users from Argentina
508
+ User.find(country: "Argentina")
509
+
510
+ # Find all active users from Argentina
511
+ User.find(country: "Argentina", status: "active")
512
+
513
+ # Find all active users from Argentina and Uruguay
514
+ User.find(status: "active").combine(country: ["Argentina", "Uruguay"])
515
+
516
+ # Find all users from Argentina, except those with a suspended account.
517
+ User.find(country: "Argentina").except(status: "suspended")
518
+
519
+ # Find all users both from Argentina and Uruguay
520
+ User.find(country: "Argentina").union(country: "Uruguay")
521
+ ```
522
+
523
+ Note that calling these methods results in new sets being created
524
+ on the fly. This is important so that you can perform further operations
525
+ before reading the items to the client.
526
+
527
+ For more information, see [SINTERSTORE](http://redis.io/commands/sinterstore),
528
+ [SDIFFSTORE](http://redis.io/commands/sdiffstore) and
529
+ [SUNIONSTORE](http://redis.io/commands/sunionstore)
530
+
531
+ Uniques
532
+ -------
533
+
534
+ Uniques are similar to indices except that there can only be one record per
535
+ entry. The canonical example of course would be the email of your user, e.g.
536
+
537
+ ```ruby
538
+ class User < Ohm::Model
539
+ attribute :email
540
+ unique :email
541
+ end
542
+
543
+ u = User.create(email: "foo@bar.com")
544
+ u == User.with(:email, "foo@bar.com")
545
+ # => true
546
+
547
+ User.create(email: "foo@bar.com")
548
+ # => raises Ohm::UniqueIndexViolation
549
+ ```
550
+
551
+ Ohm Extensions
552
+ ==============
553
+
554
+ Ohm is rather small and can be extended in many ways.
555
+
556
+ A lot of amazing contributions are available at [Ohm Contrib][contrib]
557
+ make sure to check them if you need to extend Ohm's functionality.
558
+
559
+ [contrib]: http://cyx.github.com/ohm-contrib/
560
+
561
+ Upgrading
562
+ =========
563
+
564
+ Ohm 2 breaks the compatibility with previous versions. If you're upgrading an
565
+ existing application, it's nice to have a good test coverage before going in.
566
+ To know about fixes and changes, please refer to the CHANGELOG file.
567
+
568
+ [redis]: http://redis.io
569
+ [ohm]: http://github.com/soveran/ohm
570
+ [redic]: https://github.com/amakawa/redic