1. Validations là gì?
1.1 Tại sao dùng validations?
Validations là cách để đảm bảo dữ liệu của bạn hợp lệ trước khi lưu vào database.
Có nhiều cách để thực hiện validate:
- DB constraints: Khiến cho cơ sở dữ liệu trở lên độc lập và khó để test hoặc bảo trì.
Ví dụ trong Rails tuts có đoạn này:
1 2 | add_index <span class="token symbol">:users</span><span class="token punctuation">,</span> <span class="token symbol">:email</span> <span class="token punctuation">,</span> unique<span class="token punctuation">:</span> <span class="token keyword">true</span> |
- Client-side validations: Dễ bị bỏ qua. Ví dụ nếu sử dụng JS để validations, nếu trên browser mà JS bị tắt, thì validations sẽ bị bỏ qua. Tuy nhiên nếu kết hợp được với các kỹ thuật khác, đây sẽ là 1 cách rất hay để validations và trả lại feedback ngay lập tức cho user.
- Controller-level validations: Rất khó để test validations ở level này. Ngoài ra, nó sẽ làm controller bị béo (nhiều code). Controller nên gầy 1 tý để ứng dụng có thể chạy trong thời gian dài.
Chốt lại, team Rails bảo là model-level validations là dễ dùng nhất.
1.2 When Does Validation Happen?
Có 2 loại ActiveRecord object: loại liên quan và loại không liên quan đến một record trong cơ sở dữ liệu.
Loại không liên quan được tạo bằng method new
. Ta sử dụng method new_record?
để phân biệt 2 loại object này.
1 2 3 4 5 6 7 | $ a = User.new => #<User id: nil, name: nil, location: nil, age: nil, image_path: nil, created_at: nil, updated_at: nil> $ a.new_record? => true $ User.first.new_record? => false |
Validations ở model level luôn chỉ chạy khi quá trình create hoặc update 1 bản ghi thông qua ActiveRecord được diễn ra.
Khi khởi tạo bản ghi thì ActiveRecord
sẽ gửi 1 câu INSERT
còn khi cập nhật thì nó gửi 1 câuUPDATE
query vào cơ sở dữ liệu.
Validations luôn chạy trước khi ActiveRecord định thực hiện câu INSERT hoặc UPDATE vào cơ sở dữ liệu.
Các method dưới đây sẽ gọi vào validations:
1 2 3 4 5 6 7 | create create! save save! update update! |
1.3 Skipping validations
Các method sau sẽ skip validations:
1 2 3 4 5 6 7 8 9 10 11 12 | decrement! decrement_counter increment! increment_counter toggle! touch update_all update_attribute update_column update_columns update_counters |
Hoặc có thể dùng save(validate: false)
để skip validations
1 2 3 4 5 6 7 8 9 10 11 12 | a = User.new name: "" => #<User id: nil, name: "", location: nil, age: nil, image_path: nil, created_at: nil, updated_at: nil> irb(main):006:0> a.save (0.1ms) begin transaction (0.1ms) rollback transaction => false irb(main):007:0> a.save(validate: false) (0.1ms) begin transaction User Create (0.7ms) INSERT INTO "users" ("name", "created_at", "updated_at") VALUES (?, ?, ?) [["name", ""], ["created_at", "2019-09-12 07:42:37.363766"], ["updated_at", "2019-09-12 07:42:37.363766"]] (91.0ms) commit transaction => true |
1.4 valid? , invalid?
Trước khi 1 ActiveRecord object được save, Rails sẽ chạy validations. Nếu validations có lỗi, Rails sẽ không save object lại. Các lỗi của object được lưu bởi collection
.errors.messages
.
1 2 3 4 5 6 7 | <span class="token operator">></span> a <span class="token operator">=</span> <span class="token constant">User</span><span class="token punctuation">.</span>create name<span class="token punctuation">:</span> <span class="token string">""</span> <span class="token punctuation">(</span><span class="token number">0.1</span>ms<span class="token punctuation">)</span> <span class="token keyword">begin</span> transaction <span class="token punctuation">(</span><span class="token number">0.1</span>ms<span class="token punctuation">)</span> rollback transaction <span class="token operator">=</span><span class="token operator">></span> <span class="token comment">#<User id: nil, name: "", location: nil, age: nil, image_path: nil, created_at: nil, updated_at: nil></span> <span class="token operator">></span> a<span class="token punctuation">.</span>errors<span class="token punctuation">.</span>messages <span class="token operator">=</span><span class="token operator">></span> <span class="token punctuation">{</span><span class="token symbol">:name</span><span class="token operator">=</span><span class="token operator">></span><span class="token punctuation">[</span><span class="token string">"can't be blank"</span><span class="token punctuation">]</span><span class="token punctuation">}</span> |
Sau khi chạy validations, nếu
a.errors.messages
rỗng thì a là 1valid object
.Các object tạo từ method
.new
không bao giờ trả vềerrors
, vì nó không chạy validations.
1 2 3 4 5 6 7 | > a = User.new => #<User id: nil, name: nil, location: nil, age: nil, image_path: nil, created_at: nil, updated_at: nil> > a => #<User id: nil, name: nil, location: nil, age: nil, image_path: nil, created_at: nil, updated_at: nil> > a.errors.messages => {} |
valid?
method sẽ triggers validations và trả về giá trịfalse
nếuerrors.messages
rỗng.
1 2 3 4 5 6 7 8 9 10 11 12 13 | > a = User.new => #<User id: nil, name: nil, location: nil, age: nil, image_path: nil, created_at: nil, updated_at: nil> > a => #<User id: nil, name: nil, location: nil, age: nil, image_path: nil, created_at: nil, updated_at: nil> > a.errors.messages => {} > a.valid? => false > a.errors.messages => {:name=>["can't be blank"]} |
invalid?
chỉ đơn giản là method đảo của valid?
. Nó cũng triggers validations với object và trả về giá trị boolean
ngược lại với valid?
1.5 errors
Để xác định xem 1 attribute nhất định của object có valid không, ta làm kiểm tra bằng cú pháp object.errors[:attributes].any?
:
1 2 3 | a = User.create(name: "").errors[:name].any? => false |
Cú pháp này chỉ sử dụng được sau khi quá trình validations được diễn ra.
1.6 errors.details
Lấy ra symbol của validator:
1 2 3 4 | >> person = Person.new >> person.valid? >> person.errors.details[:name] # => [{error: :blank}] |
2. Validation helper
- ActiveRecord cung cấp nhiều validation helpers. Các helper này xây dựng dựa trên nhiều nguyên tắc validations phổ biến.
- Nếu validation fail, 1 message sẽ được add vào object.errors và messages này liên kết với attribute đã được validate.
- Mỗi validation helper có thể truyền vào nhiều attributes.
1 2 3 4 | <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> validates <span class="token symbol">:name</span><span class="token punctuation">,</span> <span class="token symbol">:location</span><span class="token punctuation">,</span> presence<span class="token punctuation">:</span> <span class="token keyword">true</span> <span class="token keyword">end</span> |
- Mỗi validation helper đều có 1
default message
. Option:message
sẽ có ích khi bạn không muốn dùngdefault message
:
1 2 3 4 5 6 7 8 9 10 11 | <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> validates <span class="token symbol">:name</span><span class="token punctuation">,</span> <span class="token symbol">:location</span><span class="token punctuation">,</span> presence<span class="token punctuation">:</span> <span class="token punctuation">{</span> message<span class="token punctuation">:</span> <span class="token string">"attribute này không được trống!"</span> <span class="token punctuation">}</span> <span class="token keyword">end</span> a <span class="token operator">=</span> <span class="token constant">User</span><span class="token punctuation">.</span><span class="token keyword">new</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token comment">#<User id: nil, name: nil, location: nil, age: nil, image_path: nil, created_at: nil, updated_at: nil></span> <span class="token operator">></span> a<span class="token punctuation">.</span>valid<span class="token operator">?</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token keyword">false</span> <span class="token operator">></span> a<span class="token punctuation">.</span>errors<span class="token punctuation">.</span>messages <span class="token operator">=</span><span class="token operator">></span> <span class="token punctuation">{</span><span class="token symbol">:name</span><span class="token operator">=</span><span class="token operator">></span><span class="token punctuation">[</span><span class="token string">"attribute này không được trống!"</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token symbol">:location</span><span class="token operator">=</span><span class="token operator">></span><span class="token punctuation">[</span><span class="token string">"attribute này không được trống!"</span><span class="token punctuation">]</span><span class="token punctuation">}</span> |
Bây giờ, chúng ta cùng tìm hiểu 1 số validations helper
phổ biến.
2.1 acceptance
Ta thường sử dụng acceptance
kết hợp với thẻ <input type="checkbox">
. Giả sử ta có 1 cái form yêu cầu nhập fields is_teen
như sau:
1 2 3 4 5 6 | <span class="token comment">#app/users/edit.html.erb</span> <span class="token operator"><</span><span class="token string">%= form_for @user do |f| %> <%=</span> f<span class="token punctuation">.</span>check_box is_teen <span class="token string">%> <%= f.submit "Submit" %></span> <span class="token operator"><</span><span class="token operator">%</span> <span class="token keyword">end</span> <span class="token operator">%</span><span class="token operator">></span> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <span class="token comment">#app/controllers/users_controller.rb</span> <span class="token keyword">class</span> <span class="token class-name">UsersController</span> <span class="token operator"><</span> <span class="token constant">ApplicationController</span> <span class="token keyword">def</span> <span class="token keyword">new</span> <span class="token variable">@user</span> <span class="token operator">=</span> <span class="token constant">User</span><span class="token punctuation">.</span><span class="token keyword">new</span> <span class="token class-name">params</span><span class="token punctuation">[</span><span class="token symbol">:user</span><span class="token punctuation">]</span> <span class="token keyword">end</span> <span class="token keyword">def</span> create <span class="token variable">@user</span> <span class="token operator">=</span> <span class="token constant">User</span><span class="token punctuation">.</span><span class="token keyword">new</span> <span class="token class-name">user_params</span> <span class="token keyword">if</span> <span class="token variable">@user</span><span class="token punctuation">.</span>save redirect_to root_path <span class="token keyword">else</span> flash<span class="token punctuation">[</span><span class="token symbol">:danger</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token variable">@user</span><span class="token punctuation">.</span>errors<span class="token punctuation">.</span>messages redirect_to root_path <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">private</span> <span class="token keyword">def</span> user_params params<span class="token punctuation">.</span><span class="token keyword">require</span><span class="token punctuation">(</span><span class="token symbol">:user</span><span class="token punctuation">)</span><span class="token punctuation">.</span>permit <span class="token symbol">:is_teen</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Ta thêm helper acceptance
vào model User
1 2 3 4 | <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> validates <span class="token symbol">:is_teen</span><span class="token punctuation">,</span> acceptance<span class="token punctuation">:</span> <span class="token keyword">true</span> <span class="token keyword">end</span> |
Khi checkbox không được tích vào thì user_params sẽ có giá trị thế này:
1 2 3 | (byebug) user_params <ActionController::Parameters {"is_teen"=>"0"} permitted: true> |
Và quá trình validations diễn ra như thế này:
1 2 3 4 5 6 7 8 9 | <span class="token operator">></span> <span class="token variable">@user</span> <span class="token operator">=</span> <span class="token constant">User</span><span class="token punctuation">.</span><span class="token keyword">new</span> <span class="token class-name">user_params</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token comment">#<User id: nil, name: nil, location: nil, is_teen: false, image_path: nil, created_at: nil, updated_at: nil, male: false></span> <span class="token operator">></span> <span class="token variable">@user</span><span class="token punctuation">.</span>valid<span class="token operator">?</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token keyword">false</span> <span class="token operator">></span> <span class="token variable">@user</span><span class="token punctuation">.</span>errors<span class="token punctuation">.</span>full_messages <span class="token operator">=</span><span class="token operator">></span> <span class="token punctuation">[</span><span class="token string">"Is_teen must be accepted"</span><span class="token punctuation">]</span> |
2.2 inclusion, exclusion
Helper inclusion
dùng để kiểm tra attribute nhập vào có nằm trong 1 tập hợp các giá trị cho trước không.
Cùng xem ví dụ dưới đây để hiểu:
1 2 3 4 | <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> validates <span class="token symbol">:name</span><span class="token punctuation">,</span> inclusion<span class="token punctuation">:</span> <span class="token punctuation">{</span> <span class="token keyword">in</span><span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token string">"Hiep"</span><span class="token punctuation">,</span> <span class="token string">"Hieu"</span><span class="token punctuation">]</span> <span class="token punctuation">}</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 | <span class="token operator">></span> user <span class="token operator">=</span> <span class="token constant">User</span><span class="token punctuation">.</span><span class="token keyword">new</span> <span class="token class-name">name</span><span class="token punctuation">:</span> <span class="token string">"Hung"</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token comment">#<User id: nil, name: "Hung", location: nil, age: nil, image_path: nil, created_at: nil, updated_at: nil, male: nil></span> <span class="token operator">></span> user<span class="token punctuation">.</span>valid<span class="token operator">?</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token keyword">false</span> <span class="token operator">></span> user<span class="token punctuation">.</span>errors<span class="token punctuation">.</span>full_messages <span class="token operator">=</span><span class="token operator">></span> <span class="token punctuation">[</span><span class="token string">"Name is not included in the list"</span><span class="token punctuation">]</span> |
Ngược lại với inclusion
, chúng ta helper exclusion
. Nếu attribute nhập vào không nằm trong tập các giá trị cho trước thì được coi là valid
.
1 2 3 4 | <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> validates <span class="token symbol">:name</span><span class="token punctuation">,</span> exclusion<span class="token punctuation">:</span> <span class="token punctuation">{</span> <span class="token keyword">in</span><span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token string">"Hiep"</span><span class="token punctuation">,</span> <span class="token string">"Hieu"</span><span class="token punctuation">]</span> <span class="token punctuation">}</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 | <span class="token operator">></span> user <span class="token operator">=</span> <span class="token constant">User</span><span class="token punctuation">.</span><span class="token keyword">new</span> <span class="token class-name">name</span><span class="token punctuation">:</span> <span class="token string">"Hieu"</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token comment">#<User id: nil, name: "Hieu", location: nil, age: nil, image_path: nil, created_at: nil, updated_at: nil, male: nil></span> <span class="token operator">></span> user<span class="token punctuation">.</span>valid<span class="token operator">?</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token keyword">false</span> <span class="token operator">></span> user<span class="token punctuation">.</span>errors<span class="token punctuation">.</span>full_messages <span class="token operator">=</span><span class="token operator">></span> <span class="token punctuation">[</span><span class="token string">"Name is reserved"</span><span class="token punctuation">]</span> |
2.2 presence, absence
Helper presence
dùng để kiểm tra xem attribute
có phải 1 blank
hay không, thông qua method blank?
. Nếu attribute.blank?
trả về true
thì sau khi validations sẽ thực hiện rollback.
Đầu tiên, bạn cần biết khi nào thì method blank?
trả về true
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <span class="token operator">></span> <span class="token string">""</span><span class="token punctuation">.</span>blank<span class="token operator">?</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token keyword">true</span> <span class="token operator">></span> <span class="token string">" "</span><span class="token punctuation">.</span>blank<span class="token operator">?</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token keyword">true</span> <span class="token operator">></span> <span class="token string">" t n"</span><span class="token punctuation">.</span>blank<span class="token operator">?</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token keyword">true</span> <span class="token operator">></span> <span class="token keyword">nil</span><span class="token punctuation">.</span>blank<span class="token operator">?</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token keyword">true</span> <span class="token operator">></span> <span class="token keyword">false</span><span class="token punctuation">.</span>blank <span class="token operator">=</span><span class="token operator">></span> <span class="token keyword">true</span> |
Thử với ví dụ dưới đây:
1 2 3 4 | <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> validates <span class="token symbol">:name</span><span class="token punctuation">,</span> presence<span class="token punctuation">:</span> <span class="token keyword">true</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 | > user = User.new name: "t n" => #<User id: nil, name: "t n", location: nil, age: nil, image_path: nil, created_at: nil, updated_at: nil, male: nil> > user.valid? => false > user.errors.full_messages => ["Name can't be blank"] |
Vì presence
nghĩa là sự có mặt, nên thường chúng ta sẽ nghĩ helper
này dùng để kiểm tra sự có mặt của attribute
.Tuy nhiên với boolean value
thì không hẳn thế .
Vì false.blank?
cũng trả về true
nên để validates sự có mặt attribute
theo kiểu boolean
, chúng ta nên dùng inclusion:
1 2 3 4 | <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> validates <span class="token symbol">:is_teen</span><span class="token punctuation">,</span> inclusion<span class="token punctuation">:</span> <span class="token punctuation">{</span> <span class="token keyword">in</span><span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token keyword">true</span><span class="token punctuation">,</span> <span class="token keyword">false</span><span class="token punctuation">]</span> <span class="token punctuation">}</span> <span class="token keyword">end</span> |
1 2 3 4 5 | > user = User.new is_teen: false => #<User id: nil, name: nil, location: nil, age: nil, image_path: nil, created_at: nil, updated_at: nil, is_teen: false> > user.valid? => true |
Ngược lại với helper presence
, helper absence
dùng method present?
kiểm tra xem attribute truyền vào có rỗng hay không. Nếu attribute.present?
trả về true
thì sau validation sẽ thực hiện rollback.
1 2 3 4 5 6 7 8 9 10 11 12 | > nil.present? => false > "".present? => false > " ".present? => false > false.present? => false |
2.4 numericality
Helper này dùng để đảm bảo các thuộc tính phải là kiểu Numeric
. Nếu attribute
không phải là kiểu Numeric
, sau khi validations sẽ thực hiện rollback
. Default message của helper này là [is not a number]
1 2 3 4 | <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> validates <span class="token symbol">:age</span><span class="token punctuation">,</span> numericality<span class="token punctuation">:</span> <span class="token keyword">true</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 | > user = User.new age: "ahihihi" => #<User id: nil, name: nil, location: nil, age: 0, image_path: nil, created_at: nil, updated_at: nil, male: nil> > user.valid? => false > user.errors.full_messages => ["Age is not a number"] |
Có khá nhiều tùy chọn với helper này. Nếu bạn muốn attribute
được validate
chỉ thuộc kiểu integer
, chúng ta có option only_integer: true
1 2 3 4 | <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> validates <span class="token symbol">:age</span><span class="token punctuation">,</span> numericality<span class="token punctuation">:</span> <span class="token punctuation">{</span> only_integer<span class="token punctuation">:</span> <span class="token keyword">true</span> <span class="token punctuation">}</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 | > user = User.new age: 1.5 => #<User id: nil, name: nil, location: nil, age: 1, image_path: nil, created_at: nil, updated_at: nil, male: nil> > user.valid? => false > user.errors.full_messages => ["Age must be an integer"] |
Ngoài ra chúng ta còn có một loạt các tùy chọn khác tương ứng với các symbol
sau:
:greater_than
: đảm bảoattribute
phải lớn hơn 1 giá trị mà bạn muốn.:greater_than_or_equal_to
: đảm bảoattribute
phải lớn hơn hoặc bằng 1 giá trị mà bạn muốn.:equal_to
: đảm bảo attribute phải bằng một giá trị mà bạn muốn.:odd
: đảm bảo attribute phải là một số lẻ.:even
: đảm bảo attribute phải là một số chẵn.
2.5 uniqueness
uniqueness hoạt động như nào?
1 2 3 4 | <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> validates <span class="token symbol">:name</span><span class="token punctuation">,</span> uniqueness<span class="token punctuation">:</span> <span class="token keyword">true</span> <span class="token keyword">end</span> |
thêm option case_sensitive.
1 2 | validates <span class="token symbol">:name</span><span class="token punctuation">,</span> uniqueness<span class="token punctuation">:</span> <span class="token punctuation">{</span> case_sensitive<span class="token punctuation">:</span> <span class="token keyword">false</span> <span class="token punctuation">}</span> |
Với bài toán, 1 người không được follow người khác 2 lần.
1 2 3 4 | <span class="token keyword">class</span> <span class="token class-name">Relationship</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> <span class="token operator">?</span><span class="token operator">?</span><span class="token operator">?</span><span class="token operator">?</span><span class="token operator">?</span> <span class="token keyword">end</span> |
Còn rất nhiều validation helper
khác rất hữu dụng, các bạn có thể tìm đọc thêm tại đây.
3. Một số option phổ biến trong các helper
3.1 allow_nil
Bạn muốn :name không thể là rỗng, nhưng chấp nhận giá trị nil.
1 2 | validates <span class="token symbol">:name</span><span class="token punctuation">,</span> presence<span class="token punctuation">:</span> <span class="token keyword">true</span><span class="token punctuation">,</span> allow_nil<span class="token punctuation">:</span> <span class="token keyword">true</span> |
3.2 allow_blank
3.3 :on
1 2 | validates <span class="token symbol">:age</span><span class="token punctuation">,</span> numericality<span class="token punctuation">:</span> <span class="token punctuation">{</span> greater_than<span class="token punctuation">:</span> <span class="token number">16</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> on<span class="token punctuation">:</span> <span class="token symbol">:update</span> |
4. Custom validation
Nếu những validations helper mà ActiveRecord tạo sẵn cho bạn vẫn không thể giải quyết được bài toán mà bạn đang gặp phải, thì bạn có thể tự validation theo cách của mình .
Mình tìm thấy 2 cách có thể thực hiện custom validation: custom method
vàcustom validator
4.1 Custom method
Ví dụ, mình có bài toán follow
rất quen thuộc với 2 bảng User
và Relationship
như sau.
1 2 3 4 5 6 7 8 9 10 11 | <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> has_many <span class="token symbol">:active_relationships</span><span class="token punctuation">,</span> class_name<span class="token punctuation">:</span> <span class="token constant">Relationship</span><span class="token punctuation">.</span>name<span class="token punctuation">,</span> foreign_key<span class="token punctuation">:</span> <span class="token symbol">:follower_id</span> has_many <span class="token symbol">:passive_relationships</span><span class="token punctuation">,</span> class_name<span class="token punctuation">:</span> <span class="token constant">Relationship</span><span class="token punctuation">.</span>name<span class="token punctuation">,</span> foreign_key<span class="token punctuation">:</span> <span class="token symbol">:followed_id</span> has_many <span class="token symbol">:following</span><span class="token punctuation">,</span> through<span class="token punctuation">:</span> <span class="token symbol">:active_relationships</span><span class="token punctuation">,</span> source<span class="token punctuation">:</span> <span class="token symbol">:followed</span> has_many <span class="token symbol">:followers</span><span class="token punctuation">,</span> through<span class="token punctuation">:</span> <span class="token symbol">:passive_relationships</span><span class="token punctuation">,</span> source<span class="token punctuation">:</span> <span class="token symbol">:follower</span> <span class="token keyword">end</span> |
1 2 3 4 5 | <span class="token keyword">class</span> <span class="token class-name">Relationship</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> belongs_to <span class="token symbol">:follower</span><span class="token punctuation">,</span> class_name<span class="token punctuation">:</span> <span class="token constant">User</span><span class="token punctuation">.</span>name belongs_to <span class="token symbol">:followed</span><span class="token punctuation">,</span> class_name<span class="token punctuation">:</span> <span class="token constant">User</span><span class="token punctuation">.</span>name <span class="token keyword">end</span> |
Bây giờ mình muốn validate
trường hợp, user
không thể tự follow
chính mình. Nghĩa là khi 1 bản ghi Relationship được tạo, nếu follower_id == followed_id
trả về true
, thì sau quá trình validation phải thực hiện rollback
.
Bài toán này khá khó để dùng các validation helper sẵn có của ActiveRecord
, vì vậy mình sẽ thực hiện custom validation như sau .
1 2 3 4 5 6 7 8 9 10 11 12 13 | <span class="token keyword">class</span> <span class="token class-name">Relationship</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> validate <span class="token symbol">:cannot_follow_yourself</span> belongs_to <span class="token symbol">:follower</span><span class="token punctuation">,</span> class_name<span class="token punctuation">:</span> <span class="token constant">User</span><span class="token punctuation">.</span>name belongs_to <span class="token symbol">:followed</span><span class="token punctuation">,</span> class_name<span class="token punctuation">:</span> <span class="token constant">User</span><span class="token punctuation">.</span>name <span class="token keyword">private</span> <span class="token keyword">def</span> cannot_follow_yourself <span class="token keyword">if</span> followed_id <span class="token operator">==</span> follower_id errors<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token symbol">:you</span><span class="token punctuation">,</span> <span class="token constant">Settings</span><span class="token punctuation">.</span>relationship<span class="token punctuation">.</span>cannot_follow_yourself <span class="token punctuation">)</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Thực hiện validate:
1 2 3 4 5 6 7 8 9 10 11 | > relationship = Relationship.new follower_id: 1, followed_id: 1 => #<Relationship id: nil, follower_id: 1, followed_id: 1, created_at: nil, updated_at: nil> > relationship.valid? User Load (15.3ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 User Load (0.3ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 => false > relationship.errors.full_messages => ["You can't follow yourself"] |
Phương pháp thực hiện validate nói trên được gọi là custom validation sử dụng custom method
.
3.2 Custom validator.
Giả sử trong rails app của bạn có các model Course
, Plan
và Event
. Cả 3 bảng này đều có 2 trường start_date
và end_date
. Bạn cần validates ở cả 3 model, để đảm bảo rằng start_date
luôn phải nhỏ hơn hoặc bằng end_date
. Sẽ có 2 vấn đề ở bài toán này:
- Một là, khó để tìm ra 1 validation helper có sẵn mà phù hợp với yêu cầu của bài toán, vậy nên chúng ta sẽ sử dụng custom validation.
- Hai là, nếu sử dụng
custom method
chúng ta sẽ phải viết lại method đấy 3 lần ở mỗi model. Như thế khá khó để sử dụng lại code .
Để giải quyết cả 2 vấn đề nói trên, mình sẽ viết 1 class riêng chứa validator, và gọi lại class này ở mỗi model cần validate.
Đầu tiên mình tạo một thư lục để lưu các class validators và config để rails có thể load được nó trong file application.rb
1 2 3 4 | <span class="token comment">#config/application.rb</span> config<span class="token punctuation">.</span>load_defaults <span class="token number">5.2</span> config<span class="token punctuation">.</span>autoload_paths <span class="token operator">+</span><span class="token operator">=</span> <span class="token string">%W["<span class="token interpolation"><span class="token delimiter tag">#{</span>config<span class="token punctuation">.</span>root<span class="token delimiter tag">}</span></span>/app/validators/"]</span> |
1 2 3 4 5 6 7 8 9 | <span class="token comment">#app/validators/date_validator.rb</span> <span class="token keyword">class</span> <span class="token class-name">DateValidator</span> <span class="token operator"><</span> <span class="token constant">ActiveModel</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Validator</span> <span class="token keyword">def</span> <span class="token function">validate</span><span class="token punctuation">(</span>record<span class="token punctuation">)</span> <span class="token keyword">if</span> record<span class="token punctuation">.</span>start_date <span class="token operator">></span> record<span class="token punctuation">.</span>end_date record<span class="token punctuation">.</span>errors<span class="token punctuation">[</span><span class="token symbol">:start_date</span><span class="token punctuation">]</span> <span class="token operator"><</span><span class="token operator"><</span> <span class="token string">"Start date cannot greater than end date"</span>" <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Giờ mình sẽ gọi validator này với method validates_with
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <span class="token keyword">class</span> <span class="token class-name">Course</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> include <span class="token constant">ActiveModel</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Validations</span> validates_with <span class="token constant">DateValidator</span> <span class="token keyword">end</span> <span class="token keyword">class</span> <span class="token class-name">Plan</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> include <span class="token constant">ActiveModel</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Validations</span> validates_with <span class="token constant">DateValidator</span> <span class="token keyword">end</span> <span class="token keyword">class</span> <span class="token class-name">Event</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> include <span class="token constant">ActiveModel</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Validations</span> validates_with <span class="token constant">DateValidator</span> <span class="token keyword">end</span> |
Thử chạy validate nhé:
1 2 3 4 5 6 7 | > course = Course.new start_date: Date.new(2019) , end_date: Date.new(2018) => #<Course id: nil, content: nil, start_date: "2019-01-01", end_date: "2018-01-01"> > course.valid? => false > course.errors.full_messages => ["Start date cannot greater than end date"] |
1 2 3 4 5 6 7 | > plan = Plan.new start_date: Date.new(2019) , end_date: Date.new(2018) => #<Plan id: nil, content: nil, start_date: "2019-01-01", end_date: "2018-01-01"> > plan.valid? => false > plan.errors.full_messages => ["Start date cannot greater than end date"] |
Đó là tất cả những gì mình muốn trình bày trong bài viết lần này.
References:
https://guides.rubyonrails.org/active_record_validations.html