views:

32

answers:

1

Suppose I've got a card-game app, which features a Player model, which has an actions integer column; and a Card model. A player can play a card they own, which costs an action; one particular card grants two actions when it's played.

If I code this as follows:

class Player < ActiveRecord::Base
  has_many :cards

  def play_card(card)
    raise "Not yours!" unless cards.include? card
    self.actions -= 1
    card.play
    save!
  end
end

class Card < ActiveRecord::Base
  belongs_to :player

  def play
    player.actions += 2
  end
end

... then the net effect of Player#play_card is to decrement actions by 1. The only way I've found to make both changes apply to the same object, thereby resulting in a net increment of 1 action, is to define the functions like this:

class Player < ActiveRecord::Base
  has_many :cards

  def play_card(card)
    raise "Not yours!" unless cards.include? card
    self.actions -= 1

    // Stick that change in the Database
    save!

    card.play
  end
end

class Card < ActiveRecord::Base
  belongs_to :player

  def play
    // Force reload of the player object
    player(true).actions += 2

    // And save again
    player.save!
  end
end

But that turns a single database write into two writes and a read! Surely there must be a better way. What am I missing?

A: 

In the first version of your code you are loading the same row of the table players but while you are expecting rails to be smart enough to recognize that it has already load this row in memory, rails doesn't work that way. So when you are issuing a +=2 on player it does he +=2 on another instance than the one on which you have done -=1.

i've setup a little example to show that there are too instance of the same row:

ruby-1.8.7-p174 > p_instance_1 = Player.first
 => #<Player id: 1, actions: -1, created_at: "2010-10-13 17:07:22", updated_at: "2010-10-13 17:11:00"> 
ruby-1.8.7-p174 > c = Card.first
 => #<Card id: 1, player_id: 1, created_at: "2010-10-13 17:07:28", updated_at: "2010-10-13 17:07:28"> 
ruby-1.8.7-p174 > p_instance_2 = c.player
 => #<Player id: 1, actions: -1, created_at: "2010-10-13 17:07:22", updated_at: "2010-10-13 17:11:00"> 
ruby-1.8.7-p174 > p_instance_1.object_id
 => 2158703080 
ruby-1.8.7-p174 > p_instance_2.object_id
 => 2156926840 
ruby-1.8.7-p174 > p_instance_1.actions += 1
 => 0 
ruby-1.8.7-p174 > p_instance_2.actions += 1
 => 0

So finally as you haven't save the instance with the +=2 applied, there's only the one with the -1 that is saved

UPDATE

You can try to trick rails to use the same instance of player all the way. This is a little bit ugly but it works.

class Player < ActiveRecord::Base
  has_many :cards

  def play_card(card)
    raise "Not yours!" unless cards.include? card
    new_self = card.player
    card.play
    new_self.actions -= 1
    new_self.save!
  end
end

class Card < ActiveRecord::Base
  belongs_to :player

  def play
    player.actions += 2
  end
end

so when you input those commands:

ruby-1.8.7-p174 > p = Player.first
 => #<Player id: 1, actions: 0, created_at: "2010-10-14 13:33:51", updated_at: "2010-10-14 13:33:51"> 
ruby-1.8.7-p174 > p.play_card(Card.first)
 => true 
ruby-1.8.7-p174 > p
 => #<Player id: 1, actions: 0, created_at: "2010-10-14 13:33:51", updated_at: "2010-10-14 13:33:51"> 
ruby-1.8.7-p174 > p.reload
 => #<Player id: 1, actions: 1, created_at: "2010-10-14 13:33:51", updated_at: "2010-10-14 13:34:40"> 

You have the right number of actions in player, and in the logs card is only loaded once:

  Player Load (0.5ms)   SELECT * FROM "players" LIMIT 1
  Card Load (0.2ms)   SELECT * FROM "cards" LIMIT 1
  Card Load (0.2ms)   SELECT "cards".id FROM "cards" WHERE ("cards"."id" = 1) AND ("cards".player_id = 1) LIMIT 1
  Player Load (0.1ms)   SELECT * FROM "players" WHERE ("players"."id" = 1) 
  Player Update (0.6ms)   UPDATE "players" SET "updated_at" = '2010-10-14 13:34:40', "actions" = 1 WHERE "id" = 1

To sum up the whole thing, I would say that there's something wrong in your code design. If i understand well,what you would like is that every AR instance of a table row is the same object in the ObjectSpace, but I guess that if rails was build that way it would introduce strange behaviors where you could work on half backed object changed in validations and other hooks.

hellvinz
Ok, I understand that the two instances are different "objects" to Ruby. Is there a workaround to prevent doing three DB operations (other than passing the Player instance into Card#play, since that rather defeats the point of the association)?
Chris
Thanks for the workaround. It is a bit ugly, but it's nice to know I have the option. I take the point about the code design, but I'm curious if you can suggest a better option? I suppose (I'm using STI) I could put the action decrement into the Card's superclass, and the increment in the derived class...
Chris
'wrong' is maybe a little bit too strong. I just feel strange about updating the action field of the player model in the card model.
hellvinz