Parse và validate request param trong Phoenix với Ecto

Image not Found

Khi viết các API hoặc cả các endpoint thì thông thường chúng ta sẽ có một số nhu cầu:

  • Chỉ cho phép một số các tham số xác định được truyền vào.
  • Chuyển các tham số về kiểu dữ liệu mong muốn
  • Validate các tham số theo yêu cầu

Bài viết này sẽ hướng dẫn các bạn giải quyết các vấn đề trên sử dụng Ecto.Changeset

Thư viện Ecto đã cung cấp sẵn cho chúng ta module Changeset. Nó hỗ trợ việc cast các tham số về đúng kiểu dữ liệu mong muốn, nó cũng hỗ trợ các phương thức để validate các tham số yêu cầu, và nó cũng cho phép bạn giới hạn tham số nào được truyền vào.

Và sau đây là một ví dụ sử dụng Chageset để validate các tham số khi filter các đơn hàng.

1. Đầu tiên bạn phải định nghĩa một schema

 1defmodule MyApp.OrderFilterParams do
 2    use Ecto.Schema
 3    import Ecto.Changeset
 4
 5    schema "order_filter_params" do
 6        field :keyword, :string
 7        field :category_id,  :integer
 8        field :status, :string
 9        field :start_date, :utc_datetime
10        field :end_date, :utc_datetime
11    end
12end

2. Cast và validate

Sau đó phải định nghĩa một hàm để thực hiện việc cast tham số và validate changeset.

 1defmodule MyApp.OrderFilterParams do
 2
 3    ...
 4
 5    @required ~w(category_id start_date)
 6    @optional ~w(keyword status end_date)
 7
 8    def changeset(changeset_or_model, params) do
 9         cast(changeset_or_model, params, @required ++ @optional)
10        |> validate_required(@required)
11    end
12end

3. Set giá trị default động

Nếu bạn muốn sử dụng các giá trị default động, ví dụ như mặc định ngày kết thúc là ngày hiện tại, các bạn phải định nghĩa một function để set giá trị mong muốn.

 1defmodule MyApp.OrderFilterParams do
 2
 3    ...
 4
 5    def changeset(changeset_or_model, params) do
 6         cast(changeset_or_model, params, @required ++ @optional)
 7        |> validate_required(@required)
 8        |> set_default_end_date()
 9    end
10
11    defp set_defaut_end_date(changeset) do
12        end_date = get_change(changeset, :end_date)
13        if is_nil(end_date) do
14            put_change(changeset, :end_date, Timex.today())
15        else
16            changeset
17        end
18    end
19end

4. Sử dụng Params schema

 1defmodule MyApp.OrderController do
 2    use MyApp, :controller
 3    alias MyApp.OrderFilterParams
 4
 5    def index(conn, params) do
 6        changeset = OrderFilterParams.changeset(%OrderFilterParams{}, params)
 7
 8        if changeset.valid? do
 9            strong_params = Ecto.Changeset.apply_changes(changeset)
10				IO.put(strong_params.keyword)
11            # Do something with your params
12        else
13            # handle error
14        end
15    end
16end
17

Rất đơn giản đúng không, nếu bạn đã sử dụng Ecto thì việc này chỉ là ruồi muỗi. Tuy nhiên đơn giản thì phải có thứ đánh đổi chứ.

Vài thứ mà bạn sẽ thấy bất tiện

1. Lượng code mà bạn phải viết quá nhiều.

Thử tưởng tượng mỗi API bạn lại phải định nghĩa thêm một Module params cho nó thì phức tạp vl.

Bạn có thể sử dụng schemaless, nhưng mà function của bạn sẽ rối nùi lên vì code logic và code xử lý params nó không liên quan gì tới nhau cả. Và bạn thì kiểu như đổ sting vào cơm để ăn vậy.

2. Thiếu linh hoạt.

Điều này cũng đúng vì mục đích chính của Ecto là phục vụ cho việc định nghĩa các schema cho database.

Đơn giản như việc định nghĩa giá trị default động như trên, bạn phải viết luôn 1 hàm mới

Tuy nhiên nó cũng có một ưu điểm là bạn không phải sử dụng thêm thư viện của bên thứ ba.

Kết

Nếu bạn không cần phải xử lý nhiều ràng buộc liên quan đến tham số của request thì đơn giản là cứ dùng Changeset thôi.

Nếu bạn muốn nhanh gọn hơn thì trên Hex có một số thư viện để hỗ trợ định nghĩa param đơn giản hơn, ví dụ như https://github.com/bluzky/tarams/

Thư viện này cung cấp cách thức đơn giản và nhanh chóng hơn để định nghĩa param cho API. Mình sẽ viết bài hướng dẫn sau.