如何让 AR 验证发生在我的模型上的模块扩展之前?

How do I let an AR validation happen before a module extension on my model?

我正在使用 FriendlyID,它会在创建时根据记录的某些属性创建一个 slug。

  extend FriendlyId
  friendly_id :name_and_school, use: :slugged

  def name_and_school
    "#{first_name} #{last_name} #{school.name}"
  end

我的模型上也有:

  validates :first_name, :last_name, presence: true
  validates_associated :school, presence: true

但是,当我去测试此验证并提交一个 first_name, last_name and school 值为空的表单时,我收到以下错误:

  Position Load (0.8ms)  SELECT "positions".* FROM "positions" WHERE 1=0
   (0.8ms)  BEGIN
   (0.6ms)  ROLLBACK
Completed 500 Internal Server Error in 51ms (ActiveRecord: 11.7ms)



NoMethodError - undefined method `name' for nil:NilClass:
  app/models/profile.rb:79:in `name_and_school'
  friendly_id (5.1.0) lib/friendly_id/slugged.rb:295:in `should_generate_new_friendly_id?'
  friendly_id (5.1.0) lib/friendly_id/slugged.rb:304:in `set_slug'

所以很明显,它正在触发 name_and_school 方法,甚至在它触发 ActiveRecord 的验证检查之前。

如果它首先通过验证检查,则不会生成此错误,它只会重新加载包含相应错误的页面。

那么我该如何解决这个问题并确保仅在所有验证都通过时才生成 FriendlyID slug?

编辑 1

所以我试着坚持认为 name_and_school 只有 returns 一个有效的字符串,如果值是这样的话:

  def name_and_school
    "#{first_name} #{last_name} #{school.name}" if first_name.present? && last_name.present? && school.present?
  end

这现在适用于 empty/invalid first_namelast_name 属性。

但是,当我将学校字段留空时,现在出现以下错误:

Completed 500 Internal Server Error in 31689ms (Searchkick: 9.5ms | ActiveRecord: 23.6ms)

NoMethodError - undefined method `name' for nil:NilClass:
  app/models/profile.rb:139:in `search_data'
  searchkick (1.4.0) lib/searchkick/index.rb:289:in `search_data'
  searchkick (1.4.0) lib/searchkick/index.rb:66:in `block in bulk_index'
  searchkick (1.4.0) lib/searchkick/index.rb:66:in `bulk_index'
  searchkick (1.4.0) lib/searchkick/index.rb:54:in `store'
  searchkick (1.4.0) lib/searchkick/logging.rb:28:in `block in store'
  activesupport (5.0.0.1) lib/active_support/notifications.rb:164:in `block in instrument'
  activesupport (5.0.0.1) lib/active_support/notifications/instrumenter.rb:21:in `instrument'
  activesupport (5.0.0.1) lib/active_support/notifications.rb:164:in `instrument'
  searchkick (1.4.0) lib/searchkick/logging.rb:27:in `store'
  searchkick (1.4.0) lib/searchkick/index.rb:96:in `reindex_record'
  searchkick (1.4.0) lib/searchkick/model.rb:113:in `reindex'
  app/models/profile.rb:146:in `reindex_profile'

在我的模型中对应的是:

after_commit :reindex_profile

  def search_data
    {
      name: name,
      bib_color: bib_color,
      height: height,
      weight: weight,
      player_type: player_type,
      school_name: school.name,
      age: age,
      position_name: positions.map(&:name)
    }
  end

  def reindex_profile
    reindex
  end

提交新记录后,它会运行该回调——它需要一个有效的 school 属性。

因此 nil 值仍在逃避 validates_associated 检查。

我什至尝试将相同的 if 语句添加到 reindex_profile 方法中,如下所示:

  def reindex_profile
    reindex if first_name.present? && last_name.present? && school.present? && bib_color.present? && player_type.present? && age.present?
  end

