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

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New

Other Trending Topics Top

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
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews