Search code examples
ruby-on-railsrubyruby-on-rails-4ruby-on-rails-5

Explanation on the difference between Rails 4 and Rails 5 association usages in joins and where clauses


Synopsis

I am upgrading a 13 year old Rails 3 application to a Rails 7 application and ran into some hiccups with using associations in where clauses in Rails 5.

Logic

Users are going to have many companies, but we only want to look at the company that is set as the parent, or the one we're marking as "we currently care about". Not only that, but we want to ONLY return the users where this parent company that "we currently care about" IS the master_company.

User has many companies through UserCompanies
UserCompanies has a field called is_parent_company
Company has a field called is_master_company

Problematic Source Code

The entire thing revolves around one association:

has_many :parent_company, -> {where(user_companies: {is_parent_company: true})}, through: :user_companies, source: :company

Which is supposed to represent the parent_company we talked about above, and it does.

irb(main):067:0> User.first.parent_company
=> #<ActiveRecord::Associations::CollectionProxy [ #<some valid data> ] >

Though it is tempting to use this association and filter with the framework, I am interested in a fast application and reusable queries, so I need one query that can be built upon. So that takes us to the problem: Attempting to use this association inside of a where clause, such as:

User.joins(:parent_company).where(parent_company: {is_master_company: true})

ActiveRecord is looking for a table called parent_company and throws me an error about: missing FROM-clause entry for table "parent_company", which seems reasonable: after all we are joining on it and attempting to use it in the where clause like we would a table.

Working Source Code

User.joins(user_companies: :company).where(user_companies: {is_parent_company: true, companies: {is_master_company: true}})

Maybe this is how it should've been done in Rails 3 and 4 in the first place and the original code was just workaround code. What bugs me is that this is been "working" and spitting out the correct users for Rails 3 and 4, but I'm thrown this error coming into Rails 5. Did Rails 3 and 4 support this type of usage of associations inside of where clauses?

It's rather confusing and I am making this question to see if anyone can point me in the right direction for Active Record Query Interface changes from Rails 4 to Rails 5 that is responsible for throwing this error.

Thanks for anyone who takes part!


Solution

  • If the question is "what changed between v4.x and v5.x", you'd have to dig through the codebase

    https://github.com/rails/rails/blob/4-0-stable/activerecord/lib/active_record/relation/query_methods.rb

    https://github.com/rails/rails/blob/5-0-stable/activerecord/lib/active_record/relation/query_methods.rb

    But you'll wind up having to chase down minor changes in underlying methods and may still not get a clear answer until you have a total understanding of the core internals of ActiveRecord. The up-side of this approach is... you'll have a total understanding of the core internals of ActiveRecord.

    However, I think you've hit on the more important issue:

    Can I write code that is backwards-compatible so I have some confidence that it's producing the same result?

    Maybe this is how it should've been done in Rails 3 and 4 in the first place and the original code was just workaround code.

    Yeah, it seems like Rails < 4 was allowing inference for what :parent_company means, but the change in Rails 5+ indicates that was problematic.

    You could try @spickermann's suggestion of clarifying your relations to match Rails convention. Because Rails is "convention over configuration", pluralization in relationships does have importance.

    class User < ApplicationRecord
    
      has_many :user_companies
      has_many :companies, through :user_companies
    
      # a collection is returned, even if it's always a collection of one, so it should be pluralized
      has_many :parent_companies, -> {where(user_companies: {is_parent_company: true})}, through: :user_companies, source: :company
    end
    

    There's a chance this alone provides the same behavior in Rails 3, 4, and 5.