Class: Ohm::Model

Inherits:
Object
  • Object
show all
Includes:
Scrivener::Validations
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 => "foo@bar.com")
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

- (Model) initialize(atts = {})

Initialize a model using a dictionary of attributes.

Example:

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


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

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

Instance Attribute Details

- (Object) id

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:



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

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

Class Method Details

+ (Object) [](id)

Retrieve a record by ID.

Example:

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


750
751
752
# File 'lib/ohm.rb', line 750

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

+ (Object) all

An Ohm::Set wrapper for Model.key.



1062
1063
1064
# File 'lib/ohm.rb', line 1062

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

+ (Object) attribute(name, cast = nil)

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

Example:

class User < Ohm::Model
  attribute :name
end

# It's the same as:

class User < Ohm::Model
  def name
    @attributes[:name]
  end

  def name=(name)
    @attributes[:name] = name
  end
end


1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
# File 'lib/ohm.rb', line 1016

def self.attribute(name, cast = nil)
  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

+ (Object) collection(name, model, reference = to_reference)

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


930
931
932
933
934
935
# File 'lib/ohm.rb', line 930

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

+ (Object) collections (protected)



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

def self.collections
  @collections ||= []
end

+ (Object) conn



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

def self.conn
  @conn ||= Connection.new(name, Ohm.conn.options)
end

+ (Object) connect(options)



708
709
710
711
712
# File 'lib/ohm.rb', line 708

def self.connect(options)
  @key = nil
  @lua = nil
  conn.start(options)
end

+ (Object) counter(name)

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

Ohm.redis.hget "User:1:counters", "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.



1053
1054
1055
1056
1057
1058
1059
# File 'lib/ohm.rb', line 1053

def self.counter(name)
  define_method(name) do
    return 0 if new?

    key[:counters].hget(name).to_i
  end
end

+ (Object) create(atts = {})

Syntactic sugar for Model.new(atts).save



1067
1068
1069
# File 'lib/ohm.rb', line 1067

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

+ (Object) db



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

def self.db
  conn.redis
end

+ (Boolean) exists?(id)

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

Returns:

  • (Boolean)


771
772
773
# File 'lib/ohm.rb', line 771

def self.exists?(id)
  key[:all].sismember(id)
end

+ (Object) filters(dict) (protected)



1379
1380
1381
1382
1383
1384
1385
1386
1387
# File 'lib/ohm.rb', line 1379

def self.filters(dict)
  unless dict.kind_of?(Hash)
    raise ArgumentError,
      "You need to supply a hash with filters. " +
      "If you want to find by ID, use #{self}[id] instead."
  end

  dict.map { |k, v| toindices(k, v) }.flatten
end

+ (Object) find(dict)

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 => "foo@me.com")
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


830
831
832
833
834
835
836
837
838
# File 'lib/ohm.rb', line 830

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

  if keys.size == 1
    Ohm::Set.new(keys.first, key, self)
  else
    Ohm::MultiSet.new(key, self).append(:sinterstore, keys)
  end
end

+ (Object) index(attribute)

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



842
843
844
# File 'lib/ohm.rb', line 842

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

+ (Object) indices (protected)



1367
1368
1369
# File 'lib/ohm.rb', line 1367

def self.indices
  @indices ||= []
end

+ (Object) key

The namespace for all the keys generated using this model.

Example:

class User < Ohm::Model

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

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

To find out more about Nest, see:

http://github.com/soveran/nest


738
739
740
# File 'lib/ohm.rb', line 738

def self.key
  @key ||= Nest.new(self.name, db)
end

+ (Object) list(name, model)

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.



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

def self.list(name, model)
  collections << name unless collections.include?(name)

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

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

+ (Object) lua



718
719
720
# File 'lib/ohm.rb', line 718

def self.lua
  @lua ||= Lua.new(File.join(Dir.pwd, "lua"), db)
end

+ (Object) new_id (protected)



1399
1400
1401
# File 'lib/ohm.rb', line 1399

def self.new_id
  key[:id].incr
end

+ (Object) reference(name, model)

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


967
968
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
# File 'lib/ohm.rb', line 967

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

  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

+ (Object) set(name, model)

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.



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

def self.set(name, model)
  collections << name unless collections.include?(name)

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

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

+ (Object) to_proc

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::Set#fetch.



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

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

+ (Object) to_reference (protected)



1360
1361
1362
1363
1364
1365
# File 'lib/ohm.rb', line 1360

