CRUD with belongs_to and has_one / has_many - empty id field

Hi.
I want to create a CRUD where I could create a user and a token associated with the user and created the second one only if the user exists (foreign key constraint).

I started with migrations:

  • priv/repo/migraions/20190530142317_create_users.exs
defmodule Backend.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :first_name, :string, null: false
      add :second_name, :string, null: false
      add :email, :string, null: false, unique: true
      add :password, :string, null: false
      add :is_active, :boolean, default: false, null: false

      timestamps()
    end

    create unique_index(:users, [:email])
  end
end
  • priv/repo/migraions/20190607183748_create_tokens.exs
defmodule Backend.Repo.Migrations.CreateTokens do
  use Ecto.Migration

  def change do
    create table(:tokens) do
      add :token, :string

      add :user_id, references(:users)
      timestamps()
    end

  end
end

And follow up with schemas:

  • lib/backend/auth/user.ex
defmodule Backend.Auth.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :first_name, :string, null: false
    field :second_name, :string, null: false
    field :email, :string, null: false, unique: true
    field :password, :string, null: false
    field :is_active, :boolean, default: true

    has_one :tokens, Auth.Backend.Token
    timestamps()
  end

  @doc false
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:first_name, :second_name, :email, :password, :is_active])
    |> validate_required([:email, :password, :first_name, :second_name])
    |> unique_constraint(:email, name: :users_email_index)
  end
end
  • lib/backend/auth/token.ex
defmodule Backend.Auth.Token do
  use Ecto.Schema
  import Ecto.Changeset

  schema "tokens" do
    field :token, :string

    belongs_to :users, Backend.Auth.User
    timestamps()
  end

  @doc false
  def changeset(token, attrs) do
    token
    |> cast(attrs, [:token])
    |> cast_assoc(:users)
    |> assoc_constraint(:users)
    |> validate_required([:token])
  end
end

I access the token create endpoint with data as follows:

{
	"token": {
		"token": "token_test123",
		"user_email": "wor@kth.se",
		"user_id": 1
	}
}

I get back status 201 Created, but looking up the record in the database the user_id field is empty.
What am I missing?

Regards,
Wojciech

I think you need two small changes.

  1. The migration for token table should have a user_id is not null constraint, just to enforce it at the DB level.

     add :user_id, references(:users, on_delete: :delete_all), null: false 
    
  2. The association should be singular inside the token schema.

       belongs_to :user, Backend.Auth.User
    

Having :users in belongs_to makes Ecto think users_id is the foreign key, not user_id. I haven’t actually run it so I’m not entirely sure, but I think it should work with these changes.

Edit: I think has_one in the User schema too should have :token instead of :tokens.

3 Likes

@ryloric suggestions are spot_on for making changes to association names as singular, though migration change is a better suggestion but not mandatory.

Use of cast_assoc/3 is not relevant here as assumption is that user would already be there and no intent to create user along with token

Doing a quick Google search, took me to blog post here which defines aptly the use of cast_assoc.

You have 2 options to make above work,

  • use either put_assoc/4 but it would require you to load the user record first and pass to changeset so extra work
  • easy one is to cast user_id along with token which is already there in api params payload. So assoc_constraint gets you covered with actual record in users table with id: 1 in above example

So this should work

def changeset(token, attrs) do
    token
    |> cast(attrs, [:token, :user_id])
    |> validate_required([:token])
    |> assoc_constraint(:user)
  end

Here is a similar example straight from docs https://hexdocs.pm/ecto/Ecto.Changeset.html#assoc_constraint/3

4 Likes