Associate a user with the Business

Hello. I’m new to Phoenix and trying to associate a user_id to the business context.

Here is the users schema generated by phx.gen.auth LiveView:

defmodule Balaio.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset
  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  schema "users" do
    field :email, :string
    field :password, :string, virtual: true, redact: true
    field :hashed_password, :string, redact: true
    field :confirmed_at, :naive_datetime

    timestamps()
  end

And here is the business schema along with its changeset. Notice it belongs to a User.

defmodule Balaio.Catalog.Business do
  use Ecto.Schema
  import Ecto.Changeset

  alias Balaio.Accounts.User

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id

  schema "business" do
    field :name, :string
    field :address, :string
    field :description, :string
    field :category, :string
    field :phone, :string
    field :thumbnail, :string
    field :is_delivery, :boolean, default: false

    belongs_to :user, User

    timestamps()
  end

  @doc false
  def changeset(business, attrs) do
    business
    |> cast(attrs, [
      :name,
      :description,
      :phone,
      :address,
      :category,
      :thumbnail,
      :is_delivery,
      :user_id
    ])
    |> validate_required([
      :name,
      :description,
      :phone,
      :address,
      :category,
      :thumbnail,
      :is_delivery,
      :user_id
    ])
    |> unique_constraint(:user_id)
  end
end

And on the FormComponent, I’ve added the @form[:user_id] hidden field.

def render(assigns) do
    ~H"""
    <div>
      <.simple_form
        for={@form}
        id="business-form"
        phx-target={@myself}
        phx-change="validate"
        phx-submit="save"
      >
        <.input field={@form[:name]} type="text" label="Name" />
        <.input field={@form[:description]} type="text" label="Description" />
        
        <.input field={@form[:user_id]} type="hidden" /> <-- Added this line
        <:actions>
          <.button phx-disable-with="Saving...">Save Business</.button>
        </:actions>
      </.simple_form>
    </div>
    """
  end

But I don’t know how I might fill the user_id property :slightly_smiling_face:

I really appreciate any help :hugs:

Passing user_id through the form will allow a user who manipulates the DOM to create a Business for other users. If this isn’t intended, you could instead set the value directly when building the changeset.

Instead of code like:

changeset =
  %Business{}
  |> Business.changeset(attrs)
  |> etc

you could write:

changeset =
  %Business{user_id: socket.assigns.current_user.id}
  |> Business.changeset(attrs)
  |> etc
4 Likes

As an alternative I’m partial to letting Ecto build the association, provided that the User schema has the reverse relationship back to the Business with has_one :business, Business or has_many.

socket.assigns.current_user
|> Ecto.build_assoc(:business)
|> Business.changeset(params)

But not all relationships should be bidirectional so it really depends on the use case whether this technique is available.

2 Likes

Regardless of the method you use to programmatically add the user id to the Business schema, like mentioned above you probably don’t want to send the user id as a hidden form input, so then you don’t need to include its atom in the cast or validate_required lists in the changeset function.

Reason being that cast is used to mark changes to the schema passed in through the business variable, by coercing data provided from the outside world and creating a schema to validate further, and our examples are building the base schema with the user id already set, so there’s no need to change it.

Otherwise I think Ecto will fail the validation if it doesn’t get a user id key in the attrs passed from the client params.

In your forms “save” function, you can update the params like this:

    business_params = business_params 
      |> Map.put("user_id", socket.assigns.current_user.id)

     case Business.create_business(business_params) do 
     - rest of your code -

Map.Put(“FIELD”, VALUE) can be used to assign socket values to fields in the database.

You can pipe multiple values if needs be as well. Not sure on the pros and cons of this method, but it works nicely for me and in general looks quite neat imo.

2 Likes

I think the cons would be relying on what could be seen as an implementation detail, in the sense that if you have to do this kind of manipulation in a number of controllers then it’d be better to abstract it to a business logic-level context function or some other boundary level that makes sense. That way if the association needs to change in the future you don’t have to hunt down all the places in the application where you’re directly referencing the key.

Also, since the schemas are where the relationships are set, rather let Ecto deal with setting things up.

But there are places in my own controllers where I’m directly changing the parameters like in your example, so it’s not wrong as long as we keep in mind the tradeoffs.

1 Like

Ahh interesting. I was never entirely sure on the best method but I saw someone do it like that when I was first learning so its stuck.

Effectively though, if I can do it at a higher level I should do it at a higher level as there’s less room for errors if changes are made.

Thanks for the input.

1 Like

Thank you all for your answers. I’m trying the approaches raised here.

As I’m a beginner, working on baby steps firsts while absorbing the core concepts.

1 Like

@03juan thanks for your answer. I’m stuck here :slightly_smiling_face:

This is my changeset and I’m not sure how to tell it how to insert the user_id property.

def changeset(business, attrs) do
    business
    |> cast(attrs, [
      :name,
      :description,
      :phone,
      :address,
      :category,
      :thumbnail,
      :is_delivery
    ])
    |> validate_required([
      :name,
      :description,
      :phone,
      :address,
      :category,
      :thumbnail,
      :is_delivery
    ])
    |> unique_constraint(:user_id)
  end

Sorry I’m not sure exactly what you mean by this, are you getting a database or system error?

Can you share the steps you’re taking, what you’re expecting to see and how the result differs?

If you’re new to Phoenix, and maybe even Elixir, I would advise to play around in the iex interactive shell first, especially to get used to the core concepts of Phoenix contexts and Ecto schemas, changesets and repo functions, but in general to learn about any library. I do this all the time.

Have a look at the code samples in the documentation for all the Ecto functions in your code and others mentioned in this thread.

Basically try out all the core pieces first before adding the extra complexity of components, forms, params, etc.

1 Like

Thanks for your great advice. I’m now working on step by step, using the iex interactive shell and studying how things works :blush:

1 Like