Rails criam ou atualizam magia?


93

Eu tenho uma classe chamada CachedObjectque armazena objetos serializados genéricos indexados por chave. Eu quero que esta classe implemente um create_or_updatemétodo. Se um objeto for encontrado, ele o atualizará, caso contrário, criará um novo.

Existe uma maneira de fazer isso no Rails ou eu tenho que escrever meu próprio método?

Respostas:


172

Rails 6

O Rails 6 adicionou um método upserte upsert_allque oferece essa funcionalidade.

Model.upsert(column_name: value)

[upsert] Não instancia nenhum modelo nem aciona callbacks ou validações do Active Record.

Trilhos 5, 4 e 3

Não se você estiver procurando por um tipo de instrução "upsert" (onde o banco de dados executa uma atualização ou uma instrução de inserção na mesma operação). Fora da caixa, Rails e ActiveRecord não têm esse recurso. Você pode usar a gema upsert , no entanto.

Caso contrário, você pode usar: find_or_initialize_byou find_or_create_by, que oferece funcionalidade semelhante, embora ao custo de uma ocorrência adicional no banco de dados, o que, na maioria dos casos, dificilmente é um problema. Portanto, a menos que você tenha sérias preocupações de desempenho, eu não usaria a gema.

Por exemplo, se nenhum usuário for encontrado com o nome "Roger", uma nova instância do usuário é instanciada com seu nameconjunto para "Roger".

user = User.where(name: "Roger").first_or_initialize
user.email = "email@example.com"
user.save

Alternativamente, você pode usar find_or_initialize_by.

user = User.find_or_initialize_by(name: "Roger")

No Rails 3.

user = User.find_or_initialize_by_name("Roger")
user.email = "email@example.com"
user.save

Você pode usar um bloco, mas o bloco só é executado se o registro for novo .

User.where(name: "Roger").first_or_initialize do |user|
  # this won't run if a user with name "Roger" is found
  user.save 
end

User.find_or_initialize_by(name: "Roger") do |user|
  # this also won't run if a user with name "Roger" is found
  user.save
end

Se você quiser usar um bloco independentemente da persistência do registro, use tapno resultado:

User.where(name: "Roger").first_or_initialize.tap do |user|
  user.email = "email@example.com"
  user.save
end


1
O código-fonte para o qual o documento aponta mostra que isso não funciona da maneira que você sugere - o bloco é passado para um novo método apenas se o registro correspondente não existir. Parece não haver mágica "upsert" no Rails - você tem que separá-lo em duas instruções Ruby, uma para a seleção do objeto e outra para a atualização do atributo.
mesmos

@sameers Não tenho certeza se entendi o que você quis dizer. O que você acha que estou sugerindo?
Mohamad

1
Oh ... eu entendo o que você quis dizer agora - que ambos se formam, find_or_initialize_bye find_or_create_byaceitam um bloqueio. Achei que você quisesse dizer que, quer o registro exista ou não, um bloco será passado com o objeto do registro como argumento, a fim de fazer a atualização.
mesmos

3
É um pouco enganador, não necessariamente a resposta, mas a API. Seria de se esperar que o bloco fosse aprovado independentemente e, portanto, poderia ser criado / atualizado de acordo. Em vez disso, temos que dividi-lo em declarações separadas. Vaia. <3 Rails embora :)
Volte

32

No Rails 4 você pode adicionar a um modelo específico:

def self.update_or_create(attributes)
  assign_or_new(attributes).save
end

def self.assign_or_new(attributes)
  obj = first || new
  obj.assign_attributes(attributes)
  obj
end

e usar como

User.where(email: "a@b.com").update_or_create(name: "Mr A Bbb")

Ou se você preferir adicionar esses métodos a todos os modelos colocados em um inicializador:

module ActiveRecordExtras
  module Relation
    extend ActiveSupport::Concern

    module ClassMethods
      def update_or_create(attributes)
        assign_or_new(attributes).save
      end

      def update_or_create!(attributes)
        assign_or_new(attributes).save!
      end

      def assign_or_new(attributes)
        obj = first || new
        obj.assign_attributes(attributes)
        obj
      end
    end
  end
end

ActiveRecord::Base.send :include, ActiveRecordExtras::Relation

1
Não assign_or_newretornará a primeira linha da tabela se ela existir e então essa linha será atualizada? Parece estar fazendo isso por mim.
steve klein

User.where(email: "a@b.com").firstretornará nulo se não for encontrado. Certifique-se de ter uma whereluneta
montrealmike

Apenas uma nota para dizer que updated_atnão será tocado porque assign_attributesé usado
lshepstone

Não é porque você está usando assing_or_new, mas sim se usar update_or_create por causa do save
montrealmike

13

Adicione isto ao seu modelo:

def self.update_or_create_by(args, attributes)
  obj = self.find_or_create_by(args)
  obj.update(attributes)
  return obj
end

Com isso, você pode:

User.update_or_create_by({name: 'Joe'}, attributes)

A segunda parte não vou funcionar. Não é possível atualizar um único registro do nível de classe sem o ID do registro a ser atualizado.
Aeramor

1
obj = self.find_or_create_by (args); obj.update (atributos); return obj; irá funcionar.
veeresh yh

12

A magia que você estava procurando foi adicionada em Rails 6 Agora você pode fazer o upsert (atualizar ou inserir). Para uso de registro único:

Model.upsert(column_name: value)

Para vários registros, use upsert_all :

Model.upsert_all(column_name: value, unique_by: :column_name)

Nota :

  • Ambos os métodos não disparam callbacks ou validações do Active Record
  • unique_by => PostgreSQL e SQLite apenas

1

Pergunta antiga, mas jogando minha solução no ringue para ser completa. Eu precisava disso quando precisava de um achado específico, mas uma criação diferente, se ela não existir.

def self.find_by_or_create_with(args, attributes) # READ CAREFULLY! args for finding, attributes for creating!
        obj = self.find_or_initialize_by(args)
        return obj if obj.persisted?
        return obj if obj.update_attributes(attributes) 
end

0

Você pode fazer isso em uma declaração como esta:

CachedObject.where(key: "the given key").first_or_create! do |cached|
   cached.attribute1 = 'attribute value'
   cached.attribute2 = 'attribute value'
end

9
Isso não funcionará, pois só retornará o registro original se algum for encontrado. O OP pede uma solução que sempre altera o valor, mesmo se um registro for encontrado.
JeanMertz de

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.