Como executar um sql de atualização bruto com vinculação dinâmica em trilhos

98

Eu quero executar uma atualização de sql bruto como abaixo:

update table set f1=? where f2=? and f3=?

Este SQL será executado por ActiveRecord::Base.connection.execute, mas não sei como passar os valores dos parâmetros dinâmicos para o método.

Alguém poderia me ajudar nisso?

Ywenbo
fonte
Por que você quer fazer isso usando SQL bruto, o objetivo do ActiveRecord é ajudá-lo a evitar que ...
Andrew
se eu usar AR, em primeiro lugar, devo obter o objeto de modelo pelo método find do AR com o campo id e, em seguida, fazer a operação de atualização. Portanto, do ponto de vista das operações, um UPDATE AR precisa de dois sqls com banco de dados; por outro lado, não tenho certeza se o método de atualização do AR usa vinculação dinâmica. então eu quero usar sql bruto com vinculação dinâmica para uma interação com db para operação de atualização, mas não sei como passar parâmetros para substituir o? em sql por AR.
ywenbo
27
Existem muitas razões válidas para fazer isso. Primeiro, a consulta pode ser muito complexa para ser traduzida no uso do método Ruby normal ... segundo, os parâmetros podem ter caracteres especiais como%, ou aspas, e é um saco escapar do ..
Henley Chiu
3
@Andrew, é melhor usar funções cruas do mysql do que aceitar a "conveniência" que o AR oferece.
Verde de
4
@Green não se você quiser mover seu aplicativo do MySQL para PostgreSQL ou qualquer outra coisa. Um dos principais pontos de um ORM é tornar seu aplicativo portátil.
Andrew,

Respostas:

102

Não parece que a API Rails expõe métodos para fazer isso genericamente. Você pode tentar acessar a conexão subjacente e usar seus métodos, por exemplo, para MySQL:

st = ActiveRecord::Base.connection.raw_connection.prepare("update table set f1=? where f2=? and f3=?")
st.execute(f1, f2, f3)
st.close

Não tenho certeza se existem outras ramificações para fazer isso (conexões deixadas em aberto, etc). Eu rastrearia o código do Rails para uma atualização normal para ver o que ele está fazendo além da consulta real.

Usar consultas preparadas pode economizar um pouco de tempo no banco de dados, mas a menos que você esteja fazendo isso um milhão de vezes seguidas, provavelmente seria melhor apenas construir a atualização com a substituição normal do Ruby, por exemplo

ActiveRecord::Base.connection.execute("update table set f1=#{ActiveRecord::Base.sanitize(f1)}")

ou usando ActiveRecord como os comentaristas disseram.

Brian Deterling
fonte
26
Cuidado com o método sugerido de "substituição normal de Ruby", field=#{value}pois isso o deixa aberto a ataques de injeção de SQL. Se você seguir esse caminho, verifique o módulo ActiveRecord :: ConnectionAdapters :: Quoting .
Paul Annesley
3
Observe que mysql2 não suporta instruções preparadas, consulte também: stackoverflow.com/questions/9906437/…
reto
1
@reto Parece que eles estão próximos: github.com/brianmario/mysql2/pull/289
Brian Deterling
3
Nenhuma preparedeclaração em Mysql2
Verde de
1
De acordo com a documentação: "Nota: dependendo do seu conector de banco de dados, o resultado retornado por este método pode ser gerenciado manualmente pela memória. Considere usar o wrapper #exec_query." . executeé um método perigoso e pode causar vazamentos de memória ou outros recursos.
kolen
32

ActiveRecord::Base.connectiontem um quotemétodo que recebe um valor de string (e, opcionalmente, o objeto de coluna). Então você pode dizer o seguinte:

ActiveRecord::Base.connection.execute(<<-EOQ)
  UPDATE  foo
  SET     bar = #{ActiveRecord::Base.connection.quote(baz)}
EOQ

Observe se você estiver em uma migração Rails ou um objeto ActiveRecord, você pode encurtar para:

connection.execute(<<-EOQ)
  UPDATE  foo
  SET     bar = #{connection.quote(baz)}
EOQ

ATUALIZAÇÃO: como @kolen aponta, você deve usar em seu exec_updatelugar. Isso cuidará da cotação para você e também evitará o vazamento de memória. A assinatura funciona de maneira um pouco diferente:

connection.exec_update(<<-EOQ, "SQL", [[nil, baz]])
  UPDATE  foo
  SET     bar = $1
EOQ

Aqui, o último parâmetro é uma matriz de tuplas que representam os parâmetros de vinculação. Em cada tupla, a primeira entrada é o tipo de coluna e a segunda é o valor. Você pode dar nilpara o tipo de coluna e o Rails geralmente fará a coisa certa.

Existem também exec_query, exec_inserte exec_delete, dependendo do que você precisa.

