【问题标题】:Rails callbacks behaving differently on different environmentsRails 回调在不同的环境中表现不同
【发布时间】:2017-10-13 10:06:07
【问题描述】:

我有两个 Rails 环境。一个运行 Postgres 和 Rails 5.0.6 的开发环境和一个几乎相同的 Heroku 环境。

我有一个Administrator 类,它根据用户的forenamesurname 字段在before_save 回调上为Administrator 生成用户名。

class Administrator < ApplicationRecord

  validates :username, uniqueness: true
  validates :forename, presence: true
  validates :surname, presence: true

  before_save :generate_username

  def generate_username
    return if username.present?
    proposed = "#{forename}#{surname}".downcase
    existing_count = Administrator.where("username ILIKE ?", "#{proposed}%").size
    self.username = existing_count.zero? ? proposed : "#{proposed}#{existing_count}"
  end
end

验证用户后,会生成FORENAMESURNAMEX 形式的用户名,其中 X 是递增数字(或无)。

这是我在开发机器上的 Rails 控制台中运行的命令。

irb(main):012:0> Administrator.create(email: 'edward@test.net', forename: 'Edward', surname: 'Scissorhands')
D, [2017-10-13T10:00:18.985765 #280] DEBUG -- :    (0.2ms)  BEGIN
D, [2017-10-13T10:00:18.987554 #280] DEBUG -- :   Administrator Exists (0.5ms)  SELECT  1 AS one FROM "administrators" WHERE "administrators"."email" = $1 LIMIT $2  [["email", "edward@test.net"], ["LIMIT", 1]]
D, [2017-10-13T10:00:18.988923 #280] DEBUG -- :   Administrator Exists (0.4ms)  SELECT  1 AS one FROM "administrators" WHERE "administrators"."username" IS NULL LIMIT $1  [["LIMIT", 1]]
D, [2017-10-13T10:00:18.990155 #280] DEBUG -- :    (0.4ms)  SELECT COUNT(*) FROM "administrators" WHERE (username ILIKE 'edwardscissorhands%')
D, [2017-10-13T10:00:18.992000 #280] DEBUG -- :   SQL (0.5ms)  INSERT INTO "administrators" ("email", "created_at", "updated_at", "username", "forename", "surname") VALUES ($1, $2, $3, $4, $5, $6) RETURNING "id"  [["email", "edward@test.net"], ["created_at", "2017-10-13 10:00:18.990421"], ["updated_at", "2017-10-13 10:00:18.990421"], ["username", "edwardscissorhands"], ["forename", "Edward"], ["surname", "Scissorhands"]]
D, [2017-10-13T10:00:18.995845 #280] DEBUG -- :    (1.8ms)  COMMIT
=> #<Administrator id: 10, email: "edward@test.net", created_at: "2017-10-13 10:00:18", updated_at: "2017-10-13 10:00:18", role: nil, otp_public_key: nil, username: "edwardscissorhands", forename: "Edward", surname: "Scissorhands">

如您所见,回调被执行,用户的用户名按预期生成并持久化到数据库中。

但是,当我在 Heroku(和 Heroku Postgres)上运行的测试环境中运行相同的代码时,会发生以下情况:

irb(main):005:0> Administrator.create!(email: 'edward@test.net', forename: 'Edward', surname: 'Scissorhands')
   (1.9ms)  BEGIN
  Administrator Exists (1.1ms)  SELECT  1 AS one FROM "administrators" WHERE "administrators"."email" = $1 LIMIT $2  [["email", "edward@test.net"], ["LIMIT", 1]]
  Administrator Exists (0.9ms)  SELECT  1 AS one FROM "administrators" WHERE "administrators"."username" IS NULL LIMIT $1  [["LIMIT", 1]]
   (0.9ms)  ROLLBACK
ActiveRecord::RecordInvalid: Validation failed: Username has already been taken

(我在这里使用create! 而不是create 来显示开发中不会出现的验证错误。)

我不明白为什么环境之间的行为应该不同。两者都运行相同版本的 Rails (5.0.6) 并且运行相同的代码库。

【问题讨论】:

  • 这是错字还是出现在您的代码中? Administrator.where("username ILIKE ?", "#{proposed}%").size ILIKE 应该是 LIKE
  • ILIKE 是 LIKE 但不区分大小写。

标签: ruby-on-rails ruby postgresql heroku


【解决方案1】:

在验证后调用before_save,因此出现错误。

改用 before_validation。

这里是创建对象时调用的顺序回调作为参考:

  • before_validation
  • after_validation
  • before_save
  • around_save
  • before_create
  • around_create
  • after_create
  • after_save
  • after_commit/after_rollback

【讨论】:

  • 是的,这是正确的。事实证明,我们有脏测试数据导致验证失败,因为我们为我们的情况选择了错误的回调。谢谢。
【解决方案2】:

您的代码中的逻辑有缺陷。这是一个合法的错误;你需要重新设计用户名生成词的方式。

例如,假设您的系统中有一个名为:edwardscissorhands1 的用户。没有edwardscissorhands,也没有edwardscissorhands2/3/4等。

这行:Administrator.where("username ILIKE ?", "edwardscissorhands%").size 返回1,然后你的逻辑尝试创建一个已经存在的新用户。

...如果没有看到实际数据,我无法确定您的生产服务器上发生了什么,但我敢打赌它是这样的。它可能会稍微复杂一些,例如用户:tomtom3tomlord 存在;因此,您的逻辑会尝试创建第二个 tom3 用户。

例如,如果您生成了一些 edwardscissorhards 用户,然后删除其中一个或多个用户,则可能会发生这种情况。

作为一个示例,您可以通过以下方式重新设计逻辑:

def generate_username
  return if username.present?
  proposed = "#{forename}#{surname}".downcase
  return proposed unless Administrator.exists?("username ILIKE ?", proposed)

  counter = 1
  while(Administrator.exists?("username ILIKE ?", "#{proposed}#{counter}"))
    counter += 1
  end

  "#{proposed}#{counter}"
end

这可能会在性能方面得到改进,尽管此处的多个数据库查询不太可能成为实际应用程序中的主要问题(假设您没有 很多 个具有相同名称的管理员!)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-28
    相关资源
    最近更新 更多