Rails validations

1 Tổng quan về valiations (kiểm tra tính hợp lệ của đối tượng Active Record)

Ta có một ví dụ đơn giản về validation trong Rails như sau

class Person < ApplicationRecord
  validates :name, presence: true
end

Person.create(name: "Name").valid? # => true
Person.create(name: nil).valid? # => false

Trong ví dụ trên bạn có thể thấy model Person được thêm validate name không được để trống. Vì vậy khi tạo bản ghi thứ nhất với trường name là "Name" sẽ thành công, còn trường hợp thứ 2 khi truyền vào name là nil thì sẽ không thể tạo được person này.

1.1 Tại sao phải dùng validations?

Validations dùng để chắn chắn dữ liệu được lưu vào trong database là đúng. Như trong ví dụ ở trên ta có thể thấy là mỗi person bắt buộc phải có tên, với những person không tên thì sẽ ko thể tạo trong database được. Vậy tại sao ta phải thêm validate ở trong model mà không phải chỗ nào khác? Ta thêm validate ở trong model bởi vì mọi thao tác với cơ sở dữ liệu đều phải thông qua model. Thêm validate trong model sẽ đảm bảo được dữ liệu cập nhật vào database đều được kiểm định trước khi lưu.

Có một vài cách khác để validate dữ liệu trước khi lưu vào trong database như validate ở ngay trong cơ sở dữ liệu, validate từ phía client site và validate ở trong phần xử lí controller chúng ta cùng so sánh.

  • Validate ở mức cơ sở dữ liệu làm cho cơ sở dữ liệu của bạn trở nên phụ thuộc, việc này gây khó khăn cho việc bảo trì và kiểm thử. Tuy nhiên nếu cơ sở dữ liệu này sử dụng cho nhiều ứng dụng khác nhau thì việc validate trong cơ sở dữ liệu cũng ko phải là tồi bởi vì tất cả những ứng dụng sử dụng cơ sở dữ liệu này đều sẽ phải theo validate đó.

  • Vaildate từ phía client site rất hữu ích với người dùng nhưng tính tin cậy không cao. Giả sử ta sử dụng javascript để validate có nhiều trường hợp người dùng có thế tắt javascript hoặc dùng một đoạn mã nào đó vượt qua validate một cách dễ dàng. Tuy nhiên client-site validation khá hữu ích khi có thể phản hồi lại cho người dùng ngay lập tức, như vậy sẽ thân thiện với người dùng hơn.

  • Validate ở mức controller validate ở controller có vẻ không tốt lắm bởi ví khá là khó để test và bảo trì. Thêm nữa là việc đưa validate vào controller làm cho controller của bạn trở nên phức tạp và khó kiểm soát.

Từ những đặc điểm trên ta có thể thấy thêm validate ở model là hợp lí nhất.

1.2 validate hoạt động như thế nào?

Có 2 loại đối tượng của Active Record, kiểu thứ nhất là đối tượng tương ứng với một hàng (row) trong database, kiểu kia là không có trong database. Ví dụ khi bạn khởi tạo một đối tượng của một model nào đấy với từ khóa new thì nó sẽ chưa được ghi vào cơ sở dữ liệu. Khi gọi phương thức save thành công thì đối tượng lúc đó mới được lưu vào trong database. Trong rails ta dùng phương thức new_record? để kiểm tra xem đối tượng đó có phải là bản ghi mới không, hoặc persisted? để kiểm tra xem đối tượng đã có trong database chưa. Chúng ta cùng xem ví dụ đơn giản sau:

class Person < ApplicationRecord
end
$ bin/rails console
>> p = Person.new(name: "Name")
=> #<Person id: nil, name: "Name", created_at: nil, updated_at: nil>
>> p.new_record?
=> true
>> p.save
=> true
>> p.new_record?
=> false

Việc tạo ra bản ghi mới sẽ gọi đến câu lện SQL INSERT, tương tự với trường hợp update dữ liệu sẽ gọi đến câu lệnh SQL UPDATE. Validations sẽ được gọi đến trước khi chạy 2 câu lệnh trên để cập nhật vào cơ sở dữ liệu. Trong quá trình validate nếu xảy ra bất cứ lỗi gì thì đối tượng này sẽ được đánh dấu là không được gọi đến lệnh INSERT hoặc UPDATE. Điều này tránh việc lưu những đối tượng lỗi vào trong cơ sở dữ liệu. Bạn cũng có thể thêm các từy chọn chạy validations khi tạo đối tượng (create), cập nhật đối tượng (update) hoặc lưu đối tượng (save) (lưu đối tượng sẽ bảo gồm cả tạo và cập nhật).

