Scope for an optional has_one association with its

2019-08-02 13:48发布

问题:

I have a model Indent. On which I am using STI. An indent can be of two types Sale and Purchase. In purchase class I am using an optional has_one association.

class Purchase < Indent
    has_one :sale , :class_name => 'Sale', :foreign_key => 'linked_indent_id'
    # Make it work.  
    scope :unsold, lambda {includes(:sale).where('id not in (select distinct linked_indent_id from indents)')}
end

class Sale < Indent
   belongs_to :purchase , :class_name => 'Purchase', :foreign_key => 'linked_indent_id'
end

I just need a scope on Purchase class using which i can find all the purchases which are not having a Sale associated to them.

I am using Rails 3.2 and Postgres as database.

Update:

The query which is getting generated is as follows.

 SELECT "indents".* FROM "indents" WHERE "indents"."type" IN ('Purchase') AND 
 (id not in (select distinct linked_indent_id from indents)) ORDER BY indent_date DESC

Following part of the query is working fine.

=# select distinct linked_indent_id from indents;

 linked_indent_id 
 ------------------

        15013
        15019
       (3 rows)

And this is also working fine.

SELECT "indents".* FROM "indents" WHERE "indents"."type" IN ('Purchase') AND
(id not in (15013, 15019)) ORDER BY indent_date DESC

What am i missing in coupling these two parts of query?

回答1:

I first got confused by the terms purchase and sale. But your update I believe helped me understand the problem more.

So what I understood is anything unsold is purchases minus sales. Following should give you that list:

scope :unsold, lambda {includes(:sale).select { |p| !p.sale.present? } }

Update:

A brief explanation of what's happening here:

The scope does not really do all the work in the database. It does a SQL select of all the purchases including the joined sale first. This gives you all the records in your purchases table. Then this scope falls back to Ruby Array on the select method. The method returns all the purchases p without a sale which is done by negating purchases with sale.

Hope this clears up on what the scope is doing a bit.

Update 2:

A scope that's chainable!

scope :unsold, lambda { where('id not in (?)', Sale.pluck(:linked_indent_id)) }

In this scope the ids of Purchases that are not in Sale's linked_indent_id are selected.



回答2:

Rails-ey database-centric way to do this:

scope :sold, -> { joins(:sale) }
scope :unsold, -> { includes(:sale).where(sales: {linked_indent_id: nil}) }

(Note that you have to use the table name in the where clause, not the relation name. That is 'sales', not 'sale'.)