Paul A Jungwirth
fonte
3
De acordo com a documentação: "Nota: dependendo do seu conector de banco de dados, o resultado retornado por este método pode ser gerenciado manualmente pela memória. Considere usar o wrapper #exec_query." . executeé um método perigoso e pode causar vazamentos de memória ou outros recursos.
kolen de
1
Uau, boa pegada! Aqui está a documentação . Também parece que o adaptador Postgres vazaria aqui.
Paul A Jungwirth de
Imagino, se AR nos deixa com nada para on duplicate key updatetodos os lugares
Shrinath,
1
@Shrinath, já que esta questão é sobre como escrever SQL bruto, você pode fazer a consulta ON CONFLICT DO UPDATEse quiser. Para SQL não bruto, esta gema parece útil: github.com/jesjos/active_record_upsert
Paul A Jungwirth,
4

Você deve apenas usar algo como:

YourModel.update_all(
  ActiveRecord::Base.send(:sanitize_sql_for_assignment, {:value => "'wow'"})
)

Isso resolveria o problema. Usar o método ActiveRecord :: Base # send para invocar sanitize_sql_for_assignment faz com que o Ruby (pelo menos a versão 1.8.7) ignore o fato de que sanitize_sql_for_assignment é na verdade um método protegido.

Leandroico
fonte
2

Às vezes seria melhor usar o nome da classe pai em vez do nome da tabela:

# Refers to the current class
self.class.unscoped.where(self.class.primary_key => id).update_all(created _at: timestamp)

Por exemplo, classe base "Pessoa", subclasses (e tabelas de banco de dados) "Cliente" e "Vendedor" Em vez de usar:

Client.where(self.class.primary_key => id).update_all(created _at: timestamp)
Seller.where(self.class.primary_key => id).update_all(created _at: timestamp)

Você pode usar o objeto da classe base desta forma:

person.class.unscoped.where(self.class.primary_key => id).update_all(created _at: timestamp)
Mikowiec
fonte
1

Por que usar SQL bruto para isso?

Se você tiver um modelo para ele, use where:

f1 = 'foo'
f2 = 'bar'
f3 = 'buzz'
YourModel.where('f1 = ? and f2 = ?', f1, f2).each do |ym|
  # or where(f1: f1, f2: f2).each do (...)
  ym.update(f3: f3) 
end

Se você não tiver um modelo para ele (apenas a tabela), você pode criar um arquivo e um modelo que herdará deActiveRecord::Base

class YourTable < ActiveRecord::Base
  self.table_name = 'your_table' # specify explicitly if needed
end

e novamente use whereo mesmo que acima:

ToTenMilan
fonte
2
Isso é muito menos eficiente, não é? Isso não troca uma instrução de atualização por uma seleção, traz os registros para a memória do Rails, atualiza cada registro e envia uma instrução de atualização de volta para cada registro. 1 atualização vs 1 por registro e muita largura de banda, etc.? AR otimiza isso ou não? Eu acho que não.
Jimi Kimble
0

Aqui está um truque que descobri recentemente para executar sql bruto com binds:

binds = SomeRecord.bind(a_string_field: value1, a_date_field: value2) +
        SomeOtherRecord.bind(a_numeric_field: value3)
SomeRecord.connection.exec_query <<~SQL, nil, binds
  SELECT *
  FROM some_records
  JOIN some_other_records ON some_other_records.record_id = some_records.id
  WHERE some_records.a_string_field = $1
    AND some_records.a_date_field < $2
    AND some_other_records.a_numeric_field > $3
SQL

onde ApplicationRecorddefine isso:

# Convenient way of building custom sql binds
def self.bind(column_values)
  column_values.map do |column_name, value|
    [column_for_attribute(column_name), value]
  end
end

e isso é semelhante a como o AR vincula suas próprias consultas.

espuma
fonte
-11

Eu precisei usar sql puro porque falhei em fazer com que o composite_primary_keys funcionasse com o activerecord 2.3.8. Portanto, para acessar a tabela sqlserver 2000 com uma chave primária composta, o sql bruto foi necessário.

sql = "update [db].[dbo].[#{Contacts.table_name}] " +
      "set [COLUMN] = 0 " +
      "where [CLIENT_ID] = '#{contact.CLIENT_ID}' and CONTACT_ID = '#{contact.CONTACT_ID}'"
st = ActiveRecord::Base.connection.raw_connection.prepare(sql)
st.execute

Se uma solução melhor estiver disponível, compartilhe.

Donvnielsen
fonte
10
injeção sql é possível aqui!
mystdeim de
-18

No Rails 3.1, você deve usar a interface de consulta:

  • novo (atributos)
  • criar (atributos)
  • criar! (atributos)
  • find (id_or_array)
  • destruir (id_or_array)
  • destroy_all
  • delete (id_or_array)
  • delete_all
  • atualização (ids, atualizações)
  • update_all (atualizações)
  • existe?

update e update_all são as operações de que você precisa.

Veja os detalhes aqui: http://m.onkey.org/active-record-query-interface

ativares
fonte