¿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?

kid_drew
fuente

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 protected]"
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 protected]"
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 protected]"
  user.save
end
Mohamad
fuente
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: "[email protected]").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
Montreal
fuente
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: "[email protected]").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)
Karen
fuente
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
Imran Ahmad
fuente
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
Aeramor
fuente
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
Rajesh Kolappakam
fuente
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
0

La gema de la secuela agrega un método update_or_create que parece hacer lo que estás buscando.

KurtPreston
fuente
11
La pregunta es sobre Active Record.
ja