ohm 1.3.0 → 1.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +14 -6
- data/.gems +4 -0
- data/.gitignore +4 -0
- data/CHANGELOG +52 -0
- data/benchmarks/common.rb +33 -0
- data/benchmarks/create.rb +21 -0
- data/benchmarks/delete.rb +13 -0
- data/examples/activity-feed.rb +162 -0
- data/examples/chaining.rb +203 -0
- data/examples/json-hash.rb +102 -0
- data/examples/one-to-many.rb +124 -0
- data/examples/philosophy.rb +149 -0
- data/examples/redis-logging.txt +179 -0
- data/examples/slug.rb +149 -0
- data/examples/tagging.rb +234 -0
- data/lib/ohm.rb +38 -4
- data/ohm.gemspec +18 -0
- data/test/model.rb +2 -2
- metadata +30 -15
- data/test/ranks.rb +0 -21
- data/test/setup.rb +0 -48
data/examples/slug.rb
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
### All Kinds of Slugs
|
2
|
+
|
3
|
+
# The problem of making semantic URLs have definitely been a prevalent one.
|
4
|
+
# There has been quite a lot of solutions around this theme, so we'll discuss
|
5
|
+
# a few simple ways to handle slug generation.
|
6
|
+
|
7
|
+
#### ID Prefixed slugs
|
8
|
+
|
9
|
+
# This is by far the simplest (and most cost-effective way) of generating
|
10
|
+
# slugs. Implementing this is pretty simple too.
|
11
|
+
|
12
|
+
# Let's first require `Ohm`.
|
13
|
+
require "ohm"
|
14
|
+
|
15
|
+
# Now let's define our `Post` model, with just a single
|
16
|
+
# `attribute` *title*.
|
17
|
+
class Post < Ohm::Model
|
18
|
+
attribute :title
|
19
|
+
|
20
|
+
# To make it more convenient, we override the finder syntax,
|
21
|
+
# so doing a `Post["1-my-post-title"]` will in effect just call
|
22
|
+
# `Post[1]`.
|
23
|
+
def self.[](id)
|
24
|
+
super(id.to_i)
|
25
|
+
end
|
26
|
+
|
27
|
+
# This pattern was mostly borrowed from Rails' style of generating
|
28
|
+
# URLs. Here we just concatenate the `id` and a sanitized form
|
29
|
+
# of our title.
|
30
|
+
def to_param
|
31
|
+
"#{id}-#{title.to_s.gsub(/\p{^Alnum}/u, " ").gsub(/\s+/, "-").downcase}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Let's verify our code using the
|
36
|
+
# [Cutest](http://github.com/djanowski/cutest)
|
37
|
+
# testing framework.
|
38
|
+
require "cutest"
|
39
|
+
|
40
|
+
# Also we ensure every test run is guaranteed to have a clean
|
41
|
+
# *Redis* instance.
|
42
|
+
prepare { Ohm.flush }
|
43
|
+
|
44
|
+
# For each and every test, we create a post with
|
45
|
+
# the title "ID Prefixed Slugs". Since it's the last
|
46
|
+
# line of our `setup`, it will also be yielded to
|
47
|
+
# each of our test blocks.
|
48
|
+
setup do
|
49
|
+
Post.create(:title => "ID Prefixed Slugs")
|
50
|
+
end
|
51
|
+
|
52
|
+
# Now let's verify the behavior of our `to_param` method.
|
53
|
+
# Note that we make it dash-separated and lowercased.
|
54
|
+
test "to_param" do |post|
|
55
|
+
assert "1-id-prefixed-slugs" == post.to_param
|
56
|
+
end
|
57
|
+
|
58
|
+
# We also check that our easier finder syntax works.
|
59
|
+
test "finding the post" do |post|
|
60
|
+
assert post == Post[post.to_param]
|
61
|
+
end
|
62
|
+
|
63
|
+
#### We don't have to code it everytime
|
64
|
+
|
65
|
+
# Because of the prevalence, ease of use, and efficiency of this style of slug
|
66
|
+
# generation, it has been extracted to a module in
|
67
|
+
# [Ohm::Contrib](http://github.com/cyx/ohm-contrib/) called `Ohm::Slug`.
|
68
|
+
|
69
|
+
# Let's create a different model to demonstrate how to use it.
|
70
|
+
# (Run `[sudo] gem install ohm-contrib` to install ohm-contrib).
|
71
|
+
|
72
|
+
# When using `ohm-contrib`, we simply require it, and then
|
73
|
+
# directly reference the specific module. In this case, we
|
74
|
+
# use `Ohm::Slug`.
|
75
|
+
require "ohm/contrib"
|
76
|
+
|
77
|
+
class Video < Ohm::Model
|
78
|
+
include Ohm::Slug
|
79
|
+
|
80
|
+
attribute :title
|
81
|
+
|
82
|
+
# `Ohm::Slug` just uses the value of the object's `to_s`.
|
83
|
+
def to_s
|
84
|
+
title.to_s
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Now to quickly verify that everything works similar to our
|
89
|
+
# example above!
|
90
|
+
test "video slugging" do
|
91
|
+
video = Video.create(:title => "A video about ohm")
|
92
|
+
|
93
|
+
assert "1-a-video-about-ohm" == video.to_param
|
94
|
+
assert video == Video[video.id]
|
95
|
+
end
|
96
|
+
|
97
|
+
# That's it, and it works similarly to the example above.
|
98
|
+
|
99
|
+
#### What if I want a slug without an ID prefix?
|
100
|
+
|
101
|
+
# For this case, we can still make use of `Ohm::Slug`'s ability to
|
102
|
+
# make a clean string.
|
103
|
+
|
104
|
+
# Let's create an `Article` class which has a single attribute `title`.
|
105
|
+
class Article < Ohm::Model
|
106
|
+
include Ohm::Callbacks
|
107
|
+
|
108
|
+
attribute :title
|
109
|
+
|
110
|
+
# Now before creating this object, we just call `Ohm::Slug.slug` directly.
|
111
|
+
# We also check if the generated slug exists, and repeatedly try
|
112
|
+
# appending numbers.
|
113
|
+
protected
|
114
|
+
def before_create
|
115
|
+
temp = Ohm::Slug.slug(title)
|
116
|
+
self.id = temp
|
117
|
+
|
118
|
+
counter = 0
|
119
|
+
while Article.exists?(id)
|
120
|
+
self.id = "%s-%d" % [temp, counter += 1]
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# We now verify the behavior of our `Article` class
|
126
|
+
# by creating an article with the same title 3 times.
|
127
|
+
test "create an article with the same title" do
|
128
|
+
a1 = Article.create(:title => "All kinds of slugs")
|
129
|
+
a2 = Article.create(:title => "All kinds of slugs")
|
130
|
+
a3 = Article.create(:title => "All kinds of slugs")
|
131
|
+
|
132
|
+
assert a1.id == "all-kinds-of-slugs"
|
133
|
+
assert a2.id == "all-kinds-of-slugs-1"
|
134
|
+
assert a3.id == "all-kinds-of-slugs-2"
|
135
|
+
end
|
136
|
+
|
137
|
+
#### Conclusion
|
138
|
+
|
139
|
+
# Slug generation comes in all different flavors.
|
140
|
+
#
|
141
|
+
# 1. The first solution is good enough for most cases. The primary advantage
|
142
|
+
# of this solution is that we don't have to check for ID clashes.
|
143
|
+
#
|
144
|
+
# 2. The second solution may be needed for cases where you must make
|
145
|
+
# the URLs absolutely clean and readable, and you hate having those
|
146
|
+
# number prefixes.
|
147
|
+
#
|
148
|
+
# *NOTE:* The example we used for the second solution has potential
|
149
|
+
# race conditions. I'll leave fixing it as an exercise to you.
|
data/examples/tagging.rb
ADDED
@@ -0,0 +1,234 @@
|
|
1
|
+
### Tagging
|
2
|
+
|
3
|
+
#### Intro
|
4
|
+
|
5
|
+
# When building a Web 2.0 application, tagging will probably come up
|
6
|
+
# as one of the most requested features. Popularized by Delicious,
|
7
|
+
# it has quickly become a useful way to organize crowd sourced data.
|
8
|
+
|
9
|
+
#### How it was done
|
10
|
+
|
11
|
+
# Typically, when you do tagging using an RDBMS, you'll probably end up
|
12
|
+
# having a taggings and a tags table, hence a many-to-many design.
|
13
|
+
# Here is a quick sketch just to illustrate:
|
14
|
+
#
|
15
|
+
#
|
16
|
+
#
|
17
|
+
# Post Taggings Tag
|
18
|
+
# ---- -------- ---
|
19
|
+
# id tag_id id
|
20
|
+
# title post_id name
|
21
|
+
#
|
22
|
+
# As you can see, this design leads to a lot of problems:
|
23
|
+
#
|
24
|
+
# 1. Trying to find the tags of a post will have to go through taggings, and
|
25
|
+
# then individually find the actual tag.
|
26
|
+
# 2. One might be inclined to use a JOIN query, but we all know
|
27
|
+
# [joins are evil](http://stackoverflow.com/questions/1020847).
|
28
|
+
# 3. Building a tag cloud or some form of tag ranking is unintuitive.
|
29
|
+
|
30
|
+
#### The Ohm approach
|
31
|
+
|
32
|
+
# Here is a basic outline of what we'll need:
|
33
|
+
#
|
34
|
+
# 1. We should be able to tag a post (separated by commas).
|
35
|
+
# 2. We should be able to find a post with a given tag.
|
36
|
+
|
37
|
+
#### Beginning with our Post model
|
38
|
+
|
39
|
+
# Let's first require ohm.
|
40
|
+
require 'ohm'
|
41
|
+
|
42
|
+
# We then declare our class, inheriting from `Ohm::Model` in the process.
|
43
|
+
class Post < Ohm::Model
|
44
|
+
|
45
|
+
# The structure, fields, and other associations are defined in a declarative
|
46
|
+
# manner. Ohm allows us to declare *attributes*, *sets*, *lists* and
|
47
|
+
# *counters*. For our usecase here, only two *attributes* will get the job
|
48
|
+
# done. The `body` will just
|
49
|
+
# be a plain string, and the `tags` will contain our comma-separated list of
|
50
|
+
# words, i.e. "ruby, redis, ohm". We then declare an `index` (which can be
|
51
|
+
# an `attribute` or just a plain old method), which we point to our method
|
52
|
+
# `tag`.
|
53
|
+
attribute :body
|
54
|
+
attribute :tags
|
55
|
+
index :tag
|
56
|
+
|
57
|
+
# One very interesting thing about Ohm indexes is that it can either be a
|
58
|
+
# *String* or an *Enumerable* data structure. When we declare it as an
|
59
|
+
# *Enumerable*, `Ohm` will create an index for every element. So if `tag`
|
60
|
+
# returned `[ruby, redis, ohm]` then we can search it using any of the
|
61
|
+
# following:
|
62
|
+
#
|
63
|
+
# 1. ruby
|
64
|
+
# 2. redis
|
65
|
+
# 3. ohm
|
66
|
+
# 4. ruby, redis
|
67
|
+
# 5. ruby, ohm
|
68
|
+
# 6. redis, ohm
|
69
|
+
# 7. ruby, redis, ohm
|
70
|
+
#
|
71
|
+
# Pretty neat ain't it?
|
72
|
+
def tag
|
73
|
+
tags.to_s.split(/\s*,\s*/).uniq
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
#### Testing it out
|
78
|
+
|
79
|
+
# It's a very good habit to test all the time. In the Ruby community,
|
80
|
+
# a lot of test frameworks have been created.
|
81
|
+
|
82
|
+
# For our purposes in this example, we'll use cutest.
|
83
|
+
require "cutest"
|
84
|
+
|
85
|
+
# Cutest allows us to define callbacks which are guaranteed to be executed
|
86
|
+
# every time a new `test` begins. Here, we just make sure that the Redis
|
87
|
+
# instance of `Ohm` is empty everytime.
|
88
|
+
prepare { Ohm.flush }
|
89
|
+
|
90
|
+
# Next, let's create a simple `Post` instance. The return value of the `setup`
|
91
|
+
# block will be passed to every `test` block, so we don't actually have to
|
92
|
+
# assign it to an instance variable.
|
93
|
+
setup do
|
94
|
+
Post.create(:body => "Ohm Tagging", :tags => "tagging, ohm, redis")
|
95
|
+
end
|
96
|
+
|
97
|
+
# For our first run, let's verify the fact that we can find a `Post`
|
98
|
+
# using any of the tags we gave.
|
99
|
+
test "find using a single tag" do |p|
|
100
|
+
assert Post.find(tag: "tagging").include?(p)
|
101
|
+
assert Post.find(tag: "ohm").include?(p)
|
102
|
+
assert Post.find(tag: "redis").include?(p)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Now we verify our claim earlier, that it is possible to find a tag
|
106
|
+
# using any one of the combinations for the given set of tags.
|
107
|
+
#
|
108
|
+
# We also verify that if we pass in a non-existent tag name that
|
109
|
+
# we'll fail to find the `Post` we just created.
|
110
|
+
test "find using an intersection of multiple tag names" do |p|
|
111
|
+
assert Post.find(tag: ["tagging", "ohm"]).include?(p)
|
112
|
+
assert Post.find(tag: ["tagging", "redis"]).include?(p)
|
113
|
+
assert Post.find(tag: ["ohm", "redis"]).include?(p)
|
114
|
+
assert Post.find(tag: ["tagging", "ohm", "redis"]).include?(p)
|
115
|
+
|
116
|
+
assert ! Post.find(tag: ["tagging", "foo"]).include?(p)
|
117
|
+
end
|
118
|
+
|
119
|
+
#### Adding a Tag model
|
120
|
+
|
121
|
+
# Let's pretend that the client suddenly requested that we keep track
|
122
|
+
# of the number of times a tag has been used. It's a pretty fair requirement
|
123
|
+
# after all. Updating our requirements, we will now have:
|
124
|
+
#
|
125
|
+
# 1. We should be able to tag a post (separated by commas).
|
126
|
+
# 2. We should be able to find a post with a given tag.
|
127
|
+
# 3. We should be able to find top tags, and their count.
|
128
|
+
|
129
|
+
# Continuing from our example above, let's require `ohm-contrib`, which we
|
130
|
+
# will be using for callbacks.
|
131
|
+
require "ohm/contrib"
|
132
|
+
|
133
|
+
# Let's quickly re-open our Post class.
|
134
|
+
class Post
|
135
|
+
# When we want our class to have extended functionality like callbacks,
|
136
|
+
# we simply include the necessary modules, in this case `Ohm::Callbacks`,
|
137
|
+
# which will be responsible for inserting `before_*` and `after_*` methods
|
138
|
+
# in the object's lifecycle.
|
139
|
+
include Ohm::Callbacks
|
140
|
+
|
141
|
+
# To make our code more concise, we just quickly change our implementation
|
142
|
+
# of `tag` to receive a default parameter:
|
143
|
+
def tag(tags = self.tags)
|
144
|
+
tags.to_s.split(/\s*,\s*/).uniq
|
145
|
+
end
|
146
|
+
|
147
|
+
# For all but the most simple cases, we would probably need to define
|
148
|
+
# callbacks. When we included `Ohm::Callbacks` above, it actually gave us
|
149
|
+
# the following:
|
150
|
+
#
|
151
|
+
# 1. `before_validate` and `after_validate`
|
152
|
+
# 2. `before_create` and `after_create`
|
153
|
+
# 3. `before_update` and `after_update`
|
154
|
+
# 4. `before_save` and `after_save`
|
155
|
+
# 5. `before_delete` and `after_delete`
|
156
|
+
|
157
|
+
# For our scenario, we only need a `before_update` and `after_save`.
|
158
|
+
# The idea for our `before_update` is to decrement the `total` of
|
159
|
+
# all existing tags. We use `read_remote(:tags)` to make sure that
|
160
|
+
# we actually get the original `tags` for a particular record.
|
161
|
+
protected
|
162
|
+
def before_update
|
163
|
+
tag(read_remote(:tags)).map(&Tag).each { |t| t.decr :total }
|
164
|
+
end
|
165
|
+
|
166
|
+
# And of course, we increment all new tags for a particular record
|
167
|
+
# after successfully saving it.
|
168
|
+
def after_save
|
169
|
+
tag.map(&Tag).each { |t| t.incr :total }
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
#### Our Tag model
|
174
|
+
|
175
|
+
# The `Tag` model has only one type, which is a `counter` for the `total`.
|
176
|
+
# Since `Ohm` allows us to use any kind of ID (not just numeric sequences),
|
177
|
+
# we can actually use the tag name to identify a `Tag`.
|
178
|
+
class Tag < Ohm::Model
|
179
|
+
counter :total
|
180
|
+
|
181
|
+
# The syntax for finding a record by its ID is `Tag["ruby"]`. The standard
|
182
|
+
# behavior in `Ohm` is to return `nil` when the ID does not exist.
|
183
|
+
#
|
184
|
+
# To simplify our code, we override `Tag["ruby"]`, and make it create a
|
185
|
+
# new `Tag` if it doesn't exist yet. One important implementation detail
|
186
|
+
# though is that we need to encode the tag name, so special characters
|
187
|
+
# and spaces won't produce an invalid key.
|
188
|
+
def self.[](id)
|
189
|
+
super(encode(id)) || create(:id => encode(id))
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
#### Verifying our third requirement
|
194
|
+
|
195
|
+
# Continuing from our test cases above, let's add test coverage for the
|
196
|
+
# behavior of counting tags.
|
197
|
+
|
198
|
+
# For each and every tag we initially create, we need to make sure they have a
|
199
|
+
# total of 1.
|
200
|
+
test "verify total to be exactly 1" do
|
201
|
+
assert 1 == Tag["ohm"].total
|
202
|
+
assert 1 == Tag["redis"].total
|
203
|
+
assert 1 == Tag["tagging"].total
|
204
|
+
end
|
205
|
+
|
206
|
+
# If we try and create another post tagged "ruby", "redis", `Tag["redis"]`
|
207
|
+
# should then have a total of 2. All of the other tags will still have
|
208
|
+
# a total of 1.
|
209
|
+
test "verify totals increase" do
|
210
|
+
Post.create(:body => "Ruby & Redis", :tags => "ruby, redis")
|
211
|
+
|
212
|
+
assert 1 == Tag["ohm"].total
|
213
|
+
assert 1 == Tag["tagging"].total
|
214
|
+
assert 1 == Tag["ruby"].total
|
215
|
+
assert 2 == Tag["redis"].total
|
216
|
+
end
|
217
|
+
|
218
|
+
# Finally, let's verify the scenario where we create a `Post` tagged
|
219
|
+
# "ruby", "redis" and update it to only have the tag "redis",
|
220
|
+
# effectively removing the tag "ruby" from our `Post`.
|
221
|
+
test "updating an existing post decrements the tags removed" do
|
222
|
+
p = Post.create(:body => "Ruby & Redis", :tags => "ruby, redis")
|
223
|
+
p.update(:tags => "redis")
|
224
|
+
|
225
|
+
assert 0 == Tag["ruby"].total
|
226
|
+
assert 2 == Tag["redis"].total
|
227
|
+
end
|
228
|
+
|
229
|
+
## Conclusion
|
230
|
+
|
231
|
+
# Most of the time we tend to think in terms of an RDBMS way, and this is in
|
232
|
+
# no way a negative thing. However, it is important to try and switch your
|
233
|
+
# frame of mind when working with Ohm (and Redis) because it will greatly save
|
234
|
+
# you time, and possibly lead to a great design.
|
data/lib/ohm.rb
CHANGED
@@ -1147,7 +1147,7 @@ module Ohm
|
|
1147
1147
|
return self
|
1148
1148
|
end
|
1149
1149
|
|
1150
|
-
# Read an attribute
|
1150
|
+
# Read an attribute remotely from Redis. Useful if you want to get
|
1151
1151
|
# the most recent value of the attribute and not rely on locally
|
1152
1152
|
# cached value.
|
1153
1153
|
#
|
@@ -1305,9 +1305,13 @@ module Ohm
|
|
1305
1305
|
uniques = nil
|
1306
1306
|
_indices = nil
|
1307
1307
|
indices = nil
|
1308
|
+
existing_indices = nil
|
1309
|
+
existing_uniques = nil
|
1308
1310
|
|
1309
1311
|
t.read do
|
1310
1312
|
_verify_uniques
|
1313
|
+
existing_indices = _read_attributes(model.indices) if model.indices.any?
|
1314
|
+
existing_uniques = _read_attributes(model.uniques) if model.uniques.any?
|
1311
1315
|
_uniques = db.hgetall(key[:_uniques])
|
1312
1316
|
_indices = db.smembers(key[:_indices])
|
1313
1317
|
uniques = _read_index_type(:uniques)
|
@@ -1316,8 +1320,10 @@ module Ohm
|
|
1316
1320
|
|
1317
1321
|
t.write do
|
1318
1322
|
db.sadd(model.key[:all], id)
|
1319
|
-
|
1323
|
+
_delete_existing_indices(existing_indices)
|
1324
|
+
_delete_existing_uniques(existing_uniques)
|
1320
1325
|
_delete_indices(_indices)
|
1326
|
+
_delete_uniques(_uniques)
|
1321
1327
|
_save
|
1322
1328
|
_save_indices(indices)
|
1323
1329
|
_save_uniques(uniques)
|
@@ -1337,6 +1343,7 @@ module Ohm
|
|
1337
1343
|
transaction do |t|
|
1338
1344
|
_uniques = nil
|
1339
1345
|
_indices = nil
|
1346
|
+
existing = nil
|
1340
1347
|
|
1341
1348
|
t.watch(*_unique_keys)
|
1342
1349
|
|
@@ -1345,6 +1352,7 @@ module Ohm
|
|
1345
1352
|
t.watch(key[:_uniques]) if model.uniques.any?
|
1346
1353
|
|
1347
1354
|
t.read do
|
1355
|
+
existing = _read_attributes(model.indices) if model.indices.any?
|
1348
1356
|
_uniques = db.hgetall(key[:_uniques])
|
1349
1357
|
_indices = db.smembers(key[:_indices])
|
1350
1358
|
end
|
@@ -1352,6 +1360,7 @@ module Ohm
|
|
1352
1360
|
t.write do
|
1353
1361
|
_delete_uniques(_uniques)
|
1354
1362
|
_delete_indices(_indices)
|
1363
|
+
_delete_existing_indices(existing)
|
1355
1364
|
model.collections.each { |e| db.del(key[e]) }
|
1356
1365
|
db.srem(model.key[:all], id)
|
1357
1366
|
db.del(key[:counters])
|
@@ -1498,11 +1507,13 @@ module Ohm
|
|
1498
1507
|
end
|
1499
1508
|
|
1500
1509
|
def _save_uniques(uniques)
|
1510
|
+
attrs = model.attributes
|
1511
|
+
|
1501
1512
|
uniques.each do |att, val|
|
1502
1513
|
unique = model.key[:uniques][att]
|
1503
1514
|
|
1504
1515
|
db.hset(unique, val, id)
|
1505
|
-
db.hset(key[:_uniques], unique, val)
|
1516
|
+
db.hset(key[:_uniques], unique, val) unless attrs.include?(att)
|
1506
1517
|
end
|
1507
1518
|
end
|
1508
1519
|
|
@@ -1513,6 +1524,23 @@ module Ohm
|
|
1513
1524
|
end
|
1514
1525
|
end
|
1515
1526
|
|
1527
|
+
def _delete_existing_indices(existing)
|
1528
|
+
return unless existing
|
1529
|
+
|
1530
|
+
existing = existing.map { |key, value| model.to_indices(key, value) }
|
1531
|
+
existing.flatten!(1)
|
1532
|
+
|
1533
|
+
_delete_indices(existing)
|
1534
|
+
end
|
1535
|
+
|
1536
|
+
def _delete_existing_uniques(existing)
|
1537
|
+
return unless existing
|
1538
|
+
|
1539
|
+
_delete_uniques(existing.map { |key, value|
|
1540
|
+
[model.key[:uniques][key], value]
|
1541
|
+
})
|
1542
|
+
end
|
1543
|
+
|
1516
1544
|
def _delete_indices(indices)
|
1517
1545
|
indices.each do |index|
|
1518
1546
|
db.srem(index, id)
|
@@ -1521,12 +1549,18 @@ module Ohm
|
|
1521
1549
|
end
|
1522
1550
|
|
1523
1551
|
def _save_indices(indices)
|
1552
|
+
attrs = model.attributes
|
1553
|
+
|
1524
1554
|
indices.each do |att, val|
|
1525
1555
|
model.to_indices(att, val).each do |index|
|
1526
1556
|
db.sadd(index, id)
|
1527
|
-
db.sadd(key[:_indices], index)
|
1557
|
+
db.sadd(key[:_indices], index) unless attrs.include?(att)
|
1528
1558
|
end
|
1529
1559
|
end
|
1530
1560
|
end
|
1561
|
+
|
1562
|
+
def _read_attributes(attrs)
|
1563
|
+
Hash[attrs.zip(db.hmget(key, *attrs))]
|
1564
|
+
end
|
1531
1565
|
end
|
1532
1566
|
end
|