简体   繁体   中英

Setting up has_many through association

I have 3 models

class Battle < ActiveRecord::Base
  has_many :battle_videos
  has_many :videos, :through => :battle_videos
  ...
end


class BattleVideo < ActiveRecord::Base
  belongs_to :battle
  belongs_to :video  
  validates :code, :presence => true
end

class Video < ActiveRecord::Base
  belongs_to :user, :foreign_key => :user_id, :class_name => "User"
  ...
end

BattleVideo has attribute "code" (left_side || right_side) which determines BATTLE SIDE (there are always 2 sides in battles, coz always playing -- 1 vs. 1, team vs. team etc.)

I would like to specify association (left_side_video, right_side_video) in model video , which gets video for chosen side.

Now to get video for SIDE 1 (left) - use this code

Battle.first.battle_videos.where("battle_videos.code = 'left_side'").first.video

I want to get my battle videos like this

Battle.first.left_side_video

I think, Battle model should look like this, but it doesnt work (works only left side)

class Battle < ActiveRecord::Base
  has_many :battle_videos
  has_many :videos, :through => :battle_videos

  has_many :left_side_video, :through => :battle_videos, :source => :video, :conditions => ["battle_videos.code = 'left_side'"]
  has_many :right_side_video, :through => :battle_videos, :source => :video, :conditions => ["battle_videos.code = 'right_side'"]
end

UPDATE: Works, but not in desired way. The problem was in includes chain. model Battle has scope :all_inclusive , which loads all associations

scope :all_inclusive, includes(:battle_category).includes(:bets).includes(:votes).includes(:left_side_video).includes(:right_side_video)

Generated SQL:

Battle Load (0.6ms)  SELECT `battles`.* FROM `battles` WHERE (battles.status = 1 AND valid_from < '2012-01-31 13:31:50' AND battles.valid_to > '2012-01-31 13:31:50') ORDER BY battles.id DESC
BattleCategory Load (0.1ms)  SELECT `battle_categories`.* FROM `battle_categories` WHERE `battle_categories`.`id` IN (1)
Bet Load (0.1ms)  SELECT `bets`.* FROM `bets` WHERE `bets`.`parent_type` = 'Battle' AND `bets`.`parent_id` IN (1, 2)
Vote Load (0.2ms)  SELECT `votes`.* FROM `votes` WHERE `votes`.`parent_type` = 'Battle' AND `votes`.`parent_id` IN (1, 2)
BattleVideo Load (0.1ms)  SELECT `battle_videos`.* FROM `battle_videos` WHERE `battle_videos`.`battle_id` IN (1, 2) AND (battle_videos.code = 'user_1')
Video Load (0.1ms)  SELECT `videos`.* FROM `videos` WHERE `videos`.`id` IN (1, 3)

NOTICE THAT VIDEOS 2 AND 4 (FOR RIGHT SIDE) - dont load. I can access only 1,3 videos

Videos id, battle_id (side)

[1 , 1 (lft)]

[2 , 1 (rgt)]

[3 , 2 (lft)]

[4 , 2 (rgt)]

When I remove .includes(:right_side_video) from :all_inclusive chain I got this sql:

Battle Load (0.6ms)  SELECT `battles`.* FROM `battles` WHERE (battles.status = 1 AND valid_from < '2012-01-31 13:39:26' AND battles.valid_to > '2012-01-31 13:39:26') ORDER BY battles.id DESC
BattleCategory Load (0.1ms)  SELECT `battle_categories`.* FROM `battle_categories` WHERE `battle_categories`.`id` IN (1)
Bet Load (0.1ms)  SELECT `bets`.* FROM `bets` WHERE `bets`.`parent_type` = 'Battle' AND `bets`.`parent_id` IN (1,2)
Vote Load (0.2ms)  SELECT `votes`.* FROM `votes` WHERE `votes`.`parent_type` = 'Battle' AND `votes`.`parent_id` IN (1,2)
BattleVideo Load (0.3ms)  SELECT `battle_videos`.* FROM `battle_videos` WHERE `battle_videos`.`battle_id` IN (1,2) AND (battle_videos.code = 'left_side')
Video Load (0.1ms)  SELECT `videos`.* FROM `videos` WHERE `videos`.`id` IN (1, 3)
Video Load (0.2ms)  SELECT `videos`.* FROM `videos` INNER JOIN `battle_videos` ON `videos`.`id` = `battle_videos`.`video_id` WHERE `battle_videos`.`battle_id` = 1 AND (battle_videos.code = 'right_side') LIMIT 1
Video Load (0.1ms)  SELECT `videos`.* FROM `videos` INNER JOIN `battle_videos` ON `videos`.`id` = `battle_videos`.`video_id` WHERE `battle_videos`.`battle_id` = 2 AND (battle_videos.code = 'right_side') LIMIT 1

Now this works fine. But IN SQL level - its not as perfect as I want. You can see, that videos 1, 3 are loaded in correct way

Video Load (0.1ms)  SELECT `videos`.* FROM `videos` WHERE `videos`.`id` IN (1, 3)

But video 2, 4 load by seperate sqls:

Video Load (0.2ms)  SELECT `videos`.* FROM `videos` INNER JOIN `battle_videos` ON `videos`.`id` = `battle_videos`.`video_id` WHERE `battle_videos`.`battle_id` = 1 AND (battle_videos.code = 'right_side') LIMIT 1
Video Load (0.1ms)  SELECT `videos`.* FROM `videos` INNER JOIN `battle_videos` ON `videos`.`id` = `battle_videos`.`video_id` WHERE `battle_videos`.`battle_id` = 2 AND (battle_videos.code = 'right_side') LIMIT 1

What I want? FINAL SQL GENERATED BY RUBY

Video Load (0.1ms)  SELECT `videos`.* FROM `videos` WHERE `videos`.`id` IN (1, 2, 3, 4)

Why don't you try with "joins" instead of "includes"?

You can create named_scope like this

    named_scope : left_side_video, {
      :select => "videos.*",
      :joins => "INNER JOIN battle_videos ON battle_videos.video_id = videos.id INNER JOIN battles on battles.id = battle_videos.battle_id", 
      :conditions=>"battle_videos.code = 'left_side'"
    }

Haven't tested this yet. But the perfect query should be more or less the similar one.

I think, you should use preload instead of includes for loading associations.

scope :all_inclusive, preload(:battle_category, :bets, :votes, :left_side_video, :right_side_video)

preload doesn't affect original query, it makes separate query for every association you specify.

It works great in rails 3.0.x. Dunno which version you use, it may differ...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM