2010-01-21 51 views
1

我試圖避免加載的N + 1查詢問題,但它不工作。相關模型仍在單獨加載。爲什麼這個rails關聯在加載之後單獨加載?

下面是相關ActiveRecords和它們之間的關係:

class Player < ActiveRecord::Base 
    has_one :tableau 
end 

Class Tableau < ActiveRecord::Base 
    belongs_to :player 
    has_many :tableau_cards 
    has_many :deck_cards, :through => :tableau_cards 
end 

Class TableauCard < ActiveRecord::Base 
    belongs_to :tableau 
    belongs_to :deck_card, :include => :card 
end 

class DeckCard < ActiveRecord::Base 
    belongs_to :card 
    has_many :tableaus, :through => :tableau_cards 
end 

class Card < ActiveRecord::Base 
    has_many :deck_cards 
end 

class Turn < ActiveRecord::Base 
    belongs_to :game 
end 

和我使用的查詢是玩家的這種方法裏面:

def tableau_contains(card_id) 
    self.tableau.tableau_cards = TableauCard.find :all, :include => [ {:deck_card => (:card)}], :conditions => ['tableau_cards.tableau_id = ?', self.tableau.id] 
    contains = false 
    for tableau_card in self.tableau.tableau_cards 
    # my logic here, looking at attributes of the Card model, with   
    # tableau_card.deck_card.card; 
    # individual loads of related Card models related to tableau_card are done here 
    end 
    return contains 
end 

是否有與範圍呢?這個tableau_contains方法是在一個較大的循環中進行一些方法調用,我最初嘗試進行急切的加載,因爲有幾個地方這些相同的對象被循環和檢查。然後,我最終嘗試了上面的代碼,加載在循環之前,並且我仍然在日誌中的tableau_cards循環內看到針對Card的單個SELECT查詢。我可以在tableau_cards循環之前看到帶有IN子句的急切加載查詢。

編輯:下面修正循環與技巧從答案

EDIT3:下面較大,外環

EDIT2額外的信息增加了更多的細節循環與目標

這裏的大循環。這是一個觀察者內的after_save的

def after_save(pa) 
    turn = Turn.find(pa.turn_id, :include => :player_actions) 
    game = Game.find(turn.game_id, :include => :goals) 
    game.players.all(:include => [ :player_goals, {:tableau => [:tableau_cards => [:deck_card => [:card]]]} ]) 
    if turn.phase_complete(pa, players) # calls player.tableau_contains(card) 
    for goal in game.goals 
     if goal.checks_on_this_phase(pa) 
     if goal.is_available(players, pa, turn) 
      for player in game.players 
      goal.check_if_player_takes(player, turn, pa) 
       ... # loop through player.tableau_cards 
      end 
      end 
     end 
     end 
    end 
    end 

這裏是在轉彎類的相關代碼:

def phase_complete(phase, players) 
    all_players_complete = true 
    for player in players 
    if(!player_completed_phase(player, phase)) 
     all_players_complete = false 
    end 
    end 
    return all_players_complete 
end 

for player in game.players正在做另一個查詢加載的球員。它被緩存了,我的意思是它有日誌中的CACHE標籤,但我會認爲根本沒有查詢,因爲game.players應該已經加載到內存中了。

從目標模型的另一個片段:

class Goal < ActiveRecord::Base 
    has_many :game_goals 
    has_many :games, :through => :game_goals 
    has_many :player_goals 
    has_many :players, :through => :player_goals 

    def check_if_player_takes(player, turn, phase) 
    ... 
    for tab_card in player.tableau_cards 
    ... 
    end 
end 

回答

6

試試這個:

class Game 
    has_many :players 
end 

變化tableau_contains邏輯如下:

class Player < ActiveRecord::Base 
    has_one :tableau 
    belongs_to :game 

    def tableau_contains(card_id) 
    tableau.tableau_cards.any?{|tc| tc.deck_card.card.id == card_id} 
    end 

end 

更改LO的after_save GIC如下:

def after_save(turn) 
    game = Game.find(turn.game_id, :include => :goals)) 
    Rails.logger.info("Begin eager loading..")     
    players = game.players.all(:include => [:player_goals, 
      {:tableau => [:tableau_cards=> [:deck_card => [:card]]]} ]) 
    Rails.logger.info("End eager loading..")     
    Rails.logger.info("Begin tableau_contains check..")     
    if players.any?{|player| player.tableau_contains(turn.card_id)} 
    # do something..     
    end 
    Rails.logger.info("End tableau_contains check..")     
end 

after_save方法急切裝載第二行所需要的數據以執行tableau_contains檢查。諸如tableau.tableau_cardstc.deck_card.card的呼叫應該/不會擊中數據庫。

問題在你的代碼:

1)指定數組的has_many協會上述

@game.players = Player.find :all, :include => ... 

語句不是一個簡單的賦值語句。它改變palyers錶行與給定遊戲的game_id。 我認爲這不是你想要的。如果您檢查數據庫表,您會注意到球員表 行的updated_time行在分配後發生了更改。

您必須將值分配給單獨的變量,如after_save方法中的代碼示例所示。

2)手工編碼協會SQL

