2013-03-20 1 views
1

У меня есть Question & Tag модель. Я хотел бы обновить теги по существующему вопросу с помощью коллекции тегов в другой коллекции.Как обновить коллекцию предметов с помощью другого набора элементов?

Это метод на моей Question модели:

def self.update_tags(tag_list) 
    tags.each do |t| 

    end 
    end 

Я знаю, что могу сделать каждый цикл в пределах каждого цикла, но это не похоже, лучший подход (или даже самый DRY/Рубин-эск).

В основном, я пытаюсь обновить теги на вопрос, если они не существуют. Итак, теоретически, я хочу проверить каждый объект в tag_list, чтобы узнать, существует ли он в question.tags. Если это не так, я хочу нажать. Если да, то проигнорируйте его и перейдите к следующему.

Каков наиболее эффективный подход?

Edit 1

У меня есть HABTM связь между двумя моделями Question и Tag.

Edit 2

Я знаю, что это классический N + 1 проблема запроса, так что я пытаюсь выяснить, лучший способ сделать это наиболее эффективным способом.

Редактировать 3

Вот объяснение того, что происходит, и результаты, которые я пытаюсь достичь - в эффективный способ.

tag_list строится так:

tags.each do |tag| 
    tag_list << Tag.where(:name => tag.name).first_or_create(:num_questions => tag.count) 
end 

tags представляет собой совокупность объектов, возвращаемых из предыдущего вызова внешнего API.

Мне нужно пройти весь существующий question.tags текущего вопроса и проверить его на идентификаторы объектов AR в tag_list.

Say вопрос ранее был tag_ids из [5, 7, 8, 10] ... чего я хочу, так это теперь с tag_list = [5, 6, 7, 8, 9], я хочу, чтобы обновить question.tag_ids = [5, 6, 7, 8, 9].

Итак, это удалит tag_id=10 и добавит tag_id=[6, 9].

Это то, что я пытаюсь сделать.

+0

его проблему п + 1 запросов, вы можете использовать включает в себя в зависимости от вашего отношения –

+0

Я знаю о проблеме п + 1 запроса - именно поэтому я прошу наилучшим образом подойти к нему :) – marcamillion

ответ

3

Rails предоставляет Native API для этого под названием replace ..

blog.tags.replace(tag_list) 

Старый ответ

Я бы придерживался логики просто. Внутри рельсы сохраняют записи ассоциации в one transaction.Производительность этого и ручного многопозиционного оператора должна быть сопоставимой. Кроме того, использование слоя rails изолирует вас от тонкостей работы с новым или сохраненным родительским объектом.

def self.update_tags(tag_list) 
    # Add new tags 
    current_tags = self.tags.dup 
    new_tags = tag_list - current_tags 
    tags.concat(new_tags) if new_tags.present? 

    # Remove defunct tags 
    old_tags = current_tags - tag_list 
    tags.delete(old_tags) if old_tags.present?  
end 
+0

Мне очень нравится это на первый взгляд .... но будет 'concat' также удалять элементы в' self.tags', которых нет в 'tag_list', или просто добавит теги, которых не было? Я хочу обновить 'self.tags'. Другими словами, если в этом вопросе были удалены теги, я хочу, чтобы они были удалены - это конечный результат, которого я пытаюсь достичь. – marcamillion

+0

Это идеальный .... работает и ОЧЕНЬ элегантный и простой. Огромное спасибо. Это ТОЧНО, чего я пытался достичь! – marcamillion

+0

'concat' добавляет новые теги. Поскольку мы вычисляем дельта на предыдущем шаге, вы должны видеть только уникальные теги. –

0

Может быть вам нужно accepts_nested_attributes_for (docs)

+0

Не уверен, если я могу сделать 'accepts_nested_attributes_for' с HABTM с обеих сторон? Не см. Это в документах. – marcamillion

0

