Szyny tworzą lub aktualizują magię?

95

Mam klasę o nazwie, CachedObjectktóra przechowuje ogólne zserializowane obiekty indeksowane według klucza. Chcę, aby ta klasa zaimplementowała create_or_updatemetodę. Jeśli obiekt zostanie znaleziony, zaktualizuje go, w przeciwnym razie utworzy nowy.

Czy istnieje sposób na zrobienie tego w Railsach, czy też muszę napisać własną metodę?

kid_drew
źródło

Odpowiedzi:

175

Szyny 6

Railsy 6 dodały metody upserti upsert_all, które zapewniają tę funkcjonalność.

Model.upsert(column_name: value)

[upsert] Nie tworzy instancji żadnych modeli ani nie wyzwala wywołań zwrotnych ani walidacji modułu Active Record.

Szyny 5, 4 i 3

Nie, jeśli szukasz typu instrukcji „upsert” (gdzie baza danych wykonuje aktualizację lub instrukcję wstawiania w tej samej operacji). Po wyjęciu z pudełka, Railsy i ActiveRecord nie mają takiej funkcji. Możesz jednak użyć górnego klejnotu.

W przeciwnym razie możesz użyć: find_or_initialize_bylub find_or_create_by, które oferują podobną funkcjonalność, jednak kosztem dodatkowego trafienia do bazy danych, co w większości przypadków nie stanowi żadnego problemu. Więc jeśli nie masz poważnych problemów z wydajnością, nie użyłbym tego klejnotu.

Na przykład, jeśli nie zostanie znaleziony żaden użytkownik o nazwie „Roger”, tworzona jest instancja nowego użytkownika z nameustawieniem „Roger”.

user = User.where(name: "Roger").first_or_initialize
user.email = "[email protected]"
user.save

Alternatywnie możesz użyć find_or_initialize_by.

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

W Railsach 3.

user = User.find_or_initialize_by_name("Roger")
user.email = "[email protected]"
user.save

Możesz użyć bloku, ale blok działa tylko wtedy, gdy rekord jest nowy .

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

Jeśli chcesz użyć bloku niezależnie od trwałości rekordu, użyj tapna wyniku:

User.where(name: "Roger").first_or_initialize.tap do |user|
  user.email = "[email protected]"
  user.save
end
Mohamad
źródło
1
Kod źródłowy, na który wskazuje dokument, pokazuje, że nie działa to tak, jak sugerujesz - blok jest przekazywany do nowej metody tylko wtedy, gdy odpowiedni rekord nie istnieje. Wygląda na to, że w Railsach nie ma magii „upsert” - musisz ją rozdzielić na dwie instrukcje Ruby, jedną do wyboru obiektów, a drugą do aktualizacji atrybutów.
sameers
@sameers Nie jestem pewien, czy rozumiem, co masz na myśli. Jak myślisz, co sugeruję?
Mohamad,
1
Och ... Rozumiem, co miałeś na myśli - że obie formy find_or_initialize_byi find_or_create_byakceptują blok. Myślałem, że chodziło Ci o to, że niezależnie od tego, czy rekord istnieje, czy nie, blok zostanie przekazany z obiektem rekordu jako argumentem w celu wykonania aktualizacji.
sameers
3
To trochę mylące, niekoniecznie odpowiedź, ale API. Można by oczekiwać, że blok zostanie przekazany niezależnie, a zatem może zostać odpowiednio utworzony / zaktualizowany. Zamiast tego musimy podzielić to na osobne zdania. Gwizd. <3 szyny :)
Volte
32

W Rails 4 możesz dodać do konkretnego modelu:

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

i używaj go jak

User.where(email: "[email protected]").update_or_create(name: "Mr A Bbb")

Lub jeśli wolisz dodać te metody do wszystkich modeli umieszczonych w inicjatorze:

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
montrealmike
źródło
1
Czy nie assign_or_newzwróci pierwszego wiersza w tabeli, jeśli istnieje, a następnie ten wiersz zostanie zaktualizowany? Wydaje się, że robi to dla mnie.
steve klein
User.where(email: "[email protected]").firstzwróci nil, jeśli nie zostanie znaleziony. Upewnij się, że masz wherelunetę
montrealmike
Tylko uwaga, aby powiedzieć, że updated_atnie zostanie dotknięty, ponieważ assign_attributesjest używany
lshepstone
Nie będzie, jeśli używasz assing_or_new, ale zrobisz to, jeśli użyjesz update_or_create z powodu zapisu
montrealmike
13

Dodaj to do swojego modelu:

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

Dzięki temu możesz:

User.update_or_create_by({name: 'Joe'}, attributes)
Karen
źródło
Druga część nie będzie działać. Nie można zaktualizować pojedynczego rekordu z poziomu klasy bez identyfikatora rekordu do zaktualizowania.
Aeramor
2
obj = self.find_or_create_by (args); obj.update (atrybuty); return obj; będzie działać.
veeresh yh
12

Magia, której szukałeś, została dodana w Rails 6 Teraz możesz upsert (zaktualizować lub wstawić). Do jednorazowego użytku:

Model.upsert(column_name: value)

W przypadku wielu rekordów użyj upsert_all :

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

Uwaga :

  • Obie metody nie wyzwalają wywołań zwrotnych ani walidacji modułu Active Record
  • unique_by => Tylko PostgreSQL i SQLite
Imran Ahmad
źródło
1

Stare pytanie, ale wrzucam moje rozwiązanie do ringu dla kompletności. Potrzebowałem tego, gdy potrzebowałem konkretnego znaleziska, ale innego stworzenia, jeśli nie istnieje.

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
źródło
0

Możesz to zrobić w jednym oświadczeniu w ten sposób:

CachedObject.where(key: "the given key").first_or_create! do |cached|
   cached.attribute1 = 'attribute value'
   cached.attribute2 = 'attribute value'
end
Rajesh Kolappakam
źródło
9
To nie zadziała, ponieważ zwróci oryginalny rekord tylko wtedy, gdy zostanie znaleziony. PO prosi o rozwiązanie, które zawsze zmienia wartość, nawet jeśli zostanie znaleziony rekord.
JeanMertz
0

Sequel gem dodaje update_or_create sposób, który wydaje się robić to, co szukasz.

KurtPreston
źródło
11
Pytanie dotyczy modułu Active Record.
ja