在代碼中的許多地方,你是手工編碼的關聯數據的SQL。 Rails爲此提供了關聯。

E.g:

tcards = tableau.tableau_cards.all(:include => [ {:deck_card => (:card)}]) 

Tableau模型tableau_cards卡協會構建你必須手工編碼相同的SQL:

tcards= TableauCard.find :all, :include => [ {:deck_card => (:card)}], 
     :conditions => ['tableau_cards.tableau_id = ?', self.tableau.id] 

可以作爲被改寫。

您可以通過將has_many :through關聯添加到Player類來進一步改進上述說明。

class Player 
    has_one :tableau 
    has_many :tableau_cards, :through => :tableau 
end 

tcards = tableau_cards.all(:include => [ {:deck_card => (:card)}]) 

編輯1

我創建的應用程序來測試該代碼。它按預期工作。 Rails運行幾個SQL來加載數據,例如:

Begin eager loading.. 
SELECT * FROM `players` WHERE (`players`.game_id = 1) 
SELECT `tableau`.* FROM `tableau` WHERE (`tableau`.player_id IN (1,2)) 
SELECT `tableau_cards`.* FROM `tableau_cards` 
      WHERE (`tableau_cards`.tableau_id IN (1,2)) 
SELECT * FROM `deck_cards` WHERE (`deck_cards`.`id` IN (6,7,8,1,2,3,4,5)) 
SELECT * FROM `cards` WHERE (`cards`.`id` IN (6,7,8,1,2,3,4,5)) 
End eager loading.. 
Begin tableau_contains check.. 
End tableau_contains check.. 

我在加載數據之前沒有看到任何SQL執行。

編輯2

進行以下更改你的代碼。

def after_save(pa) 
    turn = Turn.find(pa.turn_id, :include => :player_actions) 
    game = Game.find(turn.game_id, :include => :goals) 
    players = game.players.all(:include => [ :player_goals, {:tableau => [:tableau_cards => [:deck_card => [:card]]]} ]) 
    if turn.phase_complete(pa, game, players) 
    for player in game.players 
     if(player.tableau_contains(card)) 
     ... 
     end 
    end 
    end 
end 
def phase_complete(phase, game, players) 
    all_players_complete = true 
    for player in players 
    if(!player_completed_phase(player, phase)) 
     all_players_complete = false 
    end 
    end 
    return all_players_complete 
end 

緩存的工作原理如下:

game.players # cached in the game object 
game.players.all # not cached in the game object 

players = game.players.all(:include => [:player_goals]) 
players.first.player_goals # cached 

上面的自定義關聯查詢結果第二條語句。因此AR不會緩存結果。在第三條語句中爲player_goals緩存每個播放器對象,因爲它們是使用標準關聯SQL獲取的。

+0

我甚至需要爲玩家做查詢嗎?我已將for循環更改爲僅用於game.players中的播放器,並且它似乎正常工作。 – user26270 2010-05-03 20:08:38

+0

對'players'的查詢迫切需要加載你需要的數據對象。隨後調用'game.players'返回緩存列表。你可以將兩個語句合併爲一個,但爲了便於閱讀,我寫了兩個。 – 2010-05-03 22:10:09

+0

我在after_save中使用了'game.players.all(:include => [:player_goals,{:tableau => [:tableau_cards => [:deck_card => [:card]]]}])'''然後在'tableau_contains'中我使用了'tableau.tableau_cards.any?{| tc | tc.deck_card.card.id == card_id}',它仍然單獨加載tableau_contains中每個deck_card和卡片;我仔細檢查了玩家的tableau或tableau_cards關聯是否在任何地方被重置,但我找不到任何關聯,所以我仍然不知道爲什麼deck_cards和卡片會在其上面的熱切加載之後單獨被查詢 – user26270 2010-05-04 14:16:16

1

問題排名第一的是:你每次重置player.tableau.tableau_cards

player.tableau.tableau_cards = TableauCard.find :all, :include => [ {:deck_card => (:card)}], :conditions => ['tableau_cards.tableau_id = ?', player.tableau.id] 

如果被認爲是一個臨時數組然後你正在做的工作超過必要。下面會更好:

temp_tableau_cards = TableauCard.find :all, :include => [ {:deck_card => (:card)}], :conditions => ['tableau_cards.tableau_id = ?', player.tableau.id] 

我還要將二者分開操作,如果你實際上是試圖設置tableau_cards,並做一些給他們。

player.tableau.tableau_cards = TableauCard.find :all, :include => [ {:deck_card => (:card)}], :conditions => ['tableau_cards.tableau_id = ?', player.tableau.id] 
card.whatever_logic if player.tableau.tableau_cards.include? card 

同樣,看起來當你不需要的時候你正在翻一翻查詢。

+0

回覆:問題#1:我沒有看到我是如何「重置」player.tableau.tableau_cards。在此之前它們不會被加載,所以我試圖加載tableau_cards以及爲每個玩家一次性加載相關的deck_cards和卡模型。稍後,是的,我再次在tableau_contains方法中加載播放器的tableau_cards。那是當我認爲原始玩家的tableau_cards關聯已被卸載或超出範圍時,因爲我看到了單個卡被加載到tableau_contains方法中。 – user26270 2010-04-30 16:51:38