def self.to_reference
  name.to_s.
    match(/^(?:.*::)*(.*)$/)[1].
    gsub(/([a-z\d])([A-Z])/, '\1_\2').
    downcase.to_sym
end

+ (Object) toindices(att, val) (protected)

Raises:



1389
1390
1391
1392
1393
1394
1395
1396
1397
# File 'lib/ohm.rb', line 1389

def self.toindices(att, val)
  raise IndexNotFound unless indices.include?(att)

  if val.kind_of?(Enumerable)
    val.map { |v| key[:indices][att][v] }
  else
    [key[:indices][att][val]]
  end
end

+ (Object) unique(attribute)

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.



852
853
854
# File 'lib/ohm.rb', line 852

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

+ (Object) uniques (protected)



1371
1372
1373
# File 'lib/ohm.rb', line 1371

def self.uniques
  @uniques ||= []
end

+ (Object) with(att, val)

Find values in `unique` indices.

Example:

class User < Ohm::Model
  unique :email
end

u = User.create(:email => "foo@bar.com")
u == User.with(:email, "foo@bar.com")
# => true


787
788
789
790
# File 'lib/ohm.rb', line 787

def self.with(att, val)
  id = key[:uniques][att].hget(val)
  id && self[id]
end

Instance Method Details

- (Object) ==(other) 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.



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

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

- (Object) __save__



1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
# File 'lib/ohm.rb', line 1279

def __save__
  Transaction.new do |t|
    t.watch(*_unique_keys)
    t.watch(key) if not new?

    t.before do
      _initialize_id if new?
    end

    existing = nil
    uniques  = nil
    indices  = nil

    t.read do
      _verify_uniques
      existing = key.hgetall
      uniques  = _read_index_type(:uniques)
      indices  = _read_index_type(:indices)
    end

    t.write do
      model.key[:all].sadd(id)
      _delete_uniques(existing)
      _delete_indices(existing)
      _save
      _save_indices(indices)
      _save_uniques(uniques)
    end
  end
end

- (Object) _delete_indices(atts) (protected)



1476
1477
1478
1479
1480
1481
1482
# File 'lib/ohm.rb', line 1476

def _delete_indices(atts)
  model.indices.each do |att|
    val = atts[att.to_s]

    model.key[:indices][att][val].srem(id)
  end
end

- (Object) _delete_uniques(atts) (protected)



1470
1471
1472
1473
1474
# File 'lib/ohm.rb', line 1470

def _delete_uniques(atts)
  model.uniques.each do |att|
    model.key[:uniques][att].hdel(atts[att.to_s])
  end
end

- (Object) _detect_duplicate (protected)



1449
1450
1451
1452
1453
1454
# File 'lib/ohm.rb', line 1449

def _detect_duplicate
  model.uniques.detect do |att|
    id = model.key[:uniques][att].hget(send(att))
    id && id != self.id.to_s
  end
end

- (Object) _initialize_id (protected)



1418
1419
1420
# File 'lib/ohm.rb', line 1418

def _initialize_id
  @id = model.new_id.to_s
end

- (Object) _read_index_type(type) (protected)



1456
1457
1458
1459
1460
1461
1462
# File 'lib/ohm.rb', line 1456

def _read_index_type(type)
  {}.tap do |ret|
    model.send(type).each do |att|
      ret[att] = send(att)
    end
  end
end

- (Object) _save (protected)



1436
1437
1438
1439
1440
1441
# File 'lib/ohm.rb', line 1436

def _save
  catch :empty do
    key.del
    key.hmset(*_skip_empty(attributes).to_a.flatten)
  end
end

- (Object) _save_indices(indices) (protected)



1484
1485
1486
1487
1488
1489
1490
# File 'lib/ohm.rb', line 1484

def _save_indices(indices)
  indices.each do |att, val|
    model.toindices(att, val).each do |index|
      index.sadd(id)
    end
  end
end

- (Object) _save_uniques(uniques) (protected)



1464
1465
1466
1467
1468
# File 'lib/ohm.rb', line 1464

def _save_uniques(uniques)
  uniques.each do |att, val|
    model.key[:uniques][att].hset(val, id)
  end
end

- (Object) _skip_empty(atts) (protected)



1422
1423
1424
1425
1426
1427
1428
1429
1430
# File 'lib/ohm.rb', line 1422