Chú ý: Có khá nhiều cách để thay đổi trạng thái của đối tượng trong cơ sở dữ liệu. Một số phương thức có gọi đến validations còn một số thì không. Điều đó có nghĩa sẽ có trường hợp bạn lưu dữ liệu vào trong cơ sở dữ liệu trong khi đối tượng này vẫn bị lỗi bởi vì có thể nó không chạy qua validations. Bạn nên thận trọng trong những trường hợp này.

Dưới đây là các phương thức thay đổi dữ lệu sẽ gọi đến validations

create
create!
save (* phương thức này sẽ không chạy qua validations nếu có options validate: false)
save! (* phương thức này sẽ không chạy qua validations nếu có options validate: false)
update
update!
update_attributes (tương đương với update)
update_attributes! (tương đương với update!)

Chú ý: Các phương thức có dấu ! ở cuối (Ví dụ: create!) sẽ bắn ra một lỗi nếu bản ghi này không hợp lệ. Những phương thức không có dấu ! ở cuối thì sẽ trả về kế quả của phương thức này. Đối với save, update sẽ trả về giá trị true hoặc false còn create sẽ trả về đối tượng (đối tượng tạo thành công sẽ có id còn không thành công sẽ không có id).

Một số phương thức cập nhật dữ liệu mà không gọi đến validations


decrement!
decrement_counter
increment!
increment_counter
toggle!
touch
update_all
update_attribute
update_column
update_columns
update_counters
save(validate: false)
save!(validate: false)

Chú ý: phương thức save khi truyền vào tùy chọn là validate: false thì nó sẽ không chạy qua validations nữa.

1.3 valid? and invalid?

Khi thay sử dụng các phương thức có gọi đến validations để lưu đối tượng vào trong cơ sở dữ liệu. Trước tiên Rails sẽ chạy validations để kiểm tra xem đối tượng này có lỗi không. Nếu đối tượng bị lỗi thì sẽ không cho lưu vào cơ sở dữ liệu. Ta cũng có thể gọi đến validations để kiểm tra xem đối tượng có hợp lệ không một cách độc lập với phương thức valid?invalid?. invalid? sẽ trả về true nếu đối tượng là hợp lệ, trả về false nếu đối tượng bị lỗi. Còn invalid? đơn giản là ngược lại của valid?

class Person < ApplicationRecord
  validates :name, presence: true
end

Person.create(name: "Name").valid? # => true
Person.create(name: "Name").invalid? # => false
Person.create(name: nil).valid? # => false
Person.create(name: nil).invalid? # => true

Sau khi chạy validations kết quả của việc kiểm tra lỗi sẽ đựơc lưu lại bạn có thể gọi thông qua phương thức errors.messages sẽ trả về một tập hợp (Hash) các lỗi của đối tượng. Nếu đối tượng này không có lỗi thì phương thức trên sẽ trả về một tập hợp (Hash) rỗng.

Chú ý: chỉ khi chạy qua validations thì mới cập nhật được thông tin lỗi của đối tượng. Ví dụ khi mới khởi tạo một đối tượng có dữ liệu lỗi ta gọi phương thức errors.messages luôn thì sẽ không nhìn thấy lỗi.

class Person < ApplicationRecord
  validates :name, presence: true
end

>> p = Person.new
# => #<Person id: nil, name: nil>
>> p.errors.messages
# => {}

>> p.valid?
# => false
>> p.errors.messages
# => {name:["can't be blank"]}

>> p.name = "Name"
# => "Name"
>> p.errors.messages # Cần chạy lại validations để cập nhật lại lỗi
# => {name:["can't be blank"]}

>> p = Person.create
# => #<Person id: nil, name: nil>
>> p.errors.messages
# => {name:["can't be blank"]}

>> p.save
# => false

>> p.save!
# => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank

>> Person.create!
# => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank

1.4 errors[] (hoặc errors.messages[])

Để kiểm tra một thuộc tính của đối tượng có hợp lệ hay không, ta có thể sử dụng errors[:attribute] (attribute là tên của thuộc tính). Nó sẽ trả về một dãy các lỗi cho thuộc tính này. Nếu không có lỗi gì nó sẽ trả về một dãy không có phần tử nào.

Như đã nói ở phần trên ta phải chạy validations trước khi kiểm tra lỗi bởi vì khi chạy valiations thì mới cập nhật lỗi chứ không có chiều ngược lại là gọi phương thức errors rồi gọi lại validations.

class Person < ApplicationRecord
  validates :name, presence: true
end

>> Person.new.errors[:name].any? # => false
>> Person.create.errors[:name].any? # => true
>> Person.create.errors[:name] # => ["can't be blank"]

Qua phần trên bạn đã có thể hiểu một chút Rails validations hoạt động như thế nào. Phần tiếp theo ta sẽ tìm hiểu cách sử dụng validations để validate dữ liệu trong Rails.