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?
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.
Related
I'm seeing some weird behaviour in my models, and was hoping someone could shed some light on the issue.
# user model
class User < ActiveRecord::Base
has_many :events
has_and_belongs_to_many :attended_events
def attend(event)
self.attended_events << event
end
end
# helper method in /spec-dir
def attend_events(host, guest)
host.events.each do |event|
guest.attend(event)
end
end
This, for some reason inserts the event with id 2 before the event with id 1, like so:
#<ActiveRecord::Associations::CollectionProxy [#<Event id: 2, name: "dummy-event", user_id: 1>, #<Event id: 1, name: "dummy-event", user_id: 1>
But, when I do something seemlingly random - like for instance change the attend_event method like so:
def attend_event(event)
self.attended_events << event
p self.attended_events # random puts statement
end
It gets inserted in the correct order.
#<ActiveRecord::Associations::CollectionProxy [#<Event id: 1, name: "dummy-event", user_id: 1>, #<Event id: 2, name: "dummy-event", user_id: 1>
What am I not getting here?
Unless you specify an order on the association, associations are unordered when they are retrieved from the database (the generated sql won't have an order clause so the database is free to return things in whatever order it wants)
You can specify an order by doing (rails 4.x upwards)
has_and_belongs_to_many :attended_events, scope: -> {order("something")}
or, on earlier versions
has_and_belongs_to_many :attended_events, :order => "something"
When you've just inserted the object you may see a different object - here you are probably seeing the loaded version of the association, which is just an array (wrapped by the proxy)
I have a couple models set up like this:
class Contract < ActiveRecord::Base
has_many :invoices, dependent: :destroy
end
class Invoice < ActiveRecord::Base
belongs_to :contract
end
I have a feature test set up like this...
feature "Some cool functionality", js: true do
let(:contract) { create(:contract) }
let(:invoice) { create(:invoice, contract: contract) }
#etc...
end
While debugging the test I noticed this...
(byebug) p contract
#<Contract id: 1, created_at: "2014-02-25 01:52:52", updated_at: "2014-02-25 01:52:52">
(byebug) p invoice
#<Invoice id: 1, contract_id: 1, created_at: "2014-02-25 01:52:52", updated_at: "2014-02-25 01:52:52">
Here's the confusing part:
(byebug) p contract.invoices.first
nil
I thought that would return my invoice defined in my feature test.
However, I think I can verify that contract has one invoice...
(byebug) p contract.invoices.count
(1.0ms) SELECT COUNT(*) FROM "invoices" WHERE "invoices"."contract_id" = $1 [["contract_id", 1]]
1
What's going on here?
Try calling contract.reload
When you call let the value of the variable/method is cached after the first time it is invoked. So when you call contract.invoices.first you are invoking invoices on the cached contract object currently in memory.
To ensure you're using the most up-to-date data, try using Rails' reload! console method:
# from command line
reload!
A has many Bs, B has many Cs. C has a property called thing:
class A < ActiveRecord::Base
has_many :bs
end
class B < ActiveRecord::Base
belongs_to :a
has_many :cs
end
class C < ActiveRecord::Base
belongs_to :b
attr_accessible :thing
end
I'd like to query for all Bs belonging to an A, and eagerly load Cs that belong to said B:
> a = A.first
A Load (0.2ms) SELECT "as".* FROM "as" LIMIT 1
=> #<A id: 1, created_at: "2012-08-21 09:25:18", updated_at: "2012-08-21 09:25:18">
> bs = a.bs.includes(:cs)
B Load (0.2ms) SELECT "bs".* FROM "bs" WHERE "bs"."a_id" = 1
C Load (0.1ms) SELECT "cs".* FROM "cs" WHERE "cs"."b_id" IN (1)
=> [#<B id: 1, a_id: 1, created_at: "2012-08-21 09:25:22", updated_at: "2012-08-21 09:25:22", thing: nil>]
>
This works well:
> bs[0]
=> #<B id: 1, a_id: 1, created_at: "2012-08-21 09:25:22", updated_at: "2012-08-21 09:25:22", thing: nil>
> bs[0].cs
=> [#<C id: 1, b_id: 1, thing: 2, created_at: "2012-08-21 09:29:31", updated_at: "2012-08-21 09:29:31">]
>
—but not in the case where I want to later perform where() searches on the Cs that belong to B instances:
> bs[0].cs.where(:thing => 1)
C Load (0.2ms) SELECT "cs".* FROM "cs" WHERE "cs"."b_id" = 1 AND "cs"."thing" = 1
=> []
> bs[0].cs.where(:thing => 2)
C Load (0.2ms) SELECT "cs".* FROM "cs" WHERE "cs"."b_id" = 1 AND "cs"."thing" = 2
=> [#<C id: 1, b_id: 1, thing: 2, created_at: "2012-08-21 09:29:31", updated_at: "2012-08-21 09:29:31">]
>
Note that queries are re-issued, despite our having the available information.
Of course, I can just use Enumerable#select:
> bs[0].cs.select {|c| c.thing == 2}
=> [#<C id: 1, b_id: 1, thing: 2, created_at: "2012-08-21 09:29:31", updated_at: "2012-08-21 09:29:31">]
>
This avoids a re-query, but I was sort of hoping Rails could do something similar itself.
The real downside is that I want to use this code where we don't know if the association has been eagerly loaded or not. If it hasn't, then the select method will load all C for B before doing the filter, whereas the where method would produce SQL to get a smaller set of data.
I'm not convinced this matters at all, but if there was something I'm missing about eager loading, I'd love to hear it.
I don't think you're missing anything. I don't believe active record can do anything that smart -- and it would be very difficult to do reliably I think. Like you say, it would have to determine whether you've eager-loaded the association, but it would also have to make a guess as to whether it would be faster to loop through the in-memory collection of Cs (if it's a small collection) or whether it would be faster to go to the database to get all the appropriate Cs in one shot (if it's a very large collection).
In your case, the best thing might be to just set the default scope to always preload the cs, and maybe even write your own fancy method to get them by thing. Something like this maybe:
class B < ActiveRecord::Base
belongs_to :a
has_many :cs
default_scope includes(:cs)
def cs_by_thing(thing)
cs.select{|c|c.thing == thing}
end
end
Then you could always know that you never go back to the DB when querying for your cs:
a = A.first
[db access]
a.bs.first
[db access]
a.bs.first.cs
a.bs.first.cs_by_thing(1)
a.bs.first.cs_by_thing(2)
Maybe the title is confusing, but I didn't know how to explain my doubt.
Say I have the following class methods that will be helpful in order to do chainings to query a model called Player. A Player belongs_to a User, but if I want to fetch Players from a particular village or city, I have to fetch the User model.
def self.by_village(village)
joins(:user).where(:village => "village")
end
def self.by_city(city)
joins(:user).where(:city => "city")
end
Let's say I want to fetch a Player by village but also by city, so I would do...
Player.by_city(city).by_village(village).
This would be doing a join of the User twice, and I don't think that is correct.. Right?
So my question is: What would be the correct way of doing so?
I haven't tried that, but I would judge the answer to your question by the actual sql query ActiveRecord generates. If it does only one join, I would use it as you did, if this results in two joins you could create a method by_village_and_city.
OK. Tried it now:
1.9.2p290 :022 > Player.by_city("Berlin").by_village("Kreuzberg")
Player Load (0.3ms) SELECT "players".* FROM "players" INNER JOIN "users" ON "users"."id" = "players"."user_id" WHERE "users"."city" = 'Berlin' AND "users"."village" = 'Kreuzberg'
=> [#<Player id: 1, user_id: 1, created_at: "2012-07-28 17:05:35", updated_at: "2012-07-28 17:05:35">, #<Player id: 2, user_id: 2, created_at: "2012-07-28 17:08:14", updated_at: "2012-07-28 17:08:14">]
So, ActiveRecors combines the two queries, does the right thing and I would use it, except:
I had to change your implementation though:
class Player < ActiveRecord::Base
belongs_to :user
def self.by_village(village)
joins(:user).where('users.village' => village)
end
def self.by_city(city)
joins(:user).where('users.city' => city)
end
end
and what you're doing is usually handled with parameterized scopes:
class Player < ActiveRecord::Base
belongs_to :user
scope :by_village, lambda { |village| joins(:user).where('users.village = ?', village) }
scope :by_city, lambda { |city| joins(:user).where('users.city = ?', city) }
end
I have a page where I display a list of threads for a user whether the tread was started by them or if they were the recipient of one started.
Here is my model:
has_many :threads_as_starter, :class_name => 'MessageThread', :foreign_key => 'sender_id'
has_many :threads_as_recipient, :class_name => 'MessageThread', :foreign_key => 'recipient_id'
What I'd like to do is define a method that I can store in a in instance variable and loop through on my view page that displays threads of the current_user.
When I run: MessageThread.where( 'sender_id OR recipient_id = ?', 4 )
1.9.3p0 :045 > MessageThread.where( 'sender_id OR recipient_id = ?', 4 )
MessageThread Load (0.4ms) SELECT `message_threads`.* FROM `message_threads` WHERE (sender_id OR recipient_id = 4) ORDER BY message_threads.created_at DESC
=> [#<MessageThread id: 89, message_id: 219, sender_id: 4, recipient_id: 38, status: 0, created_at: "2012-02-15 12:26:17", updated_at: "2012-02-15 12:26:17">, #<MessageThread id: 88, message_id: 218, sender_id: 2, recipient_id: 4, status: 0, created_at: "2012-02-14 13:41:19", updated_at: "2012-02-14 13:41:19">, #<MessageThread id: 87, message_id: 210, sender_id: 1, recipient_id: 2, status: 0, created_at: "2012-02-14 13:31:12", updated_at: "2012-02-14 13:31:12">]
I'm confused as to why it's showing me rows where the sender or recipient id isn't equal to 4.
What query would return all results where the sender_id is 4 but also all the results where the recipient_id is 4? I need to give the signed in user to see a list of all their current threads. Ones that were started by them and ones that weren't.
User A and B only have 1 thread in the message_threads table but either of them can have more threads but with different users but only 1 with each of those users. I use this message_thread table to reference conversations in my messages table where I use the acts_as_tree gem.
There must be a way I can group threads_as_starter and threads_as_recipient e.g. all_threads then call current_user.all_threads to return all.
Thanks in advance
Kind regards
where('sender_id OR recipient_id = ?', 4)
should be
where('sender_id = ? OR recipient_id = ?', 4)
or probably
where('sender_id = ? OR recipient_id = ?', 4, 4)