+0

這就是爲什麼我在player.tableau_contains方法中添加了第二個Tableau_card.find ...以查看這是否會消除各個卡片模型被單獨加載,而不是單獨加載。所以原來的問題仍然存在:在player.tableau_contains方法中,爲什麼在包含在TableauCard.find中後,單個Card模型被加載:include => [{:deck_card =>(:card)}]查詢? – user26270 2010-04-30 16:54:13

+0

您可以使用'player.tableau.tableau_cards(:include => {:deck_card =>:card})'''''''''''''''''''''player.tableau''來加載已存在DB的'tableau_cards'。 @Sixty在說的是你試圖通過在'player.tableau.tableau_cards ='中分配關聯來重新設置它們,這通常會引發一個SQL INSERT來處理與'player'不匹配的記錄.tableau.tableau_cards()'。這反過來可能會重新加載該協會的記錄。這裏的「加載」是一個糟糕的選擇,因爲你可能意味着「存儲在數據庫中」或「不需要從數據庫中加載」。 – 2010-04-30 20:04:38

1

如果您從player.tableau.tableau_cards = cards呼叫中分離出cards = TableauCard.find...呼叫,會發生什麼情況?也許rails會在代碼中重置關聯的緩存記錄,然後重新加載關聯。

這也將允許您通過顯式傳遞變量來確保將相同的數組傳遞到tableau_contains

您似乎試圖通過對player.cards.tableau_cards關聯進行多次調用來保留已加載的關聯。我不確定這個功能是否可以用rails的方式工作。我相信它緩存從sql語句返回的原始數據,但不是返回的實際數組。所以:

def test_association_identity 
    a = player.tableau.tableau_cards.all(
      :include => {:deck_card => :card}) 
      #=> Array with object_id 12345 
      # and all the eager loaded deck and card associations set up 
    b = player.tableau.tableau_cards 
      #=> Array 320984230 with no eager loaded associations set up. 
      #But no extra sql query since it should be cached. 
    assert_equal a.object_id, b.object_id #probably fails 
    a.each{|card| card.deck_card.card} 
    puts("shouldn't have fired any sql queries, 
     unless the b call reloaded the association magically.") 
    b.each{|card| card.deck_card.card; puts("should fire a query 
             for each deck_card and card")} 
    end 

,我能想到的,以幫助唯一的其他東西是分散在整個代碼的一些輸出,看看究竟在何處延遲加載正在發生的事情。

這裏就是我的意思是:

#Observer

def after_save(pa) 
    @game = Game.find(turn.game_id, :include => :goals) 
    @game.players = Player.find(:all, 
       :include => [ {:tableau => (:tableau_cards)},:player_goals ], 
       :conditions => ['players.game_id =?', @game.id] 
    for player in @game.players 
    cards = TableauCard.find(:all, 
      :include =>{:deck_card => :card}, 
      :conditions => ['tableau_cards.tableau_id = ?', player.tableau.id]) 
    logger.error("First load") 
    player.tableau.tableau_cards = cards #See above comments as well. 
    # Both sides of this^line should always be == since: 
    # Given player.tableau => Tableau(n) then Tableau(n).tableau_cards 
    # will all have tableau_id == n. In other words, if there are 
    # `tableau_cards.`tableau_id = n in the db (as in the find call), 
    # then they'll already be found in the tableau.tableau_cards call. 
    logger.error("Any second loads?") 
    if(tableau_contains(cards,card)) 
     logger.error("There certainly shouldn't be any loads here.") 
     #so that we're not relying on any additional association calls, 
     #this should at least remove one point of confusion. 
    ... 
    end 
    end 
end 

#Also in the Observer, for just these purposes (it can be moved back out 
#to Player after the subject problem here is understood better) 

def tableau_contains(cards,card_id) 
    contains = false 
      logger.error("Is this for loop loading the cards?") 
    for card in cards 
      logger.error("Are they being loaded after `card` is set?") 
    # my logic here, looking at attributes of the Card model, with   
    # card.deck_card.card; 
    logger.error("What about prior to this call?") 
    end 
    return contains 
end 
+0

你在解釋我的意圖時是正確的:我試圖將現有玩家的桌面卡從數據庫讀入內存中。感謝您解答這個問題,並解釋@Sixty和我是如何在他的答案下的評論中以不同的方式解釋事物的。我從未想過我正在更新現有的關聯。看起來像我將不得不像@Sixty建議的那樣使用臨時數組。我想現在我明白了,但我仍然覺得奇怪,你無法加載現有的關聯並使用player.tableau.tableau_cards參考。 – user26270 2010-05-03 17:23:51

+0

我試着用player.tableau.tableau_cards(:include => {:deck_card =>:card})急切地加載已存儲的tableau_cards,從你下面的評論@ Sixty的回答中;它不會加載deck_cards或卡 – user26270 2010-05-03 17:49:14

+0

特赦,它是'player.tableau.tableau_cards.all(:include => {:deck_card =>:卡}}''。 – 2010-05-03 17:59:38

相關問題