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.increment :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")


1110
1111
1112
# File 'lib/ohm.rb', line 1110

def initialize(atts = {})
  reload_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


1128
1129
1130
# File 'lib/ohm.rb', line 1128

def id
  @id
end

Class Method Details

.[](id) ⇒ Object

Retrieve a record by ID.

Example:

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


740
741
742
# File 'lib/ohm.rb', line 740

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

.allObject

An Ohm::Set wrapper for Model.key.



1088
1089
1090
# File 'lib/ohm.rb', line 1088

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.



1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
# File 'lib/ohm.rb', line 1033

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


932
933
934
935
936
937
# File 'lib/ohm.rb', line 932

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 increment and decrement 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.increment :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.



1072
1073
1074
1075
1076
1077
1078
1079
1080
# File 'lib/ohm.rb', line 1072

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

  define_method(name) do
    return 0 if new?

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

.create(atts = {}) ⇒ Object

Syntactic sugar for Model.new(atts).save



1093
1094
1095
# File 'lib/ohm.rb', line 1093

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

.exists?(id) ⇒ Boolean

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

Returns:

  • (Boolean)


761
762
763
# File 'lib/ohm.rb', line 761

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

.fetch(ids) ⇒ Object

Retrieve a set of models given an array of IDs.

Example:

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


838
839
840
# File 'lib/ohm.rb', line 838

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


822
823
824
825
826
827
828
829
830
# File 'lib/ohm.rb', line 822

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.



844
845
846
# File 'lib/ohm.rb', line 844

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.kind_of?(Nest)
# => true

To find out more about Nest, see:

http://github.com/soveran/nest


728
729
730
# File 'lib/ohm.rb', line 728

def self.key
  @key ||= Nest.new(self.name, redis)
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.



903
904
905
906
907
908
909
910
911
# File 'lib/ohm.rb', line 903

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



707
708
709
# File 'lib/ohm.rb', line 707

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

.redisObject



703
704
705
# File 'lib/ohm.rb', line 703

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

.redis=(redis) ⇒ Object



699
700
701
# File 'lib/ohm.rb', line 699

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


969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
# File 'lib/ohm.rb', line 969

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.



873
874
875
876
877
878
879
880
881
# File 'lib/ohm.rb', line 873

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



711
712
713
# File 'lib/ohm.rb', line 711

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.



756
757
758
# File 'lib/ohm.rb', line 756

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

.track(name) ⇒ Object

Keep track of key[name] and remove when deleting the object.



1083
1084
1085
# File 'lib/ohm.rb', line 1083

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.



854
855
856
# File 'lib/ohm.rb', line 854

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:



777
778
779
780
781
782
# File 'lib/ohm.rb', line 777

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

  id = key[:uniques][att].call("HGET", 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.



1137
1138
1139
# File 'lib/ohm.rb', line 1137

def ==(other)
  other.kind_of?(model) && other.hash == hash
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" }


1281
1282
1283
# File 'lib/ohm.rb', line 1281

def attributes
  @attributes
end

#decrement(att, count = 1) ⇒ Object Also known as: decr

Decrements a counter atomically. Internally uses HINCRBY.

class Post
  counter :score
end

post = Post.create

post.decrement(:score)
post.score # => -1

post.decrement(:hits, 2)
post.score # => -3


1243
1244
1245
# File 'lib/ohm.rb', line 1243

def decrement(att, count = 1)
  increment(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.



1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
# File 'lib/ohm.rb', line 1373

def delete
  uniques = {}
  model.uniques.each do |field|
    next unless (value = send(field))
    uniques[field] = value.to_s
  end

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

  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"


1171
1172
1173
# File 'lib/ohm.rb', line 1171

def get(att)
  @attributes[att] = key.call("HGET", 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


1262
1263
1264
# File 'lib/ohm.rb', line 1262

def hash
  new? ? super : key.hash
end

#increment(att, count = 1) ⇒ Object Also known as: incr

Increments a counter atomically. Internally uses HINCRBY.

class Ad
  counter :hits
end

ad = Ad.create

ad.increment(:hits)
ad.hits # => 1

ad.increment(:hits, 2)
ad.hits # => 3


1225
1226
1227
# File 'lib/ohm.rb', line 1225

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

#keyObject

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

Raises:



1099
1100
1101
1102
# File 'lib/ohm.rb', line 1099

def key
  raise MissingID if not defined?(@id)
  model.key[id]
end

#load!Object

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



1143
1144
1145
1146
# File 'lib/ohm.rb', line 1143

def load!
  reload_attributes(Utils.dict(key.call("HGETALL"))) 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

Returns:

  • (Boolean)


1207
1208
1209
# File 'lib/ohm.rb', line 1207

def new?
  !defined?(@id)
end

#reload_attributes(atts = {}) ⇒ Object

Reset the attributes table and load the passed values.



1149
1150
1151
1152
1153
# File 'lib/ohm.rb', line 1149

def reload_attributes(atts = {})
  @attributes = {}
  @_memo = {}
  update_attributes(atts)
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


1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
# File 'lib/ohm.rb', line 1334

def save
  indices = {}
  model.indices.each do |field|
    next unless (value = send(field))
    indices[field] = Array(value).map(&:to_s)
  end

  uniques = {}
  model.uniques.each do |field|
    next unless (value = send(field))
    uniques[field] = value.to_s
  end

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

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

  @id = script(LUA_SAVE, 0,
    features.to_json,
    _sanitized_attributes.to_json,
    indices.to_json,
    uniques.to_json
  )

  return self
end

#script(file, *args) ⇒ Object

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



1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
# File 'lib/ohm.rb', line 1394

def script(file, *args)
  begin
    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)

  rescue RuntimeError

    case $!.message
    when ErrorPatterns::NOSCRIPT
      LUA_CACHE[redis.url].clear
      retry
    when ErrorPatterns::DUPLICATE
      raise UniqueIndexViolation, $1
    else
      raise $!
    end
  end
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.



1181
1182
1183
1184
1185
1186
1187
1188
1189
# File 'lib/ohm.rb', line 1181

def set(att, val)
  if val.to_s.empty?
    key.call("HDEL", att)
  else
    key.call("HSET", 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" }


1313
1314
1315
1316
1317
1318
# File 'lib/ohm.rb', line 1313

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.



4
5
6
# File 'lib/ohm/json.rb', line 4

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


1435
1436
1437
1438
# File 'lib/ohm.rb', line 1435

def update(attributes)
  update_attributes(attributes)
  save
end

#update_attributes(atts) ⇒ Object

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



1441
1442
1443
# File 'lib/ohm.rb', line 1441

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