[RUBY] [Rails / ActiveRecord] I want to validate the value before the type is converted (_before_type_cast)

Introduction

In ActiveRecord, if you put a character string in a Date type or Integer type column, it will be automatically converted to that column type.

For example, if you have the following users table: (Created_at and updated_at are omitted for simplicity.)

   create_table "users", force: :cascade do |t|
    t.string "name"
    t.integer "age"
  end

Let's try entering various values using the Rails console.

 irb (main): 001: 0> User.new (name: "Taro", age: 21) Try putting a number in #age
 => # <User name: "Taro", age: 21>

 irb (main): 002: 0> User.new (name: "Taro", age: "21") Try putting a character string in #age
 => # <User name: "Taro", age: 21>

 irb (main): 003: 0> User.new (name: "Taro", age: "21") Try putting a character string in #age (1 is a full-width number)
 => # <User name: "Taro", age: 2>

The first is a numerical value. Since it is a numerical value, it can be entered as it is.

The second is a string. The string "21" has been converted to the number 21. Wonderful.

How about the third one, the 1 part of "21" is a full-width number. It has become age: 2. It's completely different from 21.

By the way, even if you use "2 1" (with a space between them) or "2 a" (with a character other than a number), it will be age: 2.

What's wrong

Suppose you try to play a non-numeric input and apply the following validation.

  validates :age, format: { with: /\A[0-9]+\z/}

In this case, if the user inputs "2 1", "2 a", etc., validation will be applied to 2 of age, and it will be saved as correct data.

Solution "\ _before_type_cast"

The solution is to add _before_type_cast to age to validate the value entered by the user instead of the value after the type has been converted. [* Reference "before_type_cast with rails / validates"](https://dora.bk.tsukuba.ac.jp/~takeuchi/?%E3%82%BD%E3%83%95%E3%83%88%E3 % 82% A6% E3% 82% A7% E3% 82% A2% 2Frails% 2Fvalidates% E3% 81% A7before_type_cast)

  validates :age_before_type_cast, format: { with: /\A[0-9]+\z/ }, presence: true

Applications (such as string conversion)

In addition to validation, _before_type_cast is also useful when you want to convert alphanumeric characters in a character string from full-width to half-width, or when you want to save after removing spaces.

As an example, the following code uses a gem called Moji to convert from full-width to half-width.

This time, I use _before_type_cast because I want to align the input that is a mixture of full-width and half-width such as "21" (1 is full-width) of the user to half-width before validation.

After receiving the character string as it is input by the user with age_before_type_cast and converting it, the converted character string is put in 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

The nature of _before_type_cast

Difference between 〇〇 and 〇〇_before_type_cast

First is the difference between normal attributes and attributes with _before_type_cast. The value cannot be obtained with [: 〇〇_before_type_cast]. In particular

 irb (main): 001: 0> user = User.new (name: "Taro", 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 

You can get the value with user.age_before_type_cast, When I try to get the value using user [: age_before_type_cast], nil is returned.

〇〇_before_type_cast cannot be updated

irb(main):001:0> user.age = 21
=> 21
irb(main):002:0> user.age_before_type_cast = 21
 => #NoMethodError occurs.

Since _before_type_cast is for checking the input before type conversion to age, of course it is not possible to update itself.

What happens after save

Also, if you save, 〇〇 and 〇〇_before_type_cast will be the same. In particular,

 irb (main): 001: 0> user = User.new (name: "Taro", age: "21")
irb(main):002:0> user.save
irb(main):003:0> user.age_before_typecast
=> 21

The number 21 is returned instead of the string "21".

Be careful when implementing or testing because of the above properties!

Reference article

[before_type_cast with rails / validates](https://dora.bk.tsukuba.ac.jp/~takeuchi/?%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6 % E3% 82% A7% E3% 82% A2% 2Frails% 2Fvalidates% E3% 81% A7before_type_cast)

Recommended Posts

[Rails / ActiveRecord] I want to validate the value before the type is converted (_before_type_cast)
I want to get the value in Ruby
[Rails] I want to reset everything because the data in the local environment is strange! What to do before that
I want to create a form to select the [Rails] category
I tried to understand how the rails method "redirect_to" is defined
I tried to understand how the rails method "link_to" is defined
I want to change the value of Attribute in Selenium of Ruby
[Ruby] I want to extract only the value of the hash and only the key
I want to introduce the committee with Rails without getting too dirty
[Rails] [Parent-child relationship] I want to register the foreign key in the child with nil when the parent is deleted.
I want to get only the time from Time type data ...! [Strftime] * Additional notes
[Rails] I tried to raise the Rails version from 5.0 to 5.2
I tried to organize the session in Rails
I want to var_dump the contents of the intent
The code I used to connect Rails 3 to PostgreSQL 10
I want to play with Firestore from Rails
I want to truncate after the decimal point
[Rails] I want to load CSS with webpacker
Rails The concept of view componentization of Rails that I want to convey to those who want to quit
[Rails] I want to display the link destination of link_to in a separate tab
I want to hit the API with Rails on multiple docker-composes set up locally
[Java] I want to calculate the difference from the date
I want to embed any TraceId in the log
I want to judge the range using the monthly degree
I want to use a little icon in Rails
I want to know the answer of the rock-paper-scissors app
[Rails] I don't know how to use the model ...
I want to display the name of the poster of the comment
I want to dark mode with the SWT app
I want to authenticate users to Rails with Devise + OmniAuth
I want to define a function in Rails Console
I want to call the main method using reflection
I want to add a reference type column later
[Rough commentary] I want to marry the pluck method
I want to be aware of the contents of variables!
I want to return the scroll position of UITableView!
I want to simplify the log output on Android
I want to create a generic annotation for a type
I want to add a delete function to the comment function
[Rails] Validate the start time (datetime type) and end time
I want to find out which version of java the jar file I have is available
After posting an article with Rails Simple Calendar, I want to reflect it in the calendar.
How to solve the problem when the value is not sent when the form is disabled in rails and sent
I want to return a type different from the input element with Java8 StreamAPI reduce ()
I want to find out if the specified character string is supported by the target character code
[Beginner] I want to modify the migration file-How to use rollback-
I tried to introduce Bootstrap 4 to the Rails 6 app [for beginners]
I want to set the conditions to be displayed in collection_check_boxes
I want to use screen sharing on the login screen on Ubuntu 18
(´-`) .. oO (I want to easily find the standard output "Hello".
I want to push an app made with Rails 6 to GitHub
I want to bring Tomcat to the server and start the application
I want to expand the clickable part of the link_to method
I want to make a specific model of ActiveRecord ReadOnly
I want to change the log output settings of UtilLoggingJdbcLogger
I want to call a method and count the number
I want to use the Java 8 DateTime API slowly (now)
I want to use the sanitize method other than View.
[Rails] I want to display "XX minutes ago" using created_at!
I want to put the JDK on my Mac PC
I want to give a class name to the select attribute