但这也不管用。

编辑 2

尝试@codyeatworld 的回答后,我们正在取得进展。

现在的问题是,即使我不再收到这些错误,尽管 validates_associated :school, presence: true 不正确,但仍在创建记录。

例如,这里是一个请求的例子:

Started POST "/profiles" for ::1 at 2016-11-13 16:32:19 -0500
Processing by ProfilesController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"skJA+9NB3XhR/JLgQ==", "profile"=>{"avatar"=>#<ActionDispatch::Http::UploadedFile:0x007fbf3b35d9a8 @tempfile=#<Tempfile:/var/folders/0f/hgplttnd7d/T/RackMultipart20161113-16651-1wvdzdx.jpg>, @original_filename="Random-Image.jpg", @content_type="image/jpeg", @headers="Content-Disposition: form-data; name=\"profile[avatar]\"; filename=\"Random-Image.jpg\"\r\nContent-Type: image/jpeg\r\n">, "first_name"=>"Random", "last_name"=>"Brown", "dob(3i)"=>"13", "dob(2i)"=>"11", "dob(1i)"=>"1986", "weight"=>"", "height"=>"", "bib_color"=>"", "player_type"=>"player", "position_ids"=>[""], "school_id"=>"", "grade"=>"", "tournament_ids"=>[""], "email"=>"", "cell_phone"=>"", "home_phone"=>"", "grades_attributes"=>{"0"=>{"subject"=>"", "result"=>"", "grade_type"=>"csec", "_destroy"=>"false"}}, "transcripts_attributes"=>{"0"=>{"url_cache"=>"", "_destroy"=>"false"}}, "achievements_attributes"=>{"0"=>{"body"=>"", "achievement_type"=>"academic", "_destroy"=>"false"}}, "videos_attributes"=>{"0"=>{"vimeo_url"=>"", "official"=>"", "_destroy"=>"false"}}, "articles_attributes"=>{"0"=>{"source"=>"", "title"=>"", "url"=>"", "_destroy"=>"false"}}}, "commit"=>"Create Profile"}
  User Load (1.8ms)  SELECT  "users".* FROM "users" WHERE "users"."id" =  ORDER BY "users"."id" ASC LIMIT   [["id", 2], ["LIMIT", 1]]
  Role Load (1.8ms)  SELECT "roles".* FROM "roles" INNER JOIN "users_roles" ON "roles"."id" = "users_roles"."role_id" WHERE "users_roles"."user_id" =  AND (((roles.name = 'admin') AND (roles.resource_type IS NULL) AND (roles.resource_id IS NULL)))  [["user_id", 2]]
  Role Load (1.3ms)  SELECT "roles".* FROM "roles" INNER JOIN "users_roles" ON "roles"."id" = "users_roles"."role_id" WHERE "users_roles"."user_id" =  AND (((roles.name = 'coach') AND (roles.resource_type IS NULL) AND (roles.resource_id IS NULL)))  [["user_id", 2]]
  Role Load (1.3ms)  SELECT "roles".* FROM "roles" INNER JOIN "users_roles" ON "roles"."id" = "users_roles"."role_id" WHERE "users_roles"."user_id" =  AND (((roles.name = 'player') AND (roles.resource_type IS NULL) AND (roles.resource_id IS NULL)))  [["user_id", 2]]
   (1.7ms)  SELECT COUNT(*) FROM "roles" INNER JOIN "users_roles" ON "roles"."id" = "users_roles"."role_id" WHERE "users_roles"."user_id" =  AND (((roles.name = 'admin') AND (roles.resource_type IS NULL) AND (roles.resource_id IS NULL)) OR ((roles.name = 'coach') AND (roles.resource_type IS NULL) AND (roles.resource_id IS NULL)))  [["user_id", 2]]
The "mime_type" Shrine metadata field will be set from the "Content-Type" request header, which might not hold the actual MIME type of the file. It is recommended to load the determine_mime_type plugin which determines MIME type from file content.
  Tournament Load (1.2ms)  SELECT "tournaments".* FROM "tournaments" WHERE 1=0
  Position Load (0.8ms)  SELECT "positions".* FROM "positions" WHERE 1=0
   (0.9ms)  BEGIN
  SQL (2.2ms)  INSERT INTO "profiles" ("first_name", "last_name", "dob", "bib_color", "created_at", "updated_at", "player_type", "grade", "home_phone", "cell_phone", "email", "avatar_data") VALUES (, , , , , , , , , , , ) RETURNING "id"  [["first_name", "Yashin"], ["last_name", "Brown"], ["dob", Thu, 13 Nov 1986], ["bib_color", ""], ["created_at", 2016-11-13 21:32:19 UTC], ["updated_at", 2016-11-13 21:32:19 UTC], ["player_type", 0], ["grade", ""], ["home_phone", ""], ["cell_phone", ""], ["email", ""], ["avatar_data", "{\"id\":\"83d162a3d33bdc5d2527502e1d423ab3.jpg\",\"storage\":\"cache\",\"metadata\":{\"filename\":\"Random-Image.jpg\",\"size\":61798,\"mime_type\":\"image/jpeg\"}}"]]
  SQL (2.0ms)  INSERT INTO "transcripts" ("profile_id", "created_at", "updated_at") VALUES (, , ) RETURNING "id"  [["profile_id", 30], ["created_at", 2016-11-13 21:32:19 UTC], ["updated_at", 2016-11-13 21:32:19 UTC]]
  Profile Load (3.7ms)  SELECT  "profiles".* FROM "profiles" WHERE "profiles"."id" =  LIMIT   [["id", 30], ["LIMIT", 1]]
   (1.6ms)  COMMIT
   (0.9ms)  BEGIN
  SQL (2.6ms)  UPDATE "profiles" SET "updated_at" = , "avatar_data" =  WHERE "profiles"."id" =   [["updated_at", 2016-11-13 21:32:19 UTC], ["avatar_data", "{\"id\":\"ec63dcc18ed5d60aa7a6626550f9f9ea.jpg\",\"storage\":\"store\",\"metadata\":{\"filename\":\"Random-Image.jpg\",\"size\":61798,\"mime_type\":\"image/jpeg\"}}"], ["id", 30]]
   (1.3ms)  COMMIT
  Profile Store (311.8ms)  {"id":30}
  Profile Store (32.2ms)  {"id":30}
  Profile Store (26.7ms)  {"id":30}
  Profile Store (18.9ms)  {"id":30}
Redirected to http://localhost:3000/profiles/30
Completed 302 Found in 530ms (Searchkick: 389.6ms | ActiveRecord: 32.2ms)

注意 "school_id"=>"",还要注意 URL 现在是 profiles/30(而不是 FriendlyID URL,这意味着 friendlyID slug 没有执行是我想要的)。

为什么尽管我对模型进行了 validates_associated 调用,但仍然创建了这条记录?

那我想你在找should_generate_new_friendly_id

def should_generate_new_friendly_id?
  first_name.present? && last_name.present? && school.present?
end

FriendlyId 只会在满足这些条件时尝试创建 slug。

我认为 searchkick 中的违规行是 school.name 属性。

def search_data
  {
    name: name,
    bib_color: bib_color,
    height: height,
    weight: weight,
    player_type: player_type,
    # school_name: school.name,
    school_name: (school.present? ? school.name : nil),
    age: age,
    position_name: positions.map(&:name)
  }
end

我假设您想验证 school 是否存在。方法 validates_associated 将 运行 验证 school object/model。它不检查 school_id.

是否存在
validates :first_name, :last_name, :school_id, presence: true
# Remove validates_associated :school, presence: true

我想你也可以省略 _id:

belongs_to :school
validates :school, presence: true