Home Posts Tags Post Search Tag Search

Post 91

LiveView 06 - Chapter 3 Understanding the Generated Core

Published on: 2025-12-09 Tags: elixir, Blog, Side Project, LiveView, Ecto, Html/CSS, Phoenix
Understand the Generated Core
        Within our project we have the boundary and the core, in this case the product is the core and the catalog is the boundary. The catalog is the API through which we will access the products.

        With this setup we want the core to be as predictable as possible. That way the DB is as consistent as possible. We should now look at how the core handles the responsibilities and how the context and core work together to expose an API for database interactions.

        The Product Migration
            Let's head to pento/priv/repo/migrations/..._create_products.exs
                defmodule Pento.Repo.Migrations.CreateProducts do
                    use Ecto.Migration

                    def change do
                        create table(:products) do
                        add :name, :string
                        add :description, :string
                        add :unit_price, :float
                        add :sku, :integer
                        add :user_id, references(:users, type: :id, on_delete: :delete_all)

                        timestamps(type: :utc_datetime)
                        end

                        create index(:products, [:user_id])

                        create unique_index(:products, [:sku])
                    end
                end

            We already ran the migration but to go over it one more time this is the file that will create a new database for the project. Any time you ask mix to create a new migration file they will be created with a "timestamp" so that hey are run in the right order. 
                mix ecto.migrate

        The Product Schema
            Schemas are maps between to kinds of data. Let's head to pento/lib/pento/catalog/product.ex
                schema "products" do
                    field :name, :string
                    field :description, :string
                    field :unit_price, :float
                    field :sku, :integer
                    field :user_id, :id

                    timestamps(type: :utc_datetime)
                end

            Notice the schema expression. With the use above this in the file we are injecting the Ecto.Schema into the file. That then leverages the schema function. The schema function creates an Elixir struct that uses the fields that we set with the initial generator. Let's take some time and spin up a IEX session.
                iex -S mix

                iex(1)> alias Pento.Catalog.Product
                Pento.Catalog.Product
                iex(2)> exports Product
                __changeset__/0     __schema__/1        __schema__/2        __struct__/0        
                __struct__/1        changeset/3

            Let's start the Product.__struct__ let's leverage the struct/2 to create a new Product struct
                iex(4)> struct Product
                %Pento.Catalog.Product{
                    __meta__: #Ecto.Schema.Metadata<:built, "products">,
                    id: nil,
                    name: nil,
                    description: nil,
                    unit_price: nil,
                    sku: nil,
                    user_id: nil,
                    inserted_at: nil,
                    updated_at: nil
                }

            Now let's pass in some data to the struct that we just created
                iex(5)> struct(Product, name: "Exploding Ninja Cows")
                %Pento.Catalog.Product{
                    __meta__: #Ecto.Schema.Metadata<:built, "products">,
                    id: nil,
                    name: "Exploding Ninja Cows",
                    description: nil,
                    unit_price: nil,
                    sku: nil,
                    user_id: nil,
                    inserted_at: nil,
                    updated_at: nil
                }

            Now let's move onto the changeset. This is best used to make safe maps that will be able to be added into the database

            "Changesets"
                Same file as the last one let's start to look at the next block of code.
                    @doc false
                    def changeset(product, attrs, user_scope) do
                        product
                        |> cast(attrs, [:name, :description, :unit_price, :sku])
                        |> validate_required([:name, :description, :unit_price, :sku])
                        |> unique_constraint(:sku)
                        |> put_change(:user_id, user_scope.user.id)
                    end

                We start with the product, then cast the keys that are set in the list, then validate that we have the required keys, then make sure that we have a unique sku, then add in the user that made the change to the product. 

            "Test Drive the Schema"
                Now let's keep the iex session going and deal with an other layer of the schema. Let's create a new product and add it into the database.
                    iex(6)> alias Pento.Catalog.Product
                    Pento.Catalog.Product
                    iex(7)> product = %Product{}
                    %Pento.Catalog.Product{
                        __meta__: #Ecto.Schema.Metadata<:built, "products">,
                        id: nil,
                        name: nil,
                        description: nil,
                        unit_price: nil,
                        sku: nil,
                        user_id: nil,
                        inserted_at: nil,
                        updated_at: nil
                    }

                Let's set a valid map of a single product
                    iex(8)> attrs = %{
                    ...(8)> name: "Pentominoes",
                    ...(8)> sku: 123456,
                    ...(8)> unit_price: 5.00,
                    ...(8)> description: "A super fun game!"
                    ...(8)> }
                    %{
                        name: "Pentominoes",
                        description: "A super fun game!",
                        unit_price: 5.0,
                        sku: 123456
                    }

                Now let's turn that into a changeset, now the book says that there should be a changeset/2 but with the generated code there is only a changeset 3, that includes the user_scope as a parameter. So before you run the next line add this to the product.ex, think about deleting it after we move on from this chapter.
                    @doc false
                    def changeset(product, attrs) do
                        product
                        |> cast(attrs, [:name, :description, :unit_price, :sku])
                        |> validate_required([:name, :description, :unit_price, :sku])
                        |> unique_constraint(:sku)
                    end

                    iex(4)> Product.changeset(product, attrs)
                    #Ecto.Changeset<
                    action: nil,
                    changes: %{
                        name: "Pentominoes",
                        description: "A super fun game!",
                        sku: 123456,
                        unit_price: 5.0
                    },
                    errors: [],
                    data: #Pento.Catalog.Product<>,
                    valid?: true,
                    ...
                    >
                    
                Now that we have a changeset let's add it into the database.
                    iex(5)> alias Pento.Repo
                    Pento.Repo
                    iex(6)> Product.changeset(product, attrs) |> Repo.insert()
                    [debug] QUERY OK source="products" db=9.2ms decode=0.9ms queue=4.7ms idle=375.1ms
                    INSERT INTO "products" ("name","description","sku","unit_price","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5,$6) RETURNING "id" ["Pentominoes", "A super fun game!", 123456, 5.0, ~U[2025-12-09 00:58:12Z], ~U[2025-12-09 00:58:12Z]]
                    ↳ :elixir.eval_external_handler/3, at: src/elixir.erl:386
                    {:ok,
                    %Pento.Catalog.Product{
                        __meta__: #Ecto.Schema.Metadata<:loaded, "products">,
                        id: 1,
                        name: "Pentominoes",
                        description: "A super fun game!",
                        unit_price: 5.0,
                        sku: 123456,
                        user_id: nil,
                        inserted_at: ~U[2025-12-09 00:58:12Z],
                        updated_at: ~U[2025-12-09 00:58:12Z]
                    }} 

                Now we can try the same thing with a invalid product.
                    iex(8)> invalid_attrs = %{name: "Not a valid game"}
                    %{name: "Not a valid game"}
                    iex(9)> Product.changeset(product, invalid_attrs)
                    #Ecto.Changeset<
                    action: nil,
                    changes: %{name: "Not a valid game"},
                    errors: [
                        description: {"can't be blank", [validation: :required]},
                        unit_price: {"can't be blank", [validation: :required]},
                        sku: {"can't be blank", [validation: :required]}
                    ],
                    data: #Pento.Catalog.Product<>,
                    valid?: false,
                    ...
                    >

                Now let's add in an other validation for the product and make sure that the price has to be over 0.00.
                    |> validate_number(:unit_price, greater_than: 0.0)

                Recompile and let's keep going
                    iex(14)> recompile()
                    iex(15)> invalid_price_attrs = %{
                    ...(15)> name: "Pentominoes",
                    ...(15)> sku: 123456,
                    ...(15)> unit_price: 0.00,
                    ...(15)> description: "A super fun game!"}
                    %{
                    name: "Pentominoes",
                    description: "A super fun game!",
                    sku: 123456,
                    unit_price: 0.0
                    }
                    iex(16)> Product.changeset(product, invalid_price_attrs)
                    #Ecto.Changeset<
                    action: nil,
                    changes: %{
                        name: "Pentominoes",
                        description: "A super fun game!",
                        sku: 123456,
                        unit_price: 0.0
                    },
                    errors: [
                        unit_price: {"must be greater than %{number}",
                        [validation: :number, kind: :greater_than, number: 0.0]}
                    ],
                    data: #Pento.Catalog.Product<>,
                    valid?: false,
                    ...
                    >