(Примечание: Я несколько обновлений, вы, вероятно, наиболее заинтересованы в коде, представленной в UPDATE 2 или UPDATE 3.)

Я думаю, вы можете поместить следующее в вашем вопросе модели:

def diff_tags(other_q) 
    other_q.tags - tags 
end 
def add_tags(other_q) 
    tags << diff_tags(other_q) 
end 

Затем делаем следующее:

q1 = Question.find(1) 
q2 = Question.find(2) 
q1.add_tags(q2) 

приводит к (Postgres в моем случае):

SELECT "tags".* FROM "tags" INNER JOIN "questions_tags" ON "tags"."id" = "questions_tags"."tag_id" WHERE "questions_tags"."question_id" = ? [["question_id", 2]] 
SELECT "tags".* FROM "tags" INNER JOIN "questions_tags" ON "tags"."id" = "questions_tags"."tag_id" WHERE "questions_tags"."question_id" = ? [["question_id", 1]] 
begin transaction 
INSERT INTO "questions_tags" ("question_id", "tag_id") VALUES (1, <missing tag id 1>) 
INSERT INTO "questions_tags" ("question_id", "tag_id") VALUES (1, <missing tag id 2>) 
... and all other missing tags ... 
commit transaction 

Вы можете продолжить работу над запросами по адресу:

1) выбрать только идентификаторы тегов в первых 2-х запросов, не инстанцирует целые теги объектов

2) ВСТАВИТЬ несколько значений в одном операторе SQL, как INSERT INTO "questions_tags" ("question_id", "tag_id") VALUES (<question_id>, <id1>), (<question_id>, <id2>), но вы, вероятно, необходимо с помощью сырой SQL для этого.

UPDATE: а вот оптимизированная версия:

def diff_tags_ids(other_q) 
    (other_q.tags.select(:id) - tags.select(:id)).map(&:id) 
end 
def add_tags_ids(tag_ids) 
    query_head = 'INSERT INTO "questions_tags" ("question_id", "tag_id") VALUES ' 
    query_values = [] 
    tag_ids.each do |tag_id| 
    query_values << "(#{self.id},#{tag_id})" 
    end 
    query = query_head + query_values.join(", ") 
    ActiveRecord::Base.connection.execute(query) 
end 
def add_tags_from(other_q) 
    add_tags_ids(diff_tags_ids(other_q)) 
end 

Теперь следующий

q1 = Question.find(1) 
q2 = Question.find(2) 
q1.add_tags_from(q2) 

приводит к всего 3 запросов:

SELECT id FROM "tags" INNER JOIN "questions_tags" ON "tags"."id" = "questions_tags"."tag_id" WHERE "questions_tags"."question_id" = ? [["question_id", 3]] 
SELECT id FROM "tags" INNER JOIN "questions_tags" ON "tags"."id" = "questions_tags"."tag_id" WHERE "questions_tags"."question_id" = ? [["question_id", 1]] 
INSERT INTO "questions_tags" ("question_id", "tag_id") VALUES (1,5), (1,6) # or whatever values are missing in question 1 compared to question 2 

UPDATE 2: просто понял, что ты не нужно читать теги из 2-го вопроса, вы уже имеете их в tag_list. Ну, это еще проще, то:

def diff_tags_ids(tag_list) 
    (tag_list - tags.select(:id)).map(&:id) 
end 
def add_tags_ids(tag_ids) 
    query_head = 'INSERT INTO "questions_tags" ("question_id", "tag_id") VALUES ' 
    query_values = [] 
    tag_ids.each do |tag_id| 
    query_values << "(#{self.id},#{tag_id})" 
    end 
    query = query_head + query_values.join(", ") 
    ActiveRecord::Base.connection.execute(query) 
end 
def update_tags(tag_list) 
    add_tags_ids(diff_tags_ids(tag_list)) 
end 

Это один я не пробовал на фактическое приложение, так что извините, если есть некоторые небольшие опечатки.

