phoebe

phoebe

Hi, I’ve been scratching my head around this for a while and trying different combinations, but I’m just stuck whilst using build_assoc which does not recognise binary id UUIDs in my code. Here’s my problem and question.

I’m working on a banking app, so I have a user and wallet. user was created with Pow if that makes any difference. There is a has_many relationship between user and wallet. Here are the schemas:

use MyApp.Schema

schema "users" do
  field :role, :string, null: false, default: "user"
  has_many :wallets, MyApp.Accounts.Wallet
  pow_user_fields()
  timestamps()
end

and

use MyApp.Schema
...
schema "wallets" do
  field :description, :string
  field :balance, Money.Ecto.Composite.Type
  belongs_to :user_id, MyApp.Users.User, references: :uuid, type: :binary_id
  timestamps()
end

where MyApp.Schema is set up for UUIDs:

defmodule MyApp.Schema do
  defmacro __using__(_) do
    quote do
      use Ecto.Schema
      @primary_key {:uuid, :binary_id, autogenerate: true}
      @foreign_key_type :binary_id
      @derive {Phoenix.Param, key: :uuid}
      @timestamps_opts
    end
  end
end

Then in my CreateWallet migration:

defmodule MyApp.Repo.Migrations.CreateWallets do
  use Ecto.Migration

  def change do
    create table(:wallets) do
      add :description, :string, null: false
      add :balance, :money_with_currency, null: false
      add :user_id, references(:users, column: :uuid, type: :binary_id, on_delete: :nothing)
      timestamps()
    end

    create index(:wallets, [:user_id])
  end
end

All standard I think so far. However, if I want to implement a create_user_wallet function:

def create_user_wallet(%User{} = user, %{} = wallet_params) do
  wallet = Ecto.build_assoc(user, :wallets, wallet_params)
  Repo.insert(wallet)
end

and then try to run it, there is an error:

** (KeyError) key :user_uuid not found
    (ecto 3.7.1) lib/ecto/association.ex:754: Ecto.Association.Has.build/3

This is fine if it’s referring to the uuid key in the user map, as I don’t see user_uuid there, it’s just %User{uuid: "1234..."} - or is it referring to user_id in the wallet database? However, why should the build_assoc function expect that and not function as is given the Schema implementation and how can I override it, or set up my schemas and/or own Schema implementation and/or migration correctly?

Thanks for reading this far!

Showing Posts 1 to 9

krasenyp

krasenyp

The problem is in your wallets schema. You’ve defined that each wallet belongs to an user_id but it should be just user.

phoebe

phoebe OP

Thanks @krasenyp - I changed it as you say but the error message still is the same. I think this is because it is the :foreign_key parameter for the belongs_to macro, but what I do not fully understand is why is this not already captured in the implementation of UUIDs in MyApp.Schema? I also changed it to belongs_to :user_uuid in the wallet schema. This now gets past the error message. However, in the docs for belongs_to the macro also adds the suffix _id to the parameter for the :foreign_key and this is now what I see after running

wallet = Ecto.build_assoc(user, :wallets, wallet_params)
# => %MyApp.Accounts.Wallet{
#        __meta__: #Ecto.Schema.Metadata<:built, "wallets">,
#        balance: #Money<:GBP, 0>,
#        description: "GBP",
#        user_uuid: "bbf14364-3808-48b6-b124-a16b7a24fd0b",
#        user_uuid_id: nil,
#        uuid: nil
# }

So, there is something unexpected going on with the user_uuid_id field - I expected this field to be populated, and not user_uuid.

And when I try to insert this in the database with Repo.insert(wallet), I get a new error message about invalid user_uuid field in the changeset.

{:error,
 #Ecto.Changeset<
   action: :insert,
   changes: %{
     balance: #Money<:GBP, 0>,
     description: "GBP",
   },
   errors: [user_uuid: {"is invalid", [type: :map]}],
   data: #MyApp.Accounts.Wallet<>,
   valid?: false
 >}

This kind of makes sense, that should not be there as the valid foreign key should be user_uuid_id if I read the docs correctly? Am I missing something else that’s obvious? Thanks

siddhant3030

siddhant3030

I think in your migration primary key should be false and add a new field for uuid and put primary key true. Also in your schema change this

belongs_to :user_id, MyApp.Users.User, references: :uuid, type: :binary_id

to this

belongs_to :user, MyApp.Users.User

And then you can pass user_id in the changeset.

phoebe

phoebe OP

Thanks @siddhant3030 , I forgot to add that I already have in my config.exs:

config :myapp, MyApp.Repo, migration_primary_key: [name: :uuid, type: :binary_id]

so uuid’s are being correctly generated as primary keys in the wallet and user tables.

al2o3cr

al2o3cr

The errors you are getting suggest that you have a column named user_uuid on the wallets table. You’ll need to tell the belongs_to / has_many machinery about that:

# in the user schema
has_many :wallets, foreign_key: :user_uuid

# in the wallet schema
belongs_to :user, foreign_key: :user_uuid, ...etc
phoebe

phoebe OP

Thanks @al2o3cr ! This now works when I also changed my migration to

create table(:wallets) do
  ...
  add :user_uuid, references(:users, column: :uuid, type: :binary_id, on_delete: :nothing)
 ..
end

This raises the question of what these statements actually do in MyApp.Schema and config of MyApp.Repo. It seems I would be better off being explicit with uuid everywhere?

  # in MyApp.Schema
  @primary_key {:uuid, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  @derive {Phoenix.Param, key: :uuid}
``
Seems I should just keep using `id` as a suffix when using binary ids.
aziz

aziz

I wanted to point the things in MyApp.Schema out to you yesterday :slight_smile:. So if you set :uuid as the field name for the @primary_key, Ecto will use that as the suffix in reference field names. That’s why you get user_uuid in your Wallet schema. See Ecto.Association.association_key/2.

@foreign_key_type :binary_id is correct. Maybe @derive {Phoenix.Param, key: :uuid} is too general to be inside the Schema macro. Unless you can override it in individual schemas, but I don’t know if Elixir allows you to do that.

aziz

aziz

Considering you have the :migration_primary_key configured, adding a column can/should be much shorter:

add :user_uuid, references(:users)

Yep, looks like a lot of trouble to have the uuid name/suffix everywhere. I would just go with id and set its type to :binary_id. :+1: Unless maybe you intend to have tables with integer ids?

phoebe

phoebe OP

Thanks @aziz ! This is clear now, makes sense, cheers!

— All posts loaded —

Where Next? Top

Trending in Questions Top

katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
achenet
Hello, I’m trying to build a basic Phoenix web-app, and I’d like to use Tailwind. However, when I launch mix phx.server, I get an error...
New
kpanic
Hi everyone, I am toying with the idea of building a “match maker” for giving personal help to people that wants to start coding. I sta...
New
Cxx-mlr
I’m working on a small exercise involving update_in/3, and I came up with this solution: data = %{ name: "Periodic Table", category:...
New
ChrisAmelia
I’ve got trouble wrapping my head around the order in which functions are called in this snippet (from Phoenix’s authentication): toke...
New
dillonoconnor
Is there any way to avoid the Hologram compiler running when using iex? It seems like the front-end code could potentially be disregarded...
New
thiagogsr
** (ArgumentError) expected :max_attempts to be a positive integer, got: {:@, [line: 10, column: 19], [{:max_attempts, [line: 10, column:...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New
KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews