如何让 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_name
和 last_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
我正在使用 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_name
和 last_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