Class: Ohm::Model

Inherits:
Object
  • Object
show all
Defined in:
lib/ohm.rb,
lib/ohm/json.rb

Overview

The base class for all your models. In order to better understand it, here is a semi-realtime explanation of the details involved when creating a User instance.

Example:

class User < Ohm::Model
  attribute :name
  index :name

  attribute :email
  unique :email

  counter :points

  set :posts, :Post
end

u = User.create(:name => "John", :email => "[email protected]")
u.incr :points
u.posts.add(Post.create)

When you execute `User.create(…)`, you run the following Redis commands:

# Generate an ID
INCR User:id

# Add the newly generated ID, (let's assume the ID is 1).
SADD User:all 1

# Store the unique index
HSET User:uniques:email foo@bar.com 1

# Store the name index
SADD User:indices:name:John 1

# Store the HASH
HMSET User:1 name John email foo@bar.com

Next we increment points:

HINCR User:1:counters points 1

And then we add a Post to the `posts` set. (For brevity, let's assume the Post created has an ID of 1).

SADD User:1:posts 1

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(atts = {}) ⇒ Model

Initialize a model using a dictionary of attributes.

Example:

u = User.new(:name => "John")

1095
1096
1097
1098
1099
# File 'lib/ohm.rb', line 1095

def initialize(atts = {})
  @attributes = {}
  @_memo = {}
  update_attributes(atts)
end

Instance Attribute Details

#idObject

Access the ID used to store this model. The ID is used together with the name of the class in order to form the Redis key.

Example:

class User < Ohm::Model; end

u = User.create
u.id
# => 1

u.key
# => User:1

Raises:


1115
1116
1117
1118
# File 'lib/ohm.rb', line 1115

def id
  raise MissingID if not defined?(@id)
  @id
end

Class Method Details

.[](id) ⇒ Object

Retrieve a record by ID.

Example:

u = User.create
u == User[u.id]
# =>  true

726
727
728
# File 'lib/ohm.rb', line 726

def self.[](id)
  new(:id => id).load! if id && exists?(id)
end

.allObject

An Ohm::Set wrapper for Model.key.


1074
1075
1076
# File 'lib/ohm.rb', line 1074

def self.all
  Ohm::Set.new(self, key, key[:all])
end

.attribute(name, cast = nil) ⇒ Object

The bread and butter macro of all models. Basically declares persisted attributes. All attributes are stored on the Redis hash.

class User < Ohm::Model
  attribute :name
end

user = User.new(name: "John")
user.name
# => "John"

user.name = "Jane"
user.name
# => "Jane"

A lambda can be passed as a second parameter to add typecasting support to the attribute.

class User < Ohm::Model
  attribute :age, ->(x) { x.to_i }
end

user = User.new(age: 100)

user.age
# => 100

user.age.kind_of?(Integer)
# => true

Check rubydoc.info/github/cyx/ohm-contrib#Ohm__DataTypes to see more examples about the typecasting feature.


1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
# File 'lib/ohm.rb', line 1019

def self.attribute(name, cast = nil)
  attributes << name unless attributes.include?(name)

  if cast
    define_method(name) do
      cast[@attributes[name]]
    end
  else
    define_method(name) do
      @attributes[name]
    end
  end

  define_method(:"#{name}=") do |value|
    @attributes[name] = value
  end
end

.collection(name, model, reference = to_reference) ⇒ Object

A macro for defining a method which basically does a find.

Example:

class Post < Ohm::Model
  reference :user, :User
end

class User < Ohm::Model
  collection :posts, :Post
end

# is the same as

class User < Ohm::Model
  def posts
    Post.find(:user_id => self.id)
  end
end

918
919
920
921
922
923
# File 'lib/ohm.rb', line 918

def self.collection(name, model, reference = to_reference)
  define_method name do
    model = Utils.const(self.class, model)
    model.find(:"#{reference}_id" => id)
  end
end

.counter(name) ⇒ Object

Declare a counter. All the counters are internally stored in a different Redis hash, independent from the one that stores the model attributes. Counters are updated with the `incr` and `decr` methods, which interact directly with Redis. Their value can't be assigned as with regular attributes.

Example:

class User < Ohm::Model
  counter :points
end

u = User.create
u.incr :points

u.points
# => 1

Note: You can't use counters until you save the model. If you try to do it, you'll receive an Ohm::MissingID error.


1058
1059
1060
1061
1062
1063
1064
1065
1066
# File 'lib/ohm.rb', line 1058

def self.counter(name)
  counters << name unless counters.include?(name)

  define_method(name) do
    return 0 if new?

    redis.call("HGET", key[:counters], name).to_i
  end
end

.create(atts = {}) ⇒ Object

Syntactic sugar for Model.new(atts).save


1079
1080
1081
# File 'lib/ohm.rb', line 1079

def self.create(atts = {})
  new(atts).save
end

.exists?(id) ⇒ Boolean

Check if the ID exists within <Model>:all.


747
748
749
# File 'lib/ohm.rb', line 747

def self.exists?(id)
  redis.call("SISMEMBER", key[:all], id) == 1
end

.fetch(ids) ⇒ Object

Retrieve a set of models given an array of IDs.

Example:

User.fetch([1, 2, 3])

824
825
826
# File 'lib/ohm.rb', line 824

def self.fetch(ids)
  all.fetch(ids)
end

.find(dict) ⇒ Object

Find values in indexed fields.

Example:

class User < Ohm::Model
  attribute :email

  attribute :name
  index :name

  attribute :status
  index :status

  index :provider
  index :tag

  def provider
    email[/@(.*?).com/, 1]
  end

  def tag
    ["ruby", "python"]
  end
end

u = User.create(name: "John", status: "pending", email: "[email protected]")
User.find(provider: "me", name: "John", status: "pending").include?(u)
# => true

User.find(:tag => "ruby").include?(u)
# => true

User.find(:tag => "python").include?(u)
# => true

User.find(:tag => ["ruby", "python"]).include?(u)
# => true

808
809
810
811
812
813
814
815
816
# File 'lib/ohm.rb', line 808

def self.find(dict)
  keys = filters(dict)

  if keys.size == 1
    Ohm::Set.new(self, key, keys.first)
  else
    Ohm::Set.new(self, key, [:SINTER, *keys])
  end
end

.index(attribute) ⇒ Object

Index any method on your model. Once you index a method, you can use it in `find` statements.


830
831
832
# File 'lib/ohm.rb', line 830

def self.index(attribute)
  indices << attribute unless indices.include?(attribute)
end

.keyObject

Returns the namespace for all the keys generated using this model.

Example:

class User < Ohm::Model
end

User.key == "User"
User.key.kind_of?(String)
# => true

User.key.kind_of?(Nido)
# => true

To find out more about Nido, see:

http://github.com/soveran/nido

714
715
716
# File 'lib/ohm.rb', line 714

def self.key
  @key ||= Nido.new(self.name)
end

.list(name, model) ⇒ Object

Declare an Ohm::List with the given name.

Example:

class Comment < Ohm::Model
end

class Post < Ohm::Model
  list :comments, :Comment
end

p = Post.create
p.comments.push(Comment.create)
p.comments.unshift(Comment.create)
p.comments.size == 2
# => true

Note: You can't use the list until you save the model. If you try to do it, you'll receive an Ohm::MissingID error.


889
890
891
892
893
894
895
896
897
# File 'lib/ohm.rb', line 889

def self.list(name, model)
  track(name)

  define_method name do
    model = Utils.const(self.class, model)

    Ohm::List.new(key[name], model.key, model)
  end
end

.mutexObject


689
690
691
# File 'lib/ohm.rb', line 689

def self.mutex
  @@mutex ||= Mutex.new
end

.redisObject


685
686
687
# File 'lib/ohm.rb', line 685

def self.redis
  defined?(@redis) ? @redis : Ohm.redis
end

.redis=(redis) ⇒ Object


681
682
683
# File 'lib/ohm.rb', line 681

def self.redis=(redis)
  @redis = redis
end

.reference(name, model) ⇒ Object

A macro for defining an attribute, an index, and an accessor for a given model.

Example:

class Post < Ohm::Model
  reference :user, :User
end

# It's the same as:

class Post < Ohm::Model
  attribute :user_id
  index :user_id

  def user
    @_memo[:user] ||= User[user_id]
  end

  def user=(user)
    self.user_id = user.id
    @_memo[:user] = user
  end

  def user_id=(user_id)
    @_memo.delete(:user_id)
    self.user_id = user_id
  end
end

955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
# File 'lib/ohm.rb', line 955

def self.reference(name, model)
  reader = :"#{name}_id"
  writer = :"#{name}_id="

  attributes << reader unless attributes.include?(reader)

  index reader

  define_method(reader) do
    @attributes[reader]
  end

  define_method(writer) do |value|
    @_memo.delete(name)
    @attributes[reader] = value
  end

  define_method(:"#{name}=") do |value|
    @_memo.delete(name)
    send(writer, value ? value.id : nil)
  end

  define_method(name) do
    @_memo[name] ||= begin
      model = Utils.const(self.class, model)
      model[send(reader)]
    end
  end
end

.set(name, model) ⇒ Object

Declare an Ohm::Set with the given name.

Example:

class User < Ohm::Model
  set :posts, :Post
end

u = User.create
u.posts.empty?
# => true

Note: You can't use the set until you save the model. If you try to do it, you'll receive an Ohm::MissingID error.


859
860
861
862
863
864
865
866
867
# File 'lib/ohm.rb', line 859

def self.set(name, model)
  track(name)

  define_method name do
    model = Utils.const(self.class, model)

    Ohm::MutableSet.new(model, model.key, key[name])
  end
end

.synchronize(&block) ⇒ Object


693
694
695
# File 'lib/ohm.rb', line 693

def self.synchronize(&block)
  mutex.synchronize(&block)
end

.to_procObject

Retrieve a set of models given an array of IDs.

Example:

ids = [1, 2, 3]
ids.map(&User)

Note: The use of this should be a last resort for your actual application runtime, or for simply debugging in your console. If you care about performance, you should pipeline your reads. For more information checkout the implementation of Ohm::List#fetch.


742
743
744
# File 'lib/ohm.rb', line 742

def self.to_proc
  lambda { |id| self[id] }
end

.track(name) ⇒ Object

Keep track of `key` and remove when deleting the object.


1069
1070
1071
# File 'lib/ohm.rb', line 1069

def self.track(name)
  tracked << name unless tracked.include?(name)
end

.unique(attribute) ⇒ Object

Create a unique index for any method on your model. Once you add a unique index, you can use it in `with` statements.

Note: if there is a conflict while saving, an `Ohm::UniqueIndexViolation` violation is raised.


840
841
842
# File 'lib/ohm.rb', line 840

def self.unique(attribute)
  uniques << attribute unless uniques.include?(attribute)
end

.with(att, val) ⇒ Object

Find values in `unique` indices.

Example:

class User < Ohm::Model
  unique :email
end

u = User.create(:email => "[email protected]")
u == User.with(:email, "[email protected]")
# => true

Raises:


763
764
765
766
767
768
# File 'lib/ohm.rb', line 763

def self.with(att, val)
  raise IndexNotFound unless uniques.include?(att)

  id = redis.call("HGET", key[:uniques][att], val)
  new(:id => id).load! if id
end

Instance Method Details

#==(other) ⇒ Object Also known as: eql?

Check for equality by doing the following assertions:

  1. That the passed model is of the same type.

  2. That they represent the same Redis key.


1125
1126
1127
1128
1129
# File 'lib/ohm.rb', line 1125

def ==(other)
  other.kind_of?(model) && other.key == key
rescue MissingID
  false
end

#attributesObject

Returns a hash of the attributes with their names as keys and the values of the attributes as values. It doesn't include the ID of the model.

Example:

class User < Ohm::Model
  attribute :name
end

u = User.create(:name => "John")
u.attributes
# => { :name => "John" }

1234
1235
1236
# File 'lib/ohm.rb', line 1234

def attributes
  @attributes
end

#decr(att, count = 1) ⇒ Object

Decrement a counter atomically. Internally uses HINCRBY.


1199
1200
1201
# File 'lib/ohm.rb', line 1199

def decr(att, count = 1)
  incr(att, -count)
end

#deleteObject

Delete the model, including all the following keys:

  • <Model>:<id>

  • <Model>:<id>:counters

  • <Model>:<id>:<set name>

If the model has uniques or indices, they're also cleaned up.


1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
# File 'lib/ohm.rb', line 1330

def delete
  uniques = {}
  model.uniques.each { |field| uniques[field] = send(field) }

  script(LUA_DELETE, 0,
    { "name" => model.name,
      "id" => id,
      "key" => key
    }.to_msgpack,
    uniques.to_msgpack,
    model.tracked.to_msgpack
  )

  return self
end

#get(att) ⇒ Object

Read an attribute remotely from Redis. Useful if you want to get the most recent value of the attribute and not rely on locally cached value.

Example:

User.create(:name => "A")

Session 1     |    Session 2
--------------|------------------------
u = User[1]   |    u = User[1]
u.name = "B"  |
u.save        |
              |    u.name == "A"
              |    u.get(:name) == "B"

1154
1155
1156
# File 'lib/ohm.rb', line 1154

def get(att)
  @attributes[att] = redis.call("HGET", key, att)
end

#hashObject

Return a value that allows the use of models as hash keys.

Example:

h = {}

u = User.new

h[:u] = u
h[:u] == u
# => true

1215
1216
1217
# File 'lib/ohm.rb', line 1215

def hash
  new? ? super : key.hash
end

#incr(att, count = 1) ⇒ Object

Increment a counter atomically. Internally uses HINCRBY.


1194
1195
1196
# File 'lib/ohm.rb', line 1194

def incr(att, count = 1)
  redis.call("HINCRBY", key[:counters], att, count)
end

#keyObject

Returns the namespace for the keys generated using this model. Check `Ohm::Model.key` documentation for more details.


1085
1086
1087
# File 'lib/ohm.rb', line 1085

def key
  model.key[id]
end

#load!Object

Preload all the attributes of this model from Redis. Used internally by `Model::[]`.


1133
1134
1135
1136
# File 'lib/ohm.rb', line 1133

def load!
  update_attributes(Utils.dict(redis.call("HGETALL", key))) unless new?
  return self
end

#new?Boolean

Returns true if the model is not persisted. Otherwise, returns false.

Example:

class User < Ohm::Model
  attribute :name
end

u = User.new(:name => "John")
u.new?
# => true

u.save
u.new?
# => false

1189
1190
1191
# File 'lib/ohm.rb', line 1189

def new?
  !defined?(@id)
end

#saveObject

Persist the model attributes and update indices and unique indices. The `counter`s and `set`s are not touched during save.

Example:

class User < Ohm::Model
  attribute :name
end

u = User.new(:name => "John").save
u.kind_of?(User)
# => true

1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
# File 'lib/ohm.rb', line 1287

def save
  indices = {}
  model.indices.each { |field| indices[field] = Array(send(field)) }

  uniques = {}
  model.uniques.each { |field| uniques[field] = send(field) }

  features = {
    "name" => model.name
  }

  if defined?(@id)
    features["id"] = @id
  end

  response = script(LUA_SAVE, 0,
    features.to_msgpack,
    _sanitized_attributes.to_msgpack,
    indices.to_msgpack,
    uniques.to_msgpack
  )

  if response.is_a?(RuntimeError)
    if response.message =~ /(UniqueIndexViolation: (\w+))/
      raise UniqueIndexViolation, $1
    else
      raise response
    end
  end

  @id = response

  return self
end

#script(file, *args) ⇒ Object

Run lua scripts and cache the sha in order to improve successive calls.


1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
# File 'lib/ohm.rb', line 1348

def script(file, *args)
  cache = LUA_CACHE[redis.url]

  if cache.key?(file)
    sha = cache[file]
  else
    src = File.read(file)
    sha = redis.call("SCRIPT", "LOAD", src)

    cache[file] = sha
  end

  redis.call("EVALSHA", sha, *args)
end

#set(att, val) ⇒ Object

Update an attribute value atomically. The best usecase for this is when you simply want to update one value.

Note: This method is dangerous because it doesn't update indices and uniques. Use it wisely. The safe equivalent is `update`.


1164
1165
1166
1167
1168
1169
1170
1171
1172
# File 'lib/ohm.rb', line 1164

def set(att, val)
  if val.to_s.empty?
    redis.call("HDEL", key, att)
  else
    redis.call("HSET", key, att, val)
  end

  @attributes[att] = val
end

#to_hashObject

Export the ID of the model. The approach of Ohm is to whitelist public attributes, as opposed to exporting each (possibly sensitive) attribute.

Example:

class User < Ohm::Model
  attribute :name
end

u = User.create(:name => "John")
u.to_hash
# => { :id => "1" }

In order to add additional attributes, you can override `to_hash`:

class User < Ohm::Model
  attribute :name

  def to_hash
    super.merge(:name => name)
  end
end

u = User.create(:name => "John")
u.to_hash
# => { :id => "1", :name => "John" }

1266
1267
1268
1269
1270
1271
# File 'lib/ohm.rb', line 1266

def to_hash
  attrs = {}
  attrs[:id] = id unless new?

  return attrs
end

#to_json(*args) ⇒ Object

Export a JSON representation of the model by encoding `to_hash`.


6
7
8
# File 'lib/ohm/json.rb', line 6

def to_json(*args)
  to_hash.to_json(*args)
end

#update(attributes) ⇒ Object

Update the model attributes and call save.

Example:

User[1].update(:name => "John")

# It's the same as:

u = User[1]
u.update_attributes(:name => "John")
u.save

1375
1376
1377
1378
# File 'lib/ohm.rb', line 1375

def update(attributes)
  update_attributes(attributes)
  save
end

#update_attributes(atts) ⇒ Object

Write the dictionary of key-value pairs to the model.


1381
1382
1383
# File 'lib/ohm.rb', line 1381

def update_attributes(atts)
  atts.each { |att, val| send(:"#{att}=", val) }
end