Как отфильтровать ассоциацию_id для модели ActiveRecord?

В таком домене:

 class User
  has_many :posts
  has_many :topics, :through => :posts
 end
 class Post
   belongs_to :user
   belongs_to :topic
 end
 class Topic
   has_many :posts
 end

Я могу прочитать все идентификаторы темы через user.topic_ids, но я не вижу способа применить условия фильтрации к этому методу, так как он возвращает Array вместо ActiveRecord::Relation.

Проблема заключается в том, что при заданном Пользователе и существующем наборе Тем помечаются те, для которых есть сообщения пользователя. В настоящее время я делаю что-то вроде этого:

 def mark_topics_with_post(user, topics)
   # only returns the ids of the topics for which this user has a post
   topic_ids = user.topic_ids 
   topics.each {|t| t[:has_post]=topic_ids.include(t.id)}
 end 

Но это загружает все идентификаторы темы независимо от входного набора. В идеале я хотел бы сделать что-то вроде

 def mark_topics_with_post(user, topics)
   # only returns the topics where user has a post within the subset of interest
   topic_ids = user.topic_ids.where(:id=>topics.map(&:id))
   topics.each {|t| t[:has_post]=topic_ids.include(t.id)}
 end 

Но единственное, что я могу сделать конкретно, это

 def mark_topics_with_post(user, topics)
   # needlessly create Post objects only to unwrap them later
   topic_ids = user.posts.where(:topic_id=>topics.map(&:id)).select(:topic_id).map(&:topic_id)
   topics.each {|t| t[:has_post]=topic_ids.include(t.id)}
 end 

Есть ли способ лучше? Возможно ли иметь что-то вроде select_values в ассоциации или области видимости? FWIW, я на рельсах 3.0.x, но мне было бы интересно узнать и о 3.1.

Зачем я это делаю?

По сути, у меня есть страница результатов для полусложного поиска (который происходит только на основе данных темы), и я хочу пометить результаты (темы) как материалы, с которыми взаимодействовал пользователь (написал сообщение).

Так что да, есть еще один вариант, который будет выполнять объединение [Тема, сообщение], чтобы результаты выходили как отмеченные или нет из поиска, но это лишит меня возможности кэшировать запрос темы (запрос, даже без присоединиться, дороже, чем получение только идентификаторов для пользователя)

Обратите внимание, что описанные выше подходы действительно работают, просто они кажутся неоптимальными.


person riffraff    schedule 22.12.2011    source источник
comment
Может быть, вы можете описать, почему вы делаете то, что делаете. Иногда при переходе к абстракции обнаруживаются упрощения в требованиях (Snickers против бельгийского шоколада).   -  person clyfe    schedule 22.12.2011


Ответы (2)


Я думаю, что ваше второе решение является почти оптимальным (с точки зрения задействованных запросов), по крайней мере, в отношении того, которое вы хотели бы использовать.

user.topic_ids генерирует запрос:

SELECT `topics`.id FROM `topics` 
INNER JOIN `posts` ON `topics`.`id` = `posts`.`topic_id` 
WHERE `posts`.`user_id` = 1

если бы user.topic_ids.where(:id=>topics.map(&:id)) был возможен, он бы сгенерировал это:

SELECT topics.id FROM `topics` 
INNER JOIN `posts` ON `topics`.`id` = `posts`.`topic_id` 
WHERE `posts`.`user_id` = 1 AND `topics`.`id` IN (...)

это точно такой же запрос, который генерируется при выполнении: user.topics.select("topics.id").where(:id=>topics.map(&:id))

в то время как user.posts.select(:topic_id).where(:topic_id=>topics.map(&:id)) генерирует следующий запрос:

SELECT topic_id FROM `posts` 
WHERE `posts`.`user_id` = 1 AND `posts`.`topic_id` IN (...)

какой из двух более эффективен, зависит от данных в фактических таблицах и определенных индексах (и какая база данных используется).

Если список идентификаторов тем для пользователя длинный и содержит повторяющиеся темы много раз, может иметь смысл сгруппировать по идентификатору темы на уровне запроса:

user.posts.select(:topic_id).group(:topic_id).where(:topic_id=>topics.map(&:id))
person LucaM    schedule 22.12.2011
comment
Интересно, я не считал, что соединение необходимо, что имеет смысл, если нет ссылочной целостности и дублирующихся значений. Проблема с решением posts/select/map заключается в том, что оно создает намного больше объектов в рубиновой стране (по крайней мере, один объект с 8 полями, 4 хэшами, двумя строками) только для того, чтобы получить мне Fixnum, плюс бесполезный цикл, когда они не нужны. Это, очевидно, не имеет большого значения, но это просто кажется неправильным. - person riffraff; 22.12.2011
comment
да, вы правы, и это кажется неправильным, моя точка зрения заключалась в том, что сам метод topic_ids точно следует маршруту темы/выбор/карта. Единственный (уродливый) способ избежать создания экземпляров активных записей, о котором я могу думать, заключается в непосредственном выполнении SQL-запроса... - person LucaM; 22.12.2011

Предположим, что в вашей модели Topic есть столбец с именем id, вы можете сделать что-то вроде этого

Topic.select(:id).join(:posts).where("posts.user_id = ?", user_id)

Это выполнит только один запрос к вашей базе данных и предоставит вам все идентификаторы тем, в которых есть сообщения для данного user_id.

person cristian    schedule 22.12.2011
comment
но это не то, что мне нужно: у меня уже есть список объектов Topic, я хочу отметить только те, у которых есть общая запись отношения с пользователем. - person riffraff; 22.12.2011