티스토리 뷰
이 글은 Rails 5.0 Guide 기준으로 작성됩니다.
https://guides.rubyonrails.org/v5.0/active_record_validations.html
- Active Record Validations Intro
데이터베이스에 데이터가 저장되기 전, Active Record를 통해 유효성 검사를 하는 과정을 알아보겠습니다.
- Validations Overview
class Bulletin < ApplicationRecord
validates :title, presence: true
end
Bulletin.create(title: 'Good').valid? # => true
Bulletin.create(title: '').valid? # => false
Bulletin.create(title: nil).valid? # => false
- 만약 위와같은 Bulletin 모델에 validate 조건이 있는 상태에서 데이터를 생성한다 할 때,
- title에 내용이 있냐/비어있냐/아예 초기화 조차 안되어있냐(NULL) 상태로 데이터를 생성한다 할 경우,
- 내용이 온전이 써져있을 경우 데이터가 잘 작성이 되면서 true를 반환하나,
- 내용이 비어있거나 NULL 상태인 경우 rollback 결과와 함께 false를 반환합니다.
유효성 검사(validate)는 데이터가 저장되기 전, validate 조건에 맞는 데이터에 대해서만 저장이 되도록 도와줍니다.
또한 Chapter 3 때 언급되었던 데이터베이스의 조건 중, 참조 무결성 조건도 지켜나갈 수 있다는 장점이 있습니다.
데이터베이스가 저장(Insert), 수정(Update)에 있어 유효성 검사(Validataion)을 하는 메소드는 아래와 같습니다.
create
create!
save
save!
update
update!
또한, 여기서 bang(!) 이 붙는 메소드 같은 경우에는, 기존의 메소드와 차이가 있습니다. :
1) Bulletin Model에 아래와 같은 유효성 검사 조건을 추가 후
class Bulletin < ApplicationRecord
validates :title, presence: true
end
2) create 및 create! 로 데이터를 생성을 시도할 시 아래와 같은 결과를 볼 수 있습니다.
- create 메소드 같은 경우에는 만약 데이터 생성도중 유효성 검사에 걸릴 시 boolean이 반환되나,
- create! 메소드는 boolean을 반환하지 않고, 아예 에러를 반환합니다.
하지만 또 일부 메소드는 유효성검사를 무시하는게 있다보니 이 부분은 유의해야 할 부분이 있습니다.
decrement!
decrement_counter
increment!
increment_counter
toggle!
touch
update_all
update_attribute
update_column
update_columns
update_counters
valid? and invalid?
Active Record의 객체를 통해 데이터가 저장되기 전, validation 측에서 데이터 검증이 일어나고, 조건에 맞지 않는 데이터(유효하지 않은 데이터)일 경우, 에러메세지를 남깁니다.
간단하게, 다음 예시를 통해 valid? 메소드에 대해 살펴봅시다.
class Bulletin < ApplicationRecord
validates :title, presence: true
end
valid? 메소드가 작동된 후, 만약 유효성 검사에서 false가 리턴된 경우, 에러메세지를 내부적으로 확인할 수 있습니다.
하지만, new 메소드를 통해 에러메세지를 확인을 시도할 경우 에러메세지 확인이 안됩니다.
create와는 다르게 인스턴스화에 있어 new 메소드를 쓸 경우에는 바로 Transaction 과정을 거쳐서 Database에 저장을 시키는 메소드가 아니다보니 아래와 같이 에러메세지를 반환하지는 않습니다.
errors[]
객체 내 컬럼이 유효성 검사를 함에 있어, 만약 에러가 발견될 경우, errors[:ATTRITUBE] 메소드를 통해 알아낼 수 있습니다.
에러의 원인을 알고 싶다면 detail[:ATTRITUBE] 메소드를 통해 알아낼 수 있습니다.
- Validation Helpers
acceptance
class Bulletin < ApplicationRecord
validates :condition, acceptance: true
end
컬럼 내 boolean 여부에 대해 검사합니다.
모델에서 명시된 Column이 nil(NULL) 혹은 true 형태 이어야만 데이터 저장이 됩니다.
validates_associated
class Bulletin < ApplicationRecord
has_many :images
validates_associated :images
end
2개의 Model 관계에 있어, validates_associated 옵션이 있는 Model에서 봤을 때, 다른 연계 Model에서 데이터 저장 시 유효성 조건에 맞게 저장됐는지 검사합니다. (has_many / belongs_to 관계 구분은 상관 없음.)
⚠️ 주의 하나의 Model에 대해서만 validates_associated 옵션을 줄 것.
Model 둘 다에 이 옵션을 줄 경우 무한루프 에러가 발생합니다.
confirmation
class Bulletin < ApplicationRecord
validates :pwd, confirmation: true
end
모델을 통해 데이터를 저장함에 있어, 재입력을 통해 확인이 필요할 때 사용되는 검사입니다.
⚠️ 해당 유효성 검사는 Strong Parameter과 연계되어 사용되어야 합니다.
## Good
class BulletinsController < ApplicationController
before_action :bulletins_params, :only => [:create]
def create
Bulletin.create(bulletins_params)
end
private
def bulletins_params
params.require(:bulletin).permit(:title, :content, :pwd, pwd_confirmation)
end
end
## Bad
class BulletinsController < ApplicationController
def create
Bulletin.create(title: params[:bulletin][:title], content: params[:bulletin][:content], pwd: params[:bulletin][:pwd])
end
end
exclusive
validates :title, exclusion: { in: %w(naver kakao nate),
message: "%{value} is reserved." }
validates :boolean_field_name, exclusion: { in: [nil] }
모델을 통해 데이터를 저장함에 있어, 내용 속에 사전에 validate에 설정한 예약어가 없어야 합니다.
하지만 예약어+다른 단어 조합으로 사용할 경우, 유효성 검사 결과(valid?)에서는 true 입니다.
inclusion
validates :level, inclusion: { in: %w(level1 level2 level3),
message: "%{value} is not a valid level" }
validates :boolean_field_name, inclusion: { in: [true, false] }
exclusive와는 반대로, 내용 속에 사전에 validate에 설정한 예약어가 있어야 합니다.
예약어+다른 단어 조합으로 사용할 경우, 유효성 검사 결과(valid?)에서는 false 입니다.
format
class Bulletin < ApplicationRecord
validates :title, format: { with: /\A[가-힣]+\z/,
message: "only allows letters" }
end
params[:ATTRIBUTE] 데이터에서 정규식 패턴을 분석해서 정규식 조건에 맞는 단어인지 확인합니다.
with에 명시된 조건의 내용이 아닐 경우, 데이터 작성(insert)이 안됩니다.
length
class Person < ApplicationRecord
validates :name, length: { minimum: 2 }
validates :bio, length: { maximum: 500 }
validates :password, length: { in: 6..20 }
validates :registration_number, length: { is: 6 }
end
모델을 통해 데이터를 저장함에 있어, 글자수에 대해 검사합니다. 조건이 맞을 경우 데이터가 작성(insert)됩니다.
- minimum n : 최소 조건 글자수
- maximum n : 최대 조건 글자수
- in n..m : n~m 범위 내 글자 수
- is n : 꼭 n글자여야 한다.
또한, 글자수가 너무 많거나 적을 경우, 그에 맞는 에러메세지 반환 또한 커스터마이징을 할 수 있습니다.
class Person < ApplicationRecord
validates :name, length: { minimum: 2, too_short: "%{count} characters is the minimum allowed" }
validates :bio, length: { maximum: 500, too_long: "%{count} characters is the maximum allowed" }
end
numericality
## 숫자(integer) 및 소수(음수 포함)만 허용
validates :title, numericality: true
## 숫자(integer)만 허용
validates :title, numericality: { only_integer: true }
모델을 통해 데이터를 저장함에 있어, 숫자 및 소수(음수 포함)이 있는지 검사합니다. 조건이 맞을 경우 데이터가 작성(insert)됩니다.
presence
form 안에 내용이 존재하는지 검사합니다.
validates :name, :login, :email, presence: true
inverse_of
class Bulletin < ApplicationRecord
has_many :images, inverse_of: :bulletin
end
class Image < ApplicationRecord
belongs_to :bulletin, inverse_of: :images
end
## 아래 명령어들은 inverse_of 옵션이 없어도 똑같은 결과를 볼 수 있습니다.
Bulletin.reflect_on_association(:images).has_inverse?
# => :bulletin
Image.reflect_on_association(:bulletin).has_inverse?
# => :images
외래키로 연계된 두 테이블의 관계에 있어 데이터를 참조 시, 사전에 Memory에 저장시켜 놓음으로서 메모리 최적화에 도움을 줍니다.
참고 해당 옵션은 Rails 4.1부터 기본 옵션으로 제공되는 기능입니다. 외래키 이름이 모델 이름과 같다면, 저희가 따로 설정을 안해줘도 될 듯 합니다.
부록 Exploring the `:inverse_of` Option on Rails Model Associations
absence
validates :title, absence: true
데이터를 저장함에 있어, 지목된 컬럼에 내용이 없어야 합니다.
uniqueness
validates :student_code, uniqueness: true
테이블에 데이터가 입력됨에 있어, uniqueness 옵션이 걸린 컬럼이 테이블에서 유일한 값인지 확인합니다.
만약 여러개의 컬럼 중 하나의 컬럼에 대해서만 uniqueness 옵션을 걸어야 할 경우, 다음과 같이 입력하면 됩니다.
class Holiday < ApplicationRecord
validates :name, uniqueness: { scope: :year,
message: "should happen once per year" }
end
validates_with
## app/models/bulletin.rb
class Bulletin < ApplicationRecord
validates_with GoodnessValidator
end
## app/models/goodness_validator.rb
class GoodnessValidator < ActiveModel::Validator
def validate(record)
if record.title == "Evil"
record.errors[:base] << "This person is evil"
end
byebug
end
end
참고 goodness_validator.rb 파일 내에 객체를 전달받을 때, 해당 객체는 이전 Model에서 전달받은 데이터 객체값 입니다.
validates_each
Block 단위로 에러에 대해 처리를 합니다. (여러개의 에러 상황에 대해 처리하기 좋습니다.)
validates_each :title do |record, attr, value|
record.errors.add(attr, 'only allows Korean') if value =~ /\A[가-힣]+\z/
end
- Common Validation Options
:allow_nil
## 5글자 입력 혹은 NULL일 경우에만 데이터 저장 허용
class Bulletin < ApplicationRecord
validates :title, length: { is: 5 }, allow_nil: true
end
(validates 제약조건이 있더라도) NULL에 대해서만 데이터 작성이 허용됩니다.
참고 1 Empty에 대해서는 허용을 안합니다.
참고 2 만약 데이터베이스 컬럼 규칙이 null: false 인 경우, 해당 규칙에 의해 에러가 발생합니다.
:allow_blank
## 5글자 입력 혹은 (Empty || NULL) 일 경우에만 데이터 저장 허용
class Bulletin < ApplicationRecord
validates :title, length: { is: 5 }, allow_blank: true
end
(validates 제약조건이 있더라도) NULL 혹은 Empty에 대해서 데이터 작성이 허용됩니다.
참고 만약 데이터베이스 컬럼 규칙이 null: false 인 경우, 해당 규칙에 의해 에러가 발생합니다.
:message
validates :name, presence: { message: "must be given please" }
validates :age, numericality: { message: "%{value} seems wrong" }
validates :username,
uniqueness: {
# object = person object being validated
# data = { model: "Person", attribute: "Username", value: <username> }
message: ->(object, data) do
"Hey #{object.name}!, #{data[:value]} is taken already! Try again #{Time.zone.tomorrow}"
end
}
에러 메세지의 기본 양식을 개발자 임의대로 변경합니다.
변수를 전해주는 방식으로 쓸 수도 있고, Proc의 원리를 응용해서도 쓸 쑤 있습니다.
:on
validates :title, length: { is: 5 }, on: :update
SQL에 접근하는 특정 행위 메소드일 때에만(save, create, update) 유효성검사가 이루어지도록 설정합니다.
- Conditional Validation
유효성 검사를 함에 있어 때로는 :if 혹은 :unless(if not) 문법을 두어 조건문 구분을 해둬야 할 때가 있습니다.
만약 validation의 실행을 원한다면 :if 문법을, 그렇지 않을경우 :unless 를 쓰면 됩니다.
Using a Symbol / String / Proc with :if and :unless
1) Symbol
class Purchase < ApplicationRecord
validates :card_no, presence: true, if: :paid_with_card?
def paid_with_card?
payment_type == "card"
end
end
Purchase.create(card_no: "", payment_type: "card")
# => Rollback (저장 실패)
Purchase.create(card_no: "9462-1602-3410-0810", payment_type: "card")
# => Commit (저장 성공)
Purchase.create(card_no: "", payment_type: "")
# => Commit (저장 성공)
Purchase.create(card_no: "", payment_type: "cash")
# => Commit (저장 성공)
Validates 옵션 및 Model 내에 생성된 메소드의 조건에 따라 데이터 저장 여부를 정합니다.
2) String
class Purchase < ApplicationRecord
validates :card_no, presence: true, if: "payment_type.empty?"
end
Purchase.create(card_no: "", payment_type: "card")
# => Commit (저장 성공)
Purchase.create(card_no: "9462-1602-3410-0810", payment_type: "card")
# => Commit (저장 성공)
Purchase.create(card_no: "", payment_type: "")
# => Commit (저장 실패)
Purchase.create(card_no: "", payment_type: "cash")
# => Commit (저장 성공)
Validates 옵션 및 Model 내에 생성된 if문 속 string으로 표현된 조건에 따라 데이터 저장 여부를 정합니다.
3) Proc
class Purchase < ApplicationRecord
validates :card_no, presence: true, :unless => Proc.new { |f| f.payment_type.blank? }
end
Purchase.create(card_no: "", payment_type: "card")
# => Commit (저장 실패)
Purchase.create(card_no: "9462-1602-3410-0810", payment_type: "card")
# => Commit (저장 성공)
Purchase.create(card_no: "", payment_type: "")
# => Commit (저장 성공)
Purchase.create(card_no: "", payment_type: "cash")
# => Commit (저장 실패)
Validates 옵션 및 Model 내에 생성된 Proc 내에 표현된 조건에 따라 데이터 저장 여부를 정합니다.
Combining Validation Conditions
class Purchase < ApplicationRecord
validates :card_no, presence: true, :if => ["paid_with_card?", "true"], :if => Proc.new { |f| f.card_no.blank? }
def paid_with_card?
payment_type == "card"
end
end
위의 사례를 응용하여 다음과 같이 종합적으로도 사용이 가능합니다.
- Performing Custom Validations
validation에서 제공하는 기본적인 기능들이 마음에 들지 않을경우, 개인적으로 따로 커스터마이징을 할 수 있습니다.
Custom Validators
## app/models/bulletin.rb
class Bulletin < ApplicationRecord
scope :find_bulletin, -> (bulletinId) { find(bulletinId) }
has_many :comments, as: :commentable
has_many :images
# 상위 모델파일과 연결
include ActiveModel::Validations
# MyValidator 라는 이름으로 개인이 만들어낸 유효성검사 실행
validates_with MyValidator
end
## app/models/my_validator.rb
class MyValidator < ActiveModel::Validator
def validate(record)
unless record.title.starts_with? 'X'
record.errors[:name] << 'Need a name starting with X please!'
end
end
end
예를들어 첫 글자가 'X'인지 판별해내는 유효성검사를 만들어야 할 때, 다음과 같이 개발을 해볼 수 있습니다.
Custom methods
class Invoice < ApplicationRecord
validate :expiration_date_cannot_be_in_the_past,
:discount_cannot_be_greater_than_total_value
def expiration_date_cannot_be_in_the_past
if expiration_date.present? && expiration_date < Date.today
errors.add(:expiration_date, "can't be in the past")
end
end
def discount_cannot_be_greater_than_total_value
if discount > total_value
errors.add(:discount, "can't be greater than total value")
end
end
end
예를들어 invoices 테이블의 컬럼 중, expiration_date 컬럼에 대해 다음과 같이 Model 파일 속에 메소드를 생성해서 validation 검사가 이루어지도록 할 수 있습니다.
또한 에러가 발생할 경우, 해당 에러에 대해 어떻게 메세지를 띄어줄건지도 보여줄 수 있습니다.
- Working with Validation Errors
만약 유효성 검사(validates) 과정에서 에러가 발생할 경우 에러에 대한 정보가 객체인 errors[] 안에 에러 정보가 나타납니다.
여기서 위 사진의 내용 중, @details 객체 속의 메세지는 에러의 원인이 무엇인지 validates 에 따라 기본 에러 message를 띄어줍니다.
하지만 만약 에러가 없이 데이터가 정상적으로 쓰여졌다면(Insert), errors[] 속 에러 정보를 가진 객체에는 빈 객체로 반환됩니다.
또한, 아래와 같이 에러메세지를 개별적으로 꾸며낼 수도 있습니다.
class Bulletin < ApplicationRecord
validate :do_not_use_korean
def do_not_use_korean
if title.match(/\A[가-힣]+\z/)
errors.add(:title, "can't be use hanguel")
end
end
end
또한, 에러가 몇 개 발생했는지도 카운팅을 해주는 메소드가 존재합니다.
class Bulletin < ApplicationRecord
validates :pwd, presence: true, confirmation: true, numericality: true
end
여기서 만약
1) Model 파일에 validation 조건을 적고
2) pwd 및 pwd_confirmation 입력폼이 존재한다
할 때, 두 form을 서로 다른 값으로 입력하고, 정수가 아닌 string으로 입력 시
presence: true 는 만족을 하나, 그 외 조건은 만족치 않으므로 에러 size에 대해서는 2를 반환하게 됩니다.
- 자료 참고
1. Exploring the `:inverse_of` Option on Rails Model Associations
2. Rails: When to use :inverse_of in has_many, has_one or belongs_to associations
'프로그래밍 공부 > TIL : Rails Tutorial' 카테고리의 다른 글
Chapter 6 : Active Record Associations (0) | 2020.07.07 |
---|---|
Chapter 5 : Active Record Callbacks (0) | 2020.07.05 |
Chapter 3 : Active Record Migrations (0) | 2020.07.02 |
Chapter 2 : Active Record Basics (0) | 2020.07.01 |
Chapter 1 : Getting Started with Rails (0) | 2020.06.23 |