Constant field in Ecto

I want the field not to change because the table is actually a reference to one of the types …

i.e.
(feature table) has (type) and (description_id) and (image_id)

so the (feature table) can either have one of two … the image or the description, it can not have both. (note this is just an example)

my goal is to either have this or that, so the (type) can not change, if I chose the type to be (image) I can not later change it,

I can remove the (type) from the cast, but the thing is, is this the correct way to make a field (constant) and prevent it from changing or should I use a constraint?

Hi!

Just to be sure that I’ve understood you problem well : you have defined a Ecto.Schema for a given model (let’s call it Feature), and in that %Feature{} struct, you want the field type to be set once and for all at creation, and never change on further updates (which can change either image_id or description_id depending on the value of type)

Is that it ?

If so…

I can remove the (type) from the cast, but the thing is, is this the correct way to make a field (constant) and prevent it from changing or should I use a constraint?

Not an expert myself but I would say that the best way to prevent your type field from changing is to remove it from the cast fields indeed.

The way you ask your question makes me want to add - if that’s helpful to you - that it took me a long time to figure out that you don’t need to always use the same changeset/2 function for all the operations you need to perform on your data (which is what appears e.g. in the simple examples from the Ecto documentation). That even seems totally counter-productive to me now.

So in your case I would have 2 functions e.g. create_cs and update_cs :

@create_params [:type, :description_id, :image_id]
@update_params [:description_id, :image_id] # No type here !

def create_cs(params \\ %{}) do
  %Feature
  |> cast(params, @create_params)
  |> validate_required([:type])
end

def update_cs(feature = %Feature{}, params) do
  feature
  |> cast(params, @update_params)
end

And then : create_cs(params) |> Repo.insert or feature |> update_cs(params) |> Repo.update

Actually since it also seems important to check consistency, you can even have two distinct clauses in update_cs, e.g. :

def update_cs(feature = %Feature{type: :image}, params) do
  feature
  |> cast(params, [:image_id])
  |> validate_required([:image_id])
end

def update_cs(feature = %Feature{type: :description}, params) do
  feature
  |> cast(params, [:description_id])
  |> validate_required([:description_id])
end

create_cs function could also have different clauses or even be split into 2 functions if the value of type can be determined from the context of the call (and not from what is passed within params).

You might also want to simply use your RDBMS capabilities and add a CHECK constraint (e.g. "type" = 'image' AND "image_id" IS NOT NULL OR "type" = 'description' AND "description_id" IS NOT NULL) and a TRIGGER run on UPDATE operations that would check e.g. that OLD.type = NEW.type and raise an exception otherwise ; the later one would be a bit more invasive in your app code, though, since last time I checked, those SQL exceptions are not caught by Ecto and will trigger an exception that you need to rescue.

3 Likes