¿Los rieles crean o actualizan magia?


93

Tengo una clase llamada CachedObjectque almacena objetos serializados genéricos indexados por clave. Quiero que esta clase implemente un create_or_updatemétodo. Si se encuentra un objeto, lo actualizará; de lo contrario, creará uno nuevo.

¿Hay alguna forma de hacer esto en Rails o tengo que escribir mi propio método?

Respuestas:


172

Carriles 6

Rails 6 agregó métodos upserty upsert_allque brindan esta funcionalidad.

Model.upsert(column_name: value)

[upsert] No crea instancias de ningún modelo ni activa devoluciones de llamada o validaciones de Active Record.

Carriles 5, 4 y 3

No si está buscando un tipo de declaración "upsert" (donde la base de datos ejecuta una actualización o una declaración de inserción en la misma operación). Fuera de la caja, Rails y ActiveRecord no tienen tal característica. Sin embargo, puedes usar la gema upsert .

De lo contrario, puede usar: find_or_initialize_byo find_or_create_by, que ofrecen una funcionalidad similar, aunque a costa de un acceso adicional a la base de datos, lo que, en la mayoría de los casos, no es un problema en absoluto. Entonces, a menos que tenga serias preocupaciones de rendimiento, no usaría la gema.

Por ejemplo, si no se encuentra ningún usuario con el nombre "Roger", se namecrea una instancia de una nueva instancia de usuario con su conjunto en "Roger".

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

Alternativamente, puede usar find_or_initialize_by.

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

En rieles 3.

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

Puede usar un bloque, pero el bloque solo se ejecuta si el registro es nuevo .

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

Si desea usar un bloque independientemente de la persistencia del registro, use tapen el resultado:

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


1
El código fuente al que apunta el documento muestra que esto no funciona de la manera que usted implica que lo hace: el bloque se pasa a un nuevo método solo si el registro correspondiente no existe. Parece que no hay magia "upsert" en Rails; tienes que separarla en dos declaraciones Ruby, una para la selección de objetos y otra para la actualización de atributos.
sameers

@sameers No estoy seguro de entender lo que quieres decir. ¿Qué crees que estoy insinuando?
Mohamad

1
Oh ... ya veo lo que quisiste decir ahora - que ambas formas, find_or_initialize_byy find_or_create_byacepto un bloque. Pensé que querías decir que, ya sea que el registro exista o no, se transmitirá un bloque con el objeto de registro como argumento para realizar la actualización.
sameers

3
Es un poco engañoso, no necesariamente la respuesta, sino la API. Uno esperaría que el bloque se pasara independientemente y, por lo tanto, podría crearse / actualizarse en consecuencia. En cambio, tenemos que dividirlo en declaraciones separadas. Abucheo. <3 rieles aunque :)
Volte

32

En Rails 4 puedes agregar a un 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

y úsalo como

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

O si prefiere agregar estos métodos a todos los modelos, coloque un 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
¿No assign_or_newdevolverá la primera fila de la tabla si existe y luego esa fila se actualizará? Parece estar haciendo eso por mí.
steve klein

User.where(email: "a@b.com").firstdevolverá nil si no se encuentra. Asegúrese de tener un wherealcance
montrealmike

Solo una nota para decir que updated_atno se tocará porque assign_attributesse usa
lshepstone

No lo hará si está usando assing_or_new, pero lo hará si usa update_or_create debido al guardado
montrealmike

13

Agregue esto a su modelo:

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

Con eso, puedes:

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

La segunda parte no funcionaré. No se puede actualizar un solo registro del nivel de clase sin el ID del registro para actualizar.
Aeramor

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

12

La magia que ha estado buscando se ha agregado en Rails 6 Ahora puede insertar (actualizar o insertar). Para uso de un solo registro:

Model.upsert(column_name: value)

Para varios registros, use upsert_all :

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

Nota :

  • Ambos métodos no activan devoluciones de llamada o validaciones de Active Record
  • unique_by => PostgreSQL y SQLite solamente

1

Antigua pregunta, pero arrojo mi solución al ring para que esté completa. Necesitaba esto cuando necesitaba un hallazgo específico pero una creación diferente si no existe.

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

Puede hacerlo en una declaración como esta:

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

9
Esto no funcionará, ya que solo devolverá el registro original si se encuentra uno. El OP solicita una solución que siempre cambie el valor, incluso si se encuentra un registro.
JeanMertz

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.