2015-04-27 2 views
0

Я привязываю для запуска запроса на обновление с подзапросом в базе данных MySQL с использованием ruby. Я использую ruby ​​1.9.3 и рельсы 4.1.Как написать запрос AREL UpdateManager для MySQL, который использует подзапрос

Запрос Я пытаюсь создать, как показано ниже:

UPDATE `items` 
SET 
    `items`.`status_id` = 12 
WHERE 
    `items`.`id` IN (SELECT DISTINCT 
      `items`.`id` 
     FROM 
      `items` 
       LEFT OUTER JOIN 
      `statuses` ON `items`.`status_id` = `statuses`.`id` 
       LEFT OUTER JOIN 
      `resources` ON `items`.`resource_id` = `resources`.`id` 
     WHERE 
      `statuses`.`title` LIKE 'On Loan' 
       AND `items`.`duedate` < '2015-04-24' 
       AND `items`.`return_date` IS NULL 
     ORDER BY `items`.`duedate`) 

я могу производить этот запрос в рубин, используя AREL с кодом ниже:

# Declare Arel objects 
i = Item.arel_table 
s = Status.arel_table 
r = Resource.arel_table 

# This is the AREL query that returns the data 
overdues = i.project(i[:id]). 
join(s, Arel::Nodes::OuterJoin).on(i[:status_id].eq(s[:id])). 
join(r, Arel::Nodes::OuterJoin).on(i[:resource_id].eq(r[:id])). 
where(s[:title].matches("On Loan"). 
    and(i[:duedate].lt(DateTime.now.to_date)). 
    and(i[:return_date].eq(nil)) 
). 
order(i[:duedate]) 

# Note: You can't chain distinct, otherwise "overdues" becomes a string with the value "DISTINCT". 
overdues.distinct 

# This creates the update... 
u = Arel::UpdateManager.new i.engine 
u.table(i) 
u.set([[i[:status_id], 10]]).where(i[:id].in(overdues)) 

Это не работает и возвращает сообщение об ошибке:

ActiveRecord :: StatementInvalid: Mysql2 :: Ошибка: вы не можете указать элементы таблицы назначения для обновления в предложении FROM:

Я попытался использовать AR "update_all", но он производит тот же SQL и, следовательно, ту же ошибку.

Item.where(i[:id].in(overdues)).update_all(:status_id => (Status.find_by(:title => "Overdue").id)) 

Сделав некоторые исследования, я обнаружил, что вы не можете запустить обновление с подзапроса, который ссылается на таблицу, которую необходимо обновить в MySQL. Я видел несколько сообщений на этом сайте и в более широком Интернете, где подробно разбираются детали.

В одном из утверждений говорится, что обновление должно использовать соединение вместо подзапроса. Посмотрев на код, стоящий за менеджером обновлений, он не имеет «соединения», поэтому я не могу этого сделать.

Другой говорит, что запускайте это в двух частях, но я не вижу, как это происходит, потому что AREL и AciveRecord выполняют целые действия.

Единственный способ, которым я могу это сделать, - наложение на таблицу и добавление дополнительного выбора (см. Ниже). Это не здорово, но было бы полезно посмотреть, можно ли это сделать.

UPDATE `items` 
SET `status_id` = 10 
WHERE `items`.`id` IN (
    SELECT x.id 
    FROM 
    (SELECT DISTINCT `items`.`id` 
     FROM `items` 
     LEFT OUTER JOIN `statuses` ON `items`.`status_id` = `statuses`.`id` 
     LEFT OUTER JOIN `resources` ON `items`.`resource_id` = `resources`.`id` 
     WHERE `statuses`.`title` LIKE 'On Loan' 
     AND `items`.`duedate` < '2015-04-24' 
     AND `items`.`return_date` IS NULL 
     ORDER BY `items`.`duedate`) x 
); 

Если я не могу получить эту работу я мог бы принять два других подхода:

1) Я мог бы просто жестко закодировать SQL, но я хочу использовать ActiveRecord и ссылки на модели, чтобы сохранить его агностик базы данных.

2) Другой способ - вернуть экземпляр всех записей и пропустить их через отдельные обновления. Это будет проблемой производительности, но я могу принять это, потому что это фоновая работа, которая не будет обновлять больше, чем несколько записей в день.

Update

У меня есть запрос AREL ниже, производит подзапрос в формате мне нужно.

x = Arel::Table.new('x') 

overdues = Item.select(x[:id]).from(
Item.select(Item.arel_table[:id]).where(
    Status.arel_table[:title].matches("On Loan").and(
    Item.arel_table[:duedate].lt(DateTime.now.to_date).and(
    Item.arel_table[:return_date].eq(nil)) 
    ) 
).joins(
    Item.arel_table.join(Status.arel_table, Arel::Nodes::OuterJoin).on(
     Item.arel_table[:status_id].eq(Status.arel_table[:id]) 
    ).join_sources 
).joins(
    Item.arel_table.join(Resource.arel_table, Arel::Nodes::OuterJoin).on(
     Item.arel_table[:resource_id].eq(Resource.arel_table[:id]) 
    ).join_sources 
).order(Item.arel_table[:duedate]).uniq.as('x') 
) 

К сожалению, он возвращает ошибку, когда я использую ее в своем заявлении о обновлении.

TypeError: Cannot visit Item::ActiveRecord_Relation 

ответ

0

После вновь этот вопрос, я нахожусь в заключении, что это не возможно сделать это из-за ограничения, связанного с MySQL:

ActiveRecord :: StatementInvalid: mysql2 :: Ошибка: Вы можете не указывать целевая таблица 'items' для обновления в предложении FROM:

Это должно быть возможно сделать с другими базами данных (хотя я не тестировал это).

Я мог бы создать временную таблицу, которая является копией исходной таблицы, ссылку, а затем отбрасывать временную таблицу, как этот пост предлагает: http://richtextblog.blogspot.co.uk/2007/09/mysql-temporary-tables-and-rails.html. Кажется, много накладных расходов сделать простой подзапрос.

Что я буду делать, это найти все идентификаторы и пропустить их и обновить записи таким образом (используя простую находку и обновление). У этого есть накладные расходы, но он должен только обновлять несколько записей каждого запуска (не более 100). Обновление будет выполняться как запланированное задание за пределами рабочего времени пользователя, чтобы оно не повлияло на производительность.

Я все еще нахожу это странным, что во всех других вариантах SQL я никогда не сталкивался с этой проблемой раньше. Ты все еще живешь и учишься.

UPDATE: С момента обновления моей версии MySQL теперь действует инструкция select. Я должен был принять заказ, чтобы он работал.

ORDER BY `items`.`duedate` 

Я сейчас использую версию: 5.7.19.