简体   繁体   中英

how do I do a join with a polymorphic has_many 'through'?


I have the following data structure (simplified), which includes a polymorphic association in the purchases table (MySql)

Purchases
  product_type (this refers to a table, book, dvd, shirt,...)
  product_id (this refers to the product's id in the table in product_type)
  amount

Books
  category_id

DVDs
  category_id

Shirts
  category_id

Categories
  name

I would like to select categories from the db, ordered by total sales (purchases.amount). How can I do this with joins and aggregate functions?

I made a sqlfiddle for this:

http://sqlfiddle.com/#!2/74705

Polymorphic IDs are similar to inheritance structures in a DB, in that to query across all of them often you have to use a union.

However, if your Books/Shirts/DVDs tables have no common columns, it makes absolutely no sense to query across them. If they did have common columns, probably makes more sense to pull that out into a parent Product table that acts as a base for the others.

If you for example wanted total sales per product, you'd just do

Select sum(amount), product_id -- or product_type if you wanted sales per type
From Purchases
Group By product_id -- product_type

You could pull in product titles like this:

Select sum(p.amount), p.product_id, b.Title
From Purchases p 
Inner Join Books b on b.category_id = p.product_id 
Group By product_id
    Union
Select sum(p.amount), p.product_id, s.Title    
From Purchases p
Inner Join Shirts s on s.category_id = p.product_id 
Group By product_id

It might be more performant to do the union first, then the group by outside of that.

My experience with inheritance hierarchies is that you should do whatever you can to avoid unions. They are a pain to query, result in non-DRY queries, and perform badly. This means pulling out commonalities such as Title into a proper normalized structure to avoid having to query across the concrete/derived types. This is essentially what Wrikken mention in comments "Usually, a better way for this would be a Products table with the shared information types"

Here's an example of how you could achieve this using temporary tables . The principle is that you create a "temporary" table and populate it with the data that you want, in the structure that you want, when your query is run. You can modify the data, and return it at the end and the table is then destroyed when the connection closes.

Anyway - you can set up your temporary table like with the schema combining a new unique, product id, product type, product name and category id. You then populate the table with a union query between all the records in your books, dvds and shirts tables to suit that format:

-- delete the temp table in case it's still there
DROP TEMPORARY TABLE IF EXISTS all_products;

-- create your new temp table
CREATE TEMPORARY TABLE IF NOT EXISTS all_products (
    new_id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    product_id INT(11) NOT NULL,
    product_type VARCHAR(50) NOT NULL,
    product_name VARCHAR(128) NOT NULL,
    category_id INT(11) NOT NULL
) AS (
    -- populate it with unioned queries
    SELECT * FROM (
        (SELECT NULL AS new_id, id AS product_id, 'book' AS product_type, `name` AS product_name, category_id FROM books)
        UNION ALL
        (SELECT NULL AS new_id, id AS product_id, 'dvd' AS product_type, `name` AS product_name, category_id FROM dvds)
        UNION ALL
        (SELECT NULL AS new_id, id AS product_id, 'shirt' AS product_type, `name` AS product_name, category_id FROM shirts)
    ) AS temp -- an alias is required - doesn't matter what it is
);

Now you can access the data in this table as you normally would. The benefit of using temporary tables is that you don't have to change your database structure - in your case it's maybe not ideal to have it set up the way it is, but doing something like this could help you to select data efficiently without changing it.

To select data, you could use a query like this:

SELECT
    purchase.*,
    product.product_name,
    category.name
FROM purchases AS purchase
-- get your product info from the temp table
INNER JOIN all_products AS product
    ON (product.product_id = purchase.product_id AND product.product_type = purchase.product_type)
-- get the category name
INNER JOIN categories AS category
    ON (category.id = product.category_id)

You said you want to order your results by amount, etc. Add your conditions and ordering to the query above as you want to.

Example output:

id  product_id  product_type  amount  product_name  name         
1   1           book          10      how to        educational  
2   1           dvd           10      video how to  educational  
3   1           shirt         10      tshirt        clothes      

These can actually be very efficient to run, for example running this on my local server is executing in 0.001s - it will scale very nicely. I would've set you up a fiddle for this, but SQLFiddle doesn't seem to support temporary tables very well. Just run the CREATE and SELECT statements at the same time on your local server to see.

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