def _skip_empty(atts)
  {}.tap do |ret|
    atts.each do |att, val|
      ret[att] = send(att).to_s unless val.to_s.empty?
    end

    throw :empty if ret.empty?
  end
end

- (Object) _unique_keys (protected)



1432
1433
1434
# File 'lib/ohm.rb', line 1432

def _unique_keys
  model.uniques.map { |att| model.key[:uniques][att] }
end

- (Object) _verify_uniques (protected)



1443
1444
1445
1446
1447
# File 'lib/ohm.rb', line 1443

def _verify_uniques
  if att = _detect_duplicate
    raise UniqueIndexViolation, "#{att} is not unique."
  end
end

- (Object) attributes



1201
1202
1203
# File 'lib/ohm.rb', line 1201

def attributes
  @attributes
end

- (Object) db (protected)



1414
1415
1416
# File 'lib/ohm.rb', line 1414

def db
  model.db
end

- (Object) decr(att, count = 1)

Decrement a counter atomically. Internally uses HINCRBY.



1180
1181
1182
# File 'lib/ohm.rb', line 1180

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

- (Object) delete

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.



1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
# File 'lib/ohm.rb', line 1318

def delete
  transaction do |t|
    t.read do |store|
      store[:existing] = key.hgetall
    end

    t.write do |store|
      _delete_uniques(store[:existing])
      _delete_indices(store[:existing])
      model.collections.each { |e| key[e].del }
      model.key[:all].srem(id)
      key[:counters].del
      key.del
    end

    yield t if block_given?
  end
end

- (Object) get(att)

Read an attribute remotly 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"


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

def get(att)
  @attributes[att] = key.hget(att)
end

- (Object) hash

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

Example:

h = {}

u = User.new

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


1196
1197
1198
# File 'lib/ohm.rb', line 1196

def hash
  new? ? super : key.hash
end

- (Object) incr(att, count = 1)

Increment a counter atomically. Internally uses HINCRBY.



1175
1176
1177
# File 'lib/ohm.rb', line 1175

def incr(att, count = 1)
  key[:counters].hincrby(att, count)
end

- (Object) key

Manipulate the Redis hash of attributes directly.

Example:

class User < Ohm::Model
  attribute :name
end

u = User.create(:name => "John")
u.key.hget(:name)
# => John

For more details see

http://github.com/soveran/nest


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

def key
  model.key[id]
end

- (Object) load!

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



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

def load!
  update_attributes(key.hgetall) unless new?
  return self
end

- (Object) model (protected)



1410
1411
1412
# File 'lib/ohm.rb', line 1410

def model
  self.class
end

- (Boolean) new?

Returns:

  • (Boolean)


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

def new?
  !defined?(@id)
end

- (Object) save(&block)

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

If the model is not valid, nil is returned. Otherwise, the persisted model is returned.

Example:

class User < Ohm::Model
  attribute :name

  def validate
    assert_present :name
  end
end

User.new(:name => nil).save
# => nil

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


1264
1265
1266
1267
# File 'lib/ohm.rb', line 1264

def save(&block)
  return if not valid?
  save!(&block)
end

- (Object) save! {|t| ... }

Saves the model without checking for validity. Refer to `Model#save` for more details.

Yields:

  • (t)


1271
1272
1273
1274
1275
1276
1277
# File 'lib/ohm.rb', line 1271

def save!
  t = __save__
  yield t if block_given?
  t.commit(db)

  return self
end

- (Object) set(att, val)

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`.



1165
1166
1167
1168
# File 'lib/ohm.rb', line 1165

def set(att, val)
  val.to_s.empty? ? key.hdel(att) : key.hset(att, val)
  @attributes[att] = val
end

- (Object) to_hash

Export the ID and the errors 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" }


1233
1234
1235
1236
1237
1238
1239
# File 'lib/ohm.rb', line 1233

def to_hash
  attrs = {}
  attrs[:id] = id unless new?
  attrs[:errors] = errors if errors.any?

  return attrs
end

- (Object) to_json(*args)

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

- (Object) transaction (protected)



1405
1406
1407
1408
# File 'lib/ohm.rb', line 1405

def transaction
  txn = Transaction.new { |t| yield t }
  txn.commit(db)
end

- (Object) update(attributes)

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


1349
1350
1351
1352
# File 'lib/ohm.rb', line 1349

def update(attributes)
  update_attributes(attributes)
  save
end

- (Object) update_attributes(atts)

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



1355
1356
1357
# File 'lib/ohm.rb', line 1355

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