简体   繁体   中英

Rails order a has_many relation using a through table

I have a User and a Campaign model in my rails app. A campaign has_many users and a user has_one campaign.

I want to order the users in the campaign by the date that they were added to it.

To do that, I created a through table called CampaignUser. I thought that I'd be able to order by the created_at column in that table, but I couldn't see an easy way to do it. See the classes below:

class Campaign < ApplicationRecord
  has_many :campaign_users
  has_many :users, through: :campaign_users
end

class User < ApplicationRecord
  has_one :campaign, through: :campaign_users, dependent: :destroy
end

class CampaignUser < ApplicationRecord
  belongs_to :campaign
  belongs_to :user
end

Ideally, I'd like to write a line like this in my Campaign class:

has_many :users, through: campaign_users, -> { order(created_at: :desc) }

Where created_at refers to campaign_users and not to users . Is there a way to do that?

I could just write a method on Campaign myself to order the users manually, but then I'd have to make sure I call that method everywhere instead. It seems like there should be an easier way.

Adding a scope to the user, as suggested in other answers is more problematic in this case. I'm looking to order users by a property of the through table, not a property of the user itself. Is there a way to write the following line, replacing email with campaign_users.created_at , or something similar?

has_many :users, -> { order(email: :desc) }, :through => :campaign_users 

EDIT : Thanks to @AdColvin I changed the code block to make it work ;)

Have you tried something like

has_many :users, -> { order('campaign_users.created_at DESC') }, through: campaign_users

You can do that because ActiveRecord will generate a JOIN in the resulting SQL, then you can order on any table that is joined.

Also, the campaign_users in the order statement should be the name of the table, not the name of the model or the relation

The trick is as @kevcha has already pointed out calling order with a string of the column you want.

But instead of adding the order clause directly to the association you may want to use a association extension :

class Campaign < ApplicationRecord
  has_many :campaign_users
  has_many :users, through: :campaign_users do
    def order_by_join_date
      order('campaign_users.created_at DESC')
    end
  end
end

This lets you call campaign.users.order_by_join_date to explicitly get the records in a specific order. It avoids some of the same pitfalls that surround default scope .

@kevcha When I tried your answer exactly as you suggested, I got the following error:

syntax error, unexpected '\\n', expecting => ...mpaign_users.created_at ASC') }

But, when I add the scope just after has_many :users , it works fine:

has_many :users, -> { order('campaign_users.created_at DESC') }, through: :campaign_users

Also worth noting is that created_at seems to be identical for objects created from a fixture. I wasn't aware of that. I had to explicitly set created_at in my fixtures for my tests around this to pass.

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