Rails - 如何加入/分组嵌套表并获取连接值?

时间:2013-06-10 22:57:14

标签: ruby-on-rails ruby-on-rails-3

我有以下模型层次结构,其中每个模型都具有下面的模型:

class AccountGroup < ActiveRecord::Base
  has_many :accounts, :inverse_of=>:account_group
  # name: string

class Account < ActiveRecord::Base
  belongs_to :accountGroup, :inverse_of=>:account
  has_many :positions, :inverse_of=>:account

class Position < ActiveRecord::Base
  belongs_to :account, :inverse_of=>:positions
  # net_position: integer

换句话说,一个AccountGroup包含一堆帐户,一个帐户包含一堆职位。

目标:我想要一个AccountGroup => (sum of its net_positions)的哈希值。这意味着涉及到GROUP BY。

我可以使用原始SQL执行此操作,但我还没有使用Rails函数破解它。原始SQL是:

SELECT account_groups.id,SUM(net_position),account_groups.name
FROM account_groups
LEFT JOIN accounts ON accounts.account_group_id = account_groups.id
LEFT JOIN positions ON positions.account_id = accounts.id
GROUP BY account_groups.id,account_groups.name;

这是Rails不能做的事情吗?

6 个答案:

答案 0 :(得分:3)

Rails(4.0.0)可以执行此操作 - 我们目前有两种方法可以执行此操作:


<强> 1。 SQL“别名”列

Rails Scoping For has_many :through To Access Extra Data

#Images
has_many :image_messages, :class_name => 'ImageMessage'
has_many :images, -> { select("#{Image.table_name}.*, #{ImageMessage.table_name}.caption AS caption") }, :class_name => 'Image', :through => :image_messages, dependent: :destroy

<强> 2。 ActiveRecord Association Extensions

这是Rails的一个鲜为人知的功能,它允许您使用collection对象。它的做法是扩展你创建的has_many关系:

class AccountGroup < ActiveRecord::Base
  has_many :accounts do
      def X
          #your code here
      end
  end
end

我们只有这个方法适用于集合,但你可以用它做各种各样的事情。您应该查看this tutorial以了解有关它的更多信息


<强>更新

我们通过使用扩展模块来实现这一点:

#app/models/message.rb
Class Message < ActiveRecord::Base
    has_many :image_messages #-> join model
    has_many :images, through: :image_messages, extend: ImageCaption
end

#app/models/concerns/image_caption.rb
module ImageCaption

    #Load
    def load
      captions.each do |caption|
        proxy_association.target << caption
      end
    end

    #Private
    private

    #Captions
    def captions
        return_array = []
        through_collection.each_with_index do |through,i|
            associate = through.send(reflection_name)
            associate.assign_attributes({caption: items[i]})
            return_array.concat Array.new(1).fill( associate )
        end
        return return_array
    end

    #######################
    #      Variables      #
    #######################

    #Association
    def reflection_name
        proxy_association.source_reflection.name
    end

    #Foreign Key
    def through_source_key
        proxy_association.reflection.source_reflection.foreign_key
    end

    #Primary Key
    def through_primary_key
        proxy_association.reflection.through_reflection.active_record_primary_key
    end

    #Through Name
    def through_name
        proxy_association.reflection.through_reflection.name
    end

    #Through
    def through_collection
        proxy_association.owner.send through_name
    end

    #Captions
    def items
        through_collection.map(&:caption)
    end

    #Target
    def target_collection
        #load_target
        proxy_association.target
    end

end

为变量函数

道具this gist

这基本上覆盖了load类中的CollectionProxy ActiveRecord函数,并使用它来创建我们自己的proxy_association.target数组:)

如果您需要有关如何实施的任何信息,请在评论中提问

答案 1 :(得分:1)

使用rails AR查询方法可以使这个比原始sql更漂亮:

AccountGroup.
  select("account_groups.id, SUM(net_position), account_groups.name").
  joins("LEFT JOIN accounts ON accounts.account_group_id = account_groups.id").
  joins("LEFT JOIN positions ON positions.account_id = accounts.id").
  group("account_groups.id,account_groups.name")

答案 2 :(得分:1)

这也可以用纯Arel来完成。

AccountGroup.select(
  AccountGroup.arel_table[:id], Arel::Nodes::NamedFunction.new('SUM', [:net_position]), AccountGroup.arel_table[:name]
).joins(
  AccountGroup.arel_table.join(Account.arel_table).on(
    Account.arel_table[:account_group_id].eq(AccountGroup.arel_table[:id])
  ).join_sources
).joins(
  AccountGroup.arel_table.join(Position.arel_table).on(
    Position.arel_table[:account_id].eq(Account.arel_table[:id])
  ).join_sources
).group(
  AccountGroup.arel_table[:id], AccountGroup.arel_table[:name]
)

我不是100%确定这会有效,我只是从上面复制你的SQL并将其放入scuttle.io

答案 3 :(得分:0)

使用包含功能,例如

ac = AccountGroup.all(:include => :account)
$ AccountGroup Load (0.6ms)  SELECT `account_groups`.* FROM `groups` 
$ Account Load (16.4ms)  SELECT `accounts`.* FROM `accounts` WHERE `accounts`.`id` IN (1010, 3, 4, 202, 203, 204, 9999)

然后你可以调用ac.account.name或类似的东西

有一个很棒的Railscast http://railscasts.com/episodes/22-eager-loading?view=asciicast

答案 4 :(得分:0)

如果你真的想使用ActiveRecord(没有SQL),它将是这样的:

ags = AccountGroup.all(:include => {:accounts => :positions})
hash = Hash[ags.map { |ag| [ag, ag.map(&:accounts).flatten.map(&:positions).flatten.map(&:net_position).reduce(0,&:+)]}]

但它会慢于你的SQL,而不是更漂亮。

答案 5 :(得分:0)

  

这是Rails不能做的事情吗?

由于这个问题已经开放了大约一个月,我将继续并假设这个问题的答案是......

编辑:是, for Rails 3 但是Rails 4 可以做到这一点!请参阅接受的答案。

除了使用find_by_sqlActiveRecord::Base.connection.execute(query)之外,Rails无法做到这一点,这些非常糟糕而且不是rails-y。