ActiveRecord:如果存在则更新记录,否则创建?

46

尝试在Active Record中实现创建如果不存在就更新记录。

目前使用:

@student = Student.where(:user_id => current_user.id).first

if @student
    Student.destroy_all(:user_id => current_user.id)
end

Student = Student.new(:user_id => current_user.id, :department => 1
)
Student.save!

如果记录存在,则更新它的正确方法是什么?否则创建它?

7个回答

82

在Rails 4中

Student.
  find_or_initialize_by(:user_id => current_user.id).
  update_attributes!(:department => 1)

6
find_or_initialize_byupdate_attributes! 的结合可以完成这个任务。 - fguillen
4
update_attributes! 方法返回的是一个布尔值而不是实际记录。您可以使用 Student.find_or_initialize_by(...).tap { |e| e.update!(...) } 来更新记录。 - elado
1
有几个问题需要解决 - 首先,你没有办法“仅在创建时设置值”,其次,它查看的是表列而不是模型方法。例如,User.where(email: email).find_or_initialize_by(password: password) 会给你一个错误 columns users.password does not exist(假设你明智地不将明文密码存储在数据库中)。 - PJSCopeland
1
不错的解决方案。在Rails 4中,我只需要将.update_attributes更改为.update。这是惯用的方法,源代码显示它与.update_attributes!相同(它只会分配属性并调用.save...请查看https://apidock.com/rails/ActiveRecord/Persistence/update和https://apidock.com/rails/v3.2.3/ActiveRecord/Persistence/update_attributes)。 - sandre89
请注意, find_or_initialize_by 可能不会触发任何验证。 - hugholousk

17

36
first_or_create 不会更新记录,所以它不会让代码更加优美。 - Rune Schjellerup Philosof
但它确实会返回找到或创建的记录,因此您可以在其上链接一个“update”调用。请参见此页面上的我的答案 - PJSCopeland
2
这会导致创建时的验证问题,并引起2个单独的数据库事务。@Zorayr的答案更可取,因为它尊重验证。 - Doug
没错。为了通过验证,我认为你可以像使用first_or_create一样使用first_or_initialize; 但是你必须在结果上调用saveupdate才能使其持久化。 - PJSCopeland
@rune-schjellerup-philosof 但你可以在此之后进行更新 .first_or_create(user_id: current_user.id, department: 1).update(department: 1) - ln9187

15

::first_or_create(自 v3.2.1 起可用)功能与其名称相符。

Model.where(find: 'find_value').
  first_or_create(create: 'create_value')

# If a record with {find: 'find_value'} already exists:
before #=> #<Model id: 1, find: "find_value", create: nil>
after  #=> #<Model id: 1, find: "find_value", create: nil>

# Otherwise:
before #=> nil
after  #=> #<Model id: 2, find: "find_value", create: "create_value">

如果你希望它也更新已经存在的记录,请尝试:

如果您需要更新现有记录,请尝试:

Model.where(find: 'find_value').
  first_or_create(create: 'create_value').
  update(update: 'update_value')

# If one already exists:
before #=> #<Model id: 1, find: "find_value", create: nil, update: nil>
after  #=> #<Model id: 1, find: "find_value", create: nil, update: "update_value">

# If it already matches, no UPDATE statement will be run:
before #=> #<Model id: 1, find: "find_value", create: nil, update: "update_value">
after  #=> #<Model id: 1, find: "find_value", create: nil, update: "update_value">

# Otherwise:
before #=> nil
after  #=> #<Model id: 2, find: "find_value", create: 'create_value', update: "update_value">

编辑于2016年3月8日:根据Dougcomment,如果您的验证在#create#update调用之间失败,或者您想最小化数据库调用,您可以使用::first_or_initialize来避免在第一次调用时持久化记录。但是,您必须确保在之后调用#save#update以便持久化记录(我不确定#update是否适用于尚未持久化的记录):

Model.validates :update, presence: true # The create call would fail this

Model.where(find: 'find_value').
  first_or_initialize(create: 'create_value'). # doesn't call validations
  update(update: 'update_value')

(注意,确实有一个名为#create_or_update的方法,但不要被在Google上找到的任何文档所迷惑;那只是由#save使用的私有方法。)


7
@student = Student.where(user_id: current_user.id).first
@student ||= Student.new(user_id: current_user.id)
@student.department_id = 1
@student.save

如果您在用户和学生之间建立了关联,那么这将更加美观。 大致如下:
@student = current_user.student || current_user.build_student
@student.department_id = 1
@student.save

编辑:

你也可以使用http://guides.rubyonrails.org/active_record_querying.html#first_or_create,就像sevenseacat所回答的那样,但你仍然需要处理不同的情况,比如更新学生的部门ID。

更新:

你可以使用find_or_create_by

@student = Student.find_or_create_by(user_id: current_user.id) do |student|
  student.department_id = 1
end

6

在Rails 5中

Student.
  find_or_initialize_by(:user_id => current_user.id).
  update(:department => 1)

(所有功劳归功于@Zorayr的回答。)

0

不幸的是,我认为最干净的方法是:

Student.where(user_id: id).first_or_create(age: 16).update_attribute(:age, 16)

-4

应该使用find_or_create_by而不是first_or_create
例如: Client.find_or_create_by(first_name: 'Andy')


first_or_create 确实存在(即使是一年前),我不确定你在这个答案中想要表达什么,但听起来像是对另一个帖子的评论。 - Paul Richter
两者的工作方式略有不同。Model.where(find: 'find_value').first_or_create(create: 'create_value') 等同于 Model.find_or_create_by(find: 'find_value') { |r| r.create = 'create_value' } - PJSCopeland

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接