Q:

Ecto insert_or_update creating multiple inserts?

My code is resulting in a rare double or triple insert into the database and I am at a loss as to why. It is very difficult to reproduce but I can look at the timestamps to see the created at time is basically the same when it happens. I believe it only occurs when the CardMeta is not already found.
I figure I need to add a unique key or wrap it in a transaction.
  def get_or_create_meta(user, card) do
    case Repo.all(from c in CardMeta, where: c.user_id == ^user.id,
      where: c.card_id == ^card.id) do
        [] ->
          %CardMeta{}
        metas ->
          hd metas
    end   
  end

  def bury(user, card) do
    get_or_create_meta(user, card)
    |> Repo.preload([:card, :user])
    |> CardMeta.changeset(%{last_seen: DateTime.utc_now(), user_id: user.id, card_id: card.id,
      learning: false, known: false, prev_interval: 0})
    |> Repo.insert_or_update
  end

Edit: adding changeset source
  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:last_seen, :difficulty, :prev_interval, :due, :known, :learning,
                      :user_id, :card_id])
    |> assoc_constraint(:user)
    |> assoc_constraint(:card)
  end

Calling bury from the controller
def update(conn, %{"currentCardId" => card_id, "command" => command}) do
    # perform some update on card
    card = Repo.get!(Card,card_id)
    user = Guardian.Plug.current_resource(conn)

    case command do
      "fail" ->
        SpacedRepetition.fail(user, card)
      "learn" ->
        SpacedRepetition.learn(user, card)
      _ ->
        SpacedRepetition.bury(user, card)
    end
    sendNextCard(conn, user)
  end

Edit:
I noticed the last_seen field is microseconds different between duplicated rows, whereas the create_at field does not have that resolution. Thus I suspect the insert_or_update call is fine, but the controller is firing twice before the DB updates. This could be something on the client side, which I don't want to think about. So I am just going to add a unique key.

A:

As an alternative to @aliCna's answer, if you don't want to change the primary key on CardMeta, you can put a unique index constraint in the database with a migration:
defmodule YourApp.Repo.Migrations.AddCardMetaUniqueIndex do
  use Ecto.Migration

  def change do
    create unique_index(
      :card_meta, 
      [:card_id, :user_id], 
      name: :card_meta_unique_index)
  end
end

Which you can then handle in your changeset to produce nice errors if conflicts occur:
def changeset(struct, params \\ %{}) do
  struct
  |> cast(params, [:last_seen, :difficulty, :prev_interval, :due, :known, :learning,
                    :user_id, :card_id])
  |> assoc_constraint(:user)
  |> assoc_constraint(:card)
  |> unique_constraint(:user_id, name: :card_meta_unique_index)
end

