rodrigoalvesvieira
rodrigoalvesvieira

Reputation: 8062

Query records before insertion with Ecto (similar to an AR callback)

I'm new to Elixir and Phoenix (less than 10 days) but very excited about it and like many others I come from a Rails background.

I understand Ecto is not AR and callbacks have been deprecated or removed but I need to add a custom validation that should only happen on creation and needs to perform a query.

Here's what my Reservation model basically looks like.

schema "reservations" do
  field :ends_at, :utc_datetime
  field :name, :string, null: false
  field :starts_at, :utc_datetime
  field :user_id, :id
end

and then I have another schema Slot, which looks like this:

schema "slots" do
  field :ends_at, :utc_datetime
  field :name, :string, null: false
  field :starts_at, :utc_datetime
  field :admin_id, :id
end

Whenever I'm adding a new reservation, I need to query my DB to check if there are any slots with matching ends_at and starts_at. If there are, I need to prevent the record from being saved and add an error to it (similar to what in Rails we accomplish with throw :abort and errors.add).

Can someone please shed a light on this? What's the Ecto way of doing this?

Best regards

Upvotes: 1

Views: 270

Answers (1)

S.B
S.B

Reputation: 847

*edit: added examples using separate changesets for creation and updation

You can add a custom validation function in your changeset validation chain and do DB queries in it.

Haven't run this code, but something like this should work

# separate changeset for creation
def create_changeset(struct, params) do
  struct
  |> cast(params, [...list of fields...])
  |> validate_unique([:name]) # lets say it has to be unique
  |> validate_slots # -- custom validation
end

# separate changeset for updation, no slot-check
def update_changeset(struct, params) do
  struct
  |> cast(params, [...list of fields...])
  |> validate_unique([:name]) # lets say it has to be unique
end


def validate_slots(changeset) do
    starts_at = get_field(changeset, :starts_at)
    ends_at = get_field(changeset, :ends_at)
    slots = Repo.all(from s in Slot, where: s.starts_at == ^starts_at and s.ends_at == ^ends_at)

    if Enum.empty?(slots) do
      changeset
    else
      add_error( changeset, :starts_at, "has slot with similar starts_at/ends_at")
    end
end

#---- using the changesets
# creation
%Reservation{} |> Reservation.create_changeset(params) |> Repo.insert()

# updation
%Reservation{} |> Reservation.update_changeset(params) |> Repo.update()

Although, from the look of it, you should probably normalize your starts_at and ends_at into a separate table called booking_time_frame or something and add unique indexes to it.

Or you might end up with more types of bookings and then have to check starts_at/ends_at across 3 tables and so on.

Upvotes: 1

Related Questions