Railsy uzyskują liczbę asocjacji za pośrednictwem tabeli złączeń


To pytanie jest rozwidleniem z

Asocjacje HABTM w Railsach: zbieranie i liczenie kategorii modelu potomnego
https://coderoad.ru/30130380/
.

Dany:
>
class Category < ActiveRecord::Base
has_and_belongs_to_many :books
validates_uniqueness_of :name
endclass Book < ActiveRecord::Base
has_and_belongs_to_many :categories
endclass Store < ActiveRecord::Base
has_many :books
has_many :categories, through: :books
end


Zadanie:
>

Podając sklep, podaj liczbę książek w każdej kategorii.

Store.first.books_per_category

pożądany rezultat:
[ { name: 'mystery', count: 5 }, { name: 'fantasy', count: 6 } ]


Jednak każdy sklep może mieć ogromną liczbę książek i kategorii.

.
Próbuję utworzyć jedno zapytanie dotyczące wydajności, które pobiera tylko kolumnę z nazwą i liczbę książek dla każdej odrębnej kategorii związanej ze sklepem, bez ładowania książek do pamięci.

Próbowałem do tej pory:
>
class Store < ActiveRecord::Base # Will load each book into memory
def books_per_category
categories.eager_load(:books).map do |c|
{
name: c.name,
count: c.books.size # Using size instead of count is important since count will always query the DB
}
end
end # will query books count for each category.
def books_per_category2
categories.distinct.map do |c|
{
name: c.name,
count: c.books.count
}
end
end
end


Schemat bazy danych:
>
ActiveRecord::Schema.define(version: 20150508184514) do create_table "books", force: true do |t|
t.string "title"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "store_id"
end add_index "books", ["store_id"], name: "index_books_on_store_id" create_table "books_categories", id: false, force: true do |t|
t.integer "book_id", null: false
t.integer "category_id", null: false
end add_index "books_categories", ["book_id", "category_id"], name: "index_books_categories_on_book_id_and_category_id"
add_index "books_categories", ["category_id", "book_id"], name: "index_books_categories_on_category_id_and_book_id" create_table "categories", force: true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end create_table "stores", force: true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
end

Zaproszony:
Anonimowy użytkownik

Anonimowy użytkownik

Potwierdzenie od:

Możesz użyć łańcuchów
select
i
group
, aby zagregować liczbę książek w każdej kategorii. Twoja metoda
books_per_category
może wyglądać następująco:
def books_per_category
categories.select('categories.id, categories.name, count(books.id) as count').group('categories.id, categories.name').map do |c|
{
name: c.name,
count: c.count
}
end
end

Spowoduje to następujące zapytanie SQL:
SELECT categories.id, categories.name, count(books.id) as count 
FROM "categories"
INNER JOIN "books_categories" ON "categories"."id" = "books_categories"."category_id"
INNER JOIN "books" ON "books_categories"."book_id" = "books"."id"
WHERE "books"."store_id" = 1
GROUP BY categories.id, categories.name
Anonimowy użytkownik

Anonimowy użytkownik

Potwierdzenie od:

Będziesz chciał utworzyć metodę (lub zakres) dla obiektu Categories, więc coś w rodzaju.
Category.joins(:books).select('categories.*, COUNT(books.id) as book_count').group('categories.id')

wynikowy obiekt będzie miał teraz każdy atrybut instancji kategorii i będzie odpowiadał metodzie
book_count
, która zwraca liczbę książek z tym identyfikatorem kategorii instancji.
W szczególności pozwoli ci to pominąć wszelkie kategorie, które nie są powiązane z książkami. jeśli chcesz je uwzględnić, zapytanie powinno zostać zaktualizowane do następujących:
Category.joins('LEFT OUTER JOIN books_categories on books_categories.category_id = categories.id').select('categories.*, COUNT(books_categories.book_id) as book_count').group('categories.id')

Aby odpowiedzieć na pytania, Zaloguj się lub Zarejestruj się