UPDATE 3: и если у вас есть тег имена, не помечать объекты в вашем tag_list, то вот обновление (если у вас есть name атрибут в модели Tag:

def diff_tags_names(tag_list) 
    tag_list - tags.select(:name).map(&:name) 
end 
def find_tags_ids_by_names(tag_list) 
    Tag.where(:name => tag_list).select(:id).map(&:id) 
    # That leads to SELECT "tags"."id" FROM "tags" WHERE "tags"."name" IN ('tag1', 'tag2', ...) 
end 
def add_tags_ids(tag_ids) 
    query_head = 'INSERT INTO "questions_tags" ("question_id", "tag_id") VALUES ' 
    query_values = [] 
    tag_ids.each do |tag_id| 
    query_values << "(#{self.id},#{tag_id})" 
    end 
    query = query_head + query_values.join(", ") 
    ActiveRecord::Base.connection.execute(query) 
end 
def update_tags(tag_list) 
    tags_ids_to_add = find_tags_ids_by_names(diff_tags_names(tag_list)) 
    add_tags_ids(tags_ids_to_add) 
end 

Еще только два запроса ...

+0

Hrmm .... проблема с этим подходом заключается в том, что он ориентирован только на создание новых записей в таблице соединений 'questions_tags' .... что не является поведением, которое я ищу. Так что происходит, я обновляю вопросы в своей БД из внешнего API. Одним из таких обновлений может быть изменение существующих 'question.tags' текущего вопроса. Итак, что я делаю, я проверяю новые теги (и искал локальный 'tag_id'). Скажем, что ранее вопрос имел «tag_ids» из '[5, 7, 8, 10]' ... то, что я хочу сделать, теперь с 'tag_list = [5, 6, 7, 8, 9]', я хочу update 'question.tag_ids = [5, 6, 7, 8, 9]'. – marcamillion

+0

Итак, это приведет к удалению 'tag_id = 10' и добавит' tag_id = [6, 9] '. – marcamillion

+0

Я обновил вопрос, чтобы показать более подробную информацию о том, что происходит, и результат, который я ищу, - основываясь на моих предыдущих комментариях здесь. – marcamillion

0

Вы можете проверить, присутствует ли тэг внутри вопроса:

@question.tags.where(:id => tag_id).present? #check if the tag_id is inside the question. 

, но глядя на ваши потребности это:

def tag_names 
    # Get all related Tags as comma-separated list 
    tag_list = [] 
    tags.each do |tag| 
    tag_list << tag.name 
    end 
    tag_list.join(', ') 
end 

def tag_names=(names) 
    # Delete tag-relations 
    self.tags.delete_all 

    # Split comma-separated list 
    names = names.split(', ') 

    # Run through each tag 
    names.each do |name| 
    tag = Tag.find_by_name(name) 

    if tag 
     # If the tag already exists, create only join-model 
     self.tags << tag 
    else 
     # New tag, save it and create join-model 
     tag = self.tags.new(:name => name) 
     if tag.save 
     self.tags << tag 
     end 
    end 
    end 
end 

код пикап здесь: Rails HABTM fields_for – check if record with same name already exists

+0

Мне не нужен список тегов, разделенных запятыми. У меня уже есть 'updated_tags' в коллекции под названием' tag_list << Tag.where (: name => tag.name) .first_or_create (: num_questions => tag.count) '. Итак, теперь 'tag_list' представляет собой коллекцию записей AR. Мне нравится ваше предложение о предложении 'where', которое поможет мне определить, находятся ли какие-либо элементы в' tag_list' в 'question.tags'. Проблема в том, как я обрабатываю элементы в 'tag_list', которые не существуют, и наоборот. Я собираюсь обновить вопрос конкретным примером того, чего я пытаюсь достичь. – marcamillion

+0

Я обновил вопрос. – marcamillion

Смежные вопросы