ohm 1.3.0 → 1.3.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.
- 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
|