We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
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,
...
>