77

Can someone tell me what is the equivalent way to do the following line in Rails 4?

has_many :friends, :through => :friendships, :conditions => "status = 'accepted'", :order => :first_name

I tried the following:

has_many :friends, -> { where status: 'accepted' }, :through => :friendships , :order => :first_name

But I get the following error:

Invalid mix of scope block and deprecated finder options on ActiveRecord association: User.has_many :friends
Mohamad
  • 34,731
  • 32
  • 140
  • 219
medBouzid
  • 7,484
  • 10
  • 56
  • 86

5 Answers5

127

Needs to be the second arg:

class Customer < ActiveRecord::Base
  has_many :orders, -> { where processed: true }
end

http://edgeguides.rubyonrails.org/association_basics.html#scopes-for-has-many

RESPONSE TO UPDATE:

Put the order inside the block:

has_many :friends, -> { where(friendship: {status: 'accepted'}).order('first_name DESC') }, :through => :friendships
Kaleidoscope
  • 3,609
  • 1
  • 26
  • 21
  • after replacing it in the second arg, i get this error : Invalid mix of scope block and deprecated finder options on ActiveRecord association: User.has_many :friends – medBouzid Dec 01 '13 at 02:03
  • Could you update your question with what you tried? Looks like you don't have enough room in your comment. – Kaleidoscope Dec 01 '13 at 02:05
  • i used this -> { where status: 'requested' , :order => :created_at} and it works for me – medBouzid Dec 01 '13 at 02:12
  • thank you for help, please add my solution as second suggestion ^^ – medBouzid Dec 01 '13 at 02:15
  • @medBo I'm not sure your solution is doing what you think it is. I think it ends up evaluating to `where(status: 'requested', order: :created_at)`, instead of actually applying an order to the relation. You could verify that by checking the generated SQL (`a_customer.friends.to_sql`) – Kaleidoscope Dec 01 '13 at 02:19
  • i get error :( SELECT "users".* FROM "users" INNER JOIN "friendships" ON "users"."id" = "friendships"."friend_id" WHERE "users"."status" = 'accepted' AND "users"."order" = 'first_name' AND "friendships"."user_id" = $1 [["user_id", 1]] PG::Error: ERROR: column users.status does not exist LINE 1: ...ON "users"."id" = "friendships"."friend_id" WHERE "users"."s... – medBouzid Dec 01 '13 at 02:28
  • it seem that scope is searching status attribute in the user model – medBouzid Dec 01 '13 at 02:29
  • You don't have a column in your `users` table called `status`. You can verify this by looking inside `db/schema.rb`. Are you trying to search against a method you defined called status? Or maybe you forgot to run a migration? – Kaleidoscope Dec 01 '13 at 02:32
  • Err, we probably should move this discussion into a separate question. – Kaleidoscope Dec 01 '13 at 02:33
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/42261/discussion-between-medbo-and-kaleidoscope) – medBouzid Dec 01 '13 at 02:36
74

While other answers on here are technically correct, they violate encapsulation. The User model should not know that the Friendship model has a column called status, and that it can have a specific value like accepted.

If you decide to make a change, to take advantage of Enums in Rails 4, for example, you would have to change both User and Friendship models. This could lead to bugs that maintaining encapsulation avoids.

I would expose a scope in the Friendship model:

scope :accepted, -> { where(status: :accepted) }

I would then use this scope in the User model, hiding any implementation details from User.

has_many :friendships, -> { Friendship.accepted }
has_many :friends, through: :friendships

# Or...

has_many :friends, -> { Friendship.accepted }, through: :friendships

You can go further and rename the scope to accepted_friendships to be clearer.

has_many :accepted_friendships, -> { Friendship.accepted }
has_many :friends, through: :accepted_friendships

Now you have successfully encapsulated implementation details in their respective models. Should anything change you only have one place to change it, reducing maintenance and increasing robustness.

Mohamad
  • 34,731
  • 32
  • 140
  • 219
  • 1
    your solution look cleaner, I will try it later on. +1 – medBouzid Apr 01 '15 at 12:07
  • I am using the same type of code, but getting `syntax error, unexpected '\n', expecting =>` with this line: `has_many :friends, :through => :friendships, class_name: "User", -> { Friendship.accepted }` – Riptyde4 Feb 23 '16 at 06:11
  • I'm using Rails 4.2.5, it did not accept the syntax until i put the where before the rest of the arguments – Riptyde4 Feb 23 '16 at 17:31
  • @Riptyde4 sorry, I didn't read your earlier comment properly: The proc (`->{ Friendship.accepted }`) has to be the second argument, as my answer shows. So yes, it will be a syntax error if you don't have it in that order. My answer shows it in the correct order: `has_many :friends, -> { Friendship.accepted }, through: :friendships` – Mohamad Feb 23 '16 at 17:58
  • @Mohamad Sorry for the confusion, I didn't realize order mattered when I was trying it for myself. Your answer was very helpful to me – Riptyde4 Feb 23 '16 at 18:29
  • @Riptyde4 no worries. I'm glad you found it helpful. Cheers! – Mohamad Feb 23 '16 at 18:32
5

A Rails 3.2 version of Mohamad's answer would be the following:

class Friend < ActiveRecord::Base
  has_many :friendships, :order => :first_name

  has_many :friends, :through => :friendships,
           :conditions => proc { Friendship.accepted.where_ast }

  has_many :pending_friends, :through => :friendships,
           class_name => Friend,
           :conditions => proc { Friendship.pending.where_ast }
end

class Friendship < ActiveRecord::Base
  scope :status, ->(status) { where(:status => status) }
  scope :accepted, -> { status('accepted') }
  scope :pending, -> { where(arel_table[:status].not_eq('accepted')) } 
end

NOTES:

  • where_ast is important as it returns the AREL nodes that are required for the condition to work
  • within the proc passed to :conditions, self is not always a model instance (e.g. when the association is merged with another query)
  • Using raw SQL within your scopes and associations will likely cause issues at some point to do with namespacing of table names... use AREL.
br3nt
  • 9,017
  • 3
  • 42
  • 63
3

In order to work on Rails 4.1 (my case), i had to put:

has_many :friends, -> { where(friendships: { status: 'accepted' }) }, through: :friendships

Note the S on friendships. It refers directly to the database name.

Oswaldo Ferreira
  • 1,339
  • 16
  • 15
0
has_many :friends, -> { where(status: 'accepted').order('first_name')}, through: :friendships

or

has_many :friends, -> { where(status: 'accepted').order(:first_name)}, through: :friendships
DickieBoy
  • 4,886
  • 1
  • 28
  • 47
Amrit Dhungana
  • 4,371
  • 5
  • 31
  • 36