Ecto.Repo Abstraction

I’d like to create an abstraction of the Ecto.Repo.insert_or_update function which will allow me to pass in the key by which to check if the record exists. This would allow me to write a single function to insert or update records in my database for multiple schemas. I’m looking for some guidance on how to do this using metaprogramming.

Below is the sample code for this, take from the example on Hex: https://hexdocs.pm/ecto/Ecto.Repo.html#c:insert_or_update/2

I want to pass three variables to my generic function: model, key and params.

  • model is name of the schema I am inserting/updating
  • key is the name of the field in the model by which I want to do a lookup to check if a record exists
  • params is the data I want to update.

For example, say I am getting user data from an external source that I want to update. The parameter values would look like this:

model = %User{}
key = :name
params = %{name: "John",  email: "john@doe.com", dob: "1970-01-01"}

I’d like to pass in these values to the following function to lookup the user by name and update his record if he exists, or insert it otherwise. However, this isn’t working:

def persist(model, key, params) do
  quote do
    case Repo.get_by(model, unquote(key) params[key]) do
      nil -> struct(model)
      record -> record
    end
    |> model.changeset(params)
    |> Repo.insert_or_update
  end
end

The code unquote(key) renders as :name, whereas Ecto.Repo.get_by requires it to be in the format name:

I’d love some guidance here. I’m new to metaprogramming.

Thanks!

Why do you need a macro here? I don’t see anything g here that couldn’t be done with a function

Perhaps I don’t, I just surmised that macros were the way to go here. I didn’t see how to solve this with a function.

Your model that you pass in may just need to be the module instead of the struct but try this:

def persist(model, key, params) do
  kw_list = [{key, params[key]}]
  Repo.get_by(model, kw_list) do
    nil -> struct(model)
    record -> record
  end
  |> model.changeset(params)
  |> Repo.insert_or_update()
end
1 Like

That worked marvelously! Thank you!

I did confirm that the model variable does need to be converted to a struct for an insert to work correctly.