#はじめに
ActiveRecordではDate型やInteger型のカラムに文字列を入れると自動でそのカラムの型に変換してくれます。
例えば以下のようなusersテーブルがあったとします。(簡単の為にcreated_atやupdated_atは省略してます。)
create_table "users", force: :cascade do |t|
t.string "name"
t.integer "age"
end
試しにRailsコンソールを使っていろいろな値を入れてみましょう。
irb(main):001:0> User.new(name: "太郎", age: 21) #ageに数値を入れてみる
=> #<User name: "太郎" , age: 21>
irb(main):002:0> User.new(name: "太郎", age: "21") #ageに文字列を入れてみる
=> #<User name: "太郎", age: 21>
irb(main):003:0> User.new(name: "太郎", age: "21") #ageに文字列を入れてみる(1は全角数字)
=> #<User name: "太郎", age: 2>
1つ目は数値を入れています。数値なのでそのまま入りますね。
2つ目は文字列を入れています。文字列の"21"が数値の21に変換されていますね。素晴らしいです。
3つ目はどうでしょうか、"21"の1の部分は全角数字です。
なんと、age: 2になってしまっています。21とは全然違いますね。
ちなみに"2 1"(間にスペースを入れる)や"2ア"(数字以外の文字を入れる)などしても、age: 2となります。
##何が困るのか
数字以外の入力を弾こうとして以下のようなバリデーションをかけたとします。
validates :age, format: { with: /\A[0-9]+\z/}
この場合、ユーザーが"2 1", "2ア"などの入力をするととageの2に対してバリデーションがかかることになり、正しいデータとして保存されてしまいます。
解決策 「_before_type_cast」
解決策として、ageに_before_type_castをつけると型が変換された後の値ではなくユーザーの入力した値に対して、バリデーションをかけることができます。※参考「rails/validatesでbefore_type_cast」
validates :age_before_type_cast, format: { with: /\A[0-9]+\z/ }, presence: true
応用(文字列の変換など)
_before_type_castはバリデーションの他に、文字列の英数字を全角から半角に変換したり、空白を取り除いてから保存したい場合などにも便利です。
例として以下のコードではMojiというgemを使って全角→半角の変換をしています。
今回はユーザーの“21”(1は全角)などの全角と半角が混じった入力をバリデーション前に半角に揃えたいので_before_type_castを使います。
age_before_type_castでユーザーの入力したそのままの文字列を受け取って変換した後、ageに変換した文字列を入れています。
validates :age_before_type_cast, format: { with: /\A[0-9]+\z/ }, presence: true
before_validation do
string = self.send(:age_before_type_cast)
string = Moji.normalize_zen_han(string)
send(:write_attribute, :age, string)
end
_before_type_castの性質
###〇〇と〇〇_before_type_castの違い
まず通常の属性と_before_type_castのついた属性の違いです。
[:〇〇_before_type_cast]では値が取得できません。
具体的には
irb(main):001:0> user = User.new(name: "太郎", age: "21")
irb(main):002:0> user.age
=> 21
irb(main):003:0> user.age_before_typecast
=> "21"
irb(main):004:0> user[:age]
=> 21
irb(main):005:0> user[:age_before_typecast]
=> nil
user.age_before_type_castなら値が取得できますが、
user[:age_before_type_cast]を使って値を取得しようとするとnilが返ってきてしまいます。
〇〇_before_type_castの更新はできない
irb(main):001:0> user.age = 21
=> 21
irb(main):002:0> user.age_before_type_cast = 21
=> #NoMethodErrorが発生します。
_before_type_castはageへの型変換前の入力を確認するためのものなので、それ自体を更新することはもちろんできないですね。
save後はどうなるか
また、saveすると〇〇と〇〇_before_type_castは同じになります。
具体的には、
irb(main):001:0> user = User.new(name: "太郎", age: "21")
irb(main):002:0> user.save
irb(main):003:0> user.age_before_typecast
=> 21
文字列の"21"ではなく、数値の21が返ってきています。
以上のような性質があるので実装やテストの際は気をつけましょう!
#参考記事