Rails membuat atau memperbarui sihir?

95

Saya memiliki kelas yang disebut CachedObjectyang menyimpan objek serial generik yang diindeks oleh kunci. Saya ingin kelas ini menerapkan create_or_updatemetode. Jika sebuah objek ditemukan itu akan memperbaruinya, jika tidak maka akan membuat yang baru.

Apakah ada cara untuk melakukan ini di Rails atau apakah saya harus menulis metode saya sendiri?

kid_drew
sumber

Jawaban:

175

Rel 6

Rails 6 menambahkan upsertdan upsert_allmetode yang memberikan fungsionalitas ini.

Model.upsert(column_name: value)

[upsert] Ini tidak membuat contoh model apa pun juga tidak memicu callback atau validasi Rekaman Aktif.

Rel 5, 4, dan 3

Tidak jika Anda mencari jenis pernyataan "upsert" (di mana database menjalankan pembaruan atau pernyataan sisipkan dalam operasi yang sama). Di luar kotak, Rails dan ActiveRecord tidak memiliki fitur seperti itu. Namun, Anda dapat menggunakan permata upsert .

Jika tidak, Anda dapat menggunakan: find_or_initialize_byatau find_or_create_by, yang menawarkan fungsionalitas serupa, meskipun dengan biaya hit database tambahan, yang, dalam banyak kasus, hampir tidak menjadi masalah sama sekali. Jadi, kecuali Anda memiliki masalah kinerja yang serius, saya tidak akan menggunakan permata itu.

Misalnya, jika tidak ada pengguna yang ditemukan dengan nama "Roger", instance pengguna baru akan dibuat dengan nameset ke "Roger".

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

Atau, Anda bisa menggunakan find_or_initialize_by.

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

Di Rails 3.

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

Anda dapat menggunakan sebuah blok, tetapi blok tersebut hanya berjalan jika recordnya baru .

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

Jika Anda ingin menggunakan blok terlepas dari persistensi record, gunakan taphasil:

User.where(name: "Roger").first_or_initialize.tap do |user|
  user.email = "[email protected]"
  user.save
end
Muhammad
sumber
1
Kode sumber yang ditunjukkan oleh dokumen menunjukkan bahwa ini tidak berfungsi seperti yang Anda maksudkan - blok diteruskan ke metode baru hanya jika rekaman yang sesuai tidak ada. Tampaknya tidak ada keajaiban "upsert" di Rails - Anda harus memisahkannya menjadi dua pernyataan Ruby, satu untuk pemilihan objek dan satu lagi untuk pembaruan atribut.
sameers
@sameers Saya tidak yakin saya mengerti apa yang Anda maksud. Menurut Anda apa yang saya maksudkan?
Mohamad
1
Oh ... Saya mengerti apa yang Anda maksud sekarang - bahwa kedua bentuk, find_or_initialize_bydan find_or_create_bymenerima satu blok. Saya pikir yang Anda maksud adalah apakah record ada atau tidak, sebuah blok akan diturunkan dengan objek record sebagai argumen, untuk melakukan pembaruan.
sameers
3
Ini sedikit menyesatkan, belum tentu jawabannya, tetapi API. Seseorang akan mengharapkan blok tersebut dilalui, dan dengan demikian dapat dibuat / diperbarui sesuai. Sebaliknya, kita harus memecahnya menjadi pernyataan terpisah. Huu. <3 Rel meskipun :)
Volte
32

Di Rails 4 Anda dapat menambahkan model tertentu:

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

dan menggunakannya seperti

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

Atau jika Anda lebih suka menambahkan metode ini ke semua model, masukkan penginisialisasi:

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
sumber
1
Tidak akan assign_or_newmengembalikan baris pertama dalam tabel jika ada dan kemudian baris itu akan diperbarui? Sepertinya melakukan itu untuk saya.
steve klein
User.where(email: "[email protected]").firstakan mengembalikan nihil jika tidak ditemukan. Pastikan Anda memiliki ruang wherelingkup
montrealmike
Hanya catatan untuk mengatakan bahwa updated_attidak akan disentuh karena assign_attributesdigunakan
lshepstone
Ini tidak akan adalah Anda menggunakan assing_or_new tetapi akan jika Anda menggunakan update_or_create karena penyimpanan
montrealmike
13

Tambahkan ini ke model Anda:

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

Dengan itu, Anda dapat:

User.update_or_create_by({name: 'Joe'}, attributes)
Karen
sumber
Bagian kedua saya tidak akan bekerja. Tidak dapat memperbarui satu catatan dari tingkat kelas tanpa ID catatan untuk diperbarui.
Aeramor
2
obj = self.find_or_create_by (args); obj.update (atribut); return obj; akan bekerja.
veeresh yh
12

Keajaiban yang Anda cari telah ditambahkan di Rails 6 Sekarang Anda dapat melakukan upsert (perbarui atau sisipkan). Untuk penggunaan rekaman tunggal:

Model.upsert(column_name: value)

Untuk beberapa catatan, gunakan upsert_all :

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

Catatan :

  • Kedua metode tidak memicu callback atau validasi Rekaman Aktif
  • unique_by => Hanya PostgreSQL dan SQLite
Imran Ahmad
sumber
1

Pertanyaan lama tapi melemparkan solusi saya ke ring untuk kelengkapan. Saya membutuhkan ini ketika saya membutuhkan penemuan khusus tetapi pembuatan yang berbeda jika tidak ada.

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
sumber
0

Anda dapat melakukannya dalam satu pernyataan seperti ini:

CachedObject.where(key: "the given key").first_or_create! do |cached|
   cached.attribute1 = 'attribute value'
   cached.attribute2 = 'attribute value'
end
Rajesh Kolappakam
sumber
9
Ini tidak akan berfungsi, karena ini hanya akan mengembalikan rekaman asli jika ditemukan. OP meminta solusi yang selalu mengubah nilainya, bahkan jika record ditemukan.
JeanMertz
0

The sekuel permata menambahkan update_or_create metode yang tampaknya melakukan apa yang Anda cari.

KurtPreston
sumber
11
Pertanyaannya adalah tentang Rekaman Aktif.
ja