now we can replace our hard-coded data with a real DB without changing controller code
- Ecto is Elixir framework for data management; here we'll update Accounts context to use Ecto backed by PgSQL
-
is a wrapper primarily intended for RDBMS
-
has encapsulated Query lang used to build layered queries
-
has
changesetsholding all changes desired to perform on Records encapsulating receiving external data, casting & validating it -
by default we have repo.ex generated Ecto Repository alongwith config for connection to default PgSQL adapter at
./config/underconfig :videologue, Videologue.Repo, .. -
mix ecto.createcreates the DB schema if not already there;mix ecto.migrateto apply migrations
-
can specify Struct with individual fields tied to DB:Table fields through a DSL
-
update Videologue.Accounts.User bare user struct to define Schema
DSL used for Schema is built with
schema&fieldMacros; allowing us to define underlying DB:Table & Elixir Struct together
- now generate a migration for DB to reflect the same structure
mix ecto.gen.migration create_userswhich generates a migration file with timestamp; let's add following to itschange/0action
..
def change do
create table(:users) do
add :name, :string
add :username, :string, null: false
add :password_hash, :string
timestamps()
end
end
- then generate a migration to add index to
users, we could have added this to earlier migration but it's just a better practice to have seemingly single step migrations
mix ecto.gen.migration create_users_index_username
* creating priv/repo/migrations/20210527193448_create_users_index_username.exs
add following to
changeherecreate unique_index(:users, [:username])
here we used
create&addmacros available viause Ecto.MigrationAPI
timestamps()add auto-generatedinserted_at&updated_atfields
-
let's migrate DB with
mix ecto.migrate -
Repo are ready to use through Accounts context; Repository service need to be up & running... Phoenix ensures that by having it in
child_speclist under application.ex
- can add data using IEx as
iex(1)> alias Videologue.Repo
Videologue.Repo
iex(2)> alias Videologue.Accounts.User
Videologue.Accounts.User
iex(3)> %User{username: "sherlock", name: "Sherlock Holmes"} |> Repo.insert()
[debug] QUERY OK db=2.5ms decode=1.7ms queue=0.8ms idle=377.5ms
INSERT INTO "users" ("name","username","inserted_at","updated_at") VALUES ($1,$2,$3,$4) RETURNING "id" ["Sherlock Holmes", "sherlock", ~N[2021-05-28 06:51:43], ~N[2021-05-28 06:51:43]]
{:ok,
%Videologue.Accounts.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
id: 1,
inserted_at: ~N[2021-05-28 06:51:43],
name: "Sherlock Holmes",
updated_at: ~N[2021-05-28 06:51:43],
username: "sherlock"
}}
iex(4)> %User{username: "moriarty", name: "James Moriarty"} |> Repo.insert()
iex(5)> %User{username: "watson", name: "John Watson"} |> Repo.insert()
iex(6)> Repo.all(User) |> Enum.filter(fn x -> x.username =~ "lock" end)
[debug] QUERY OK source="users" db=8.6ms queue=1.6ms idle=1886.3ms
SELECT u0."id", u0."name", u0."username", u0."inserted_at", u0."updated_at" FROM "users" AS u0 []
[
%Videologue.Accounts.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
id: 1,
inserted_at: ~N[2021-05-28 06:51:43],
name: "Sherlock Holmes",
updated_at: ~N[2021-05-28 06:51:43],
username: "sherlock"
}
]
iex(7)> Repo.get(User, 2)
[debug] QUERY OK source="users" db=2.4ms queue=2.2ms idle=1155.0ms
SELECT u0."id", u0."name", u0."username", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [2]
%Videologue.Accounts.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
id: 2,
inserted_at: ~N[2021-05-28 06:52:11],
name: "James Moriarty",
updated_at: ~N[2021-05-28 06:52:11],
username: "moriarty"
}
- can update
Videologue.AccountswithRepo.all/1,Repo.get/2,Repo.get!/2,Repo.get_by/2replacing hard-coding fromalias Videologue.Repo
use Phoenix form builder to enable new user creation
-
add new user account action to VideologueWeb.UserController
-
add
changeset/2function to Videologue.Accounts.User doingcast/2,validate_required/1&validate_lengthimported from Schema module -
add
change_user/1function toVideologue.Accounts.Userwhich gets utilized in ournewaction in Controller -
Videologue.Accounts module itsef shall be only public API our Controllers shall call
-
way of piping in required validations enables flexible stream
-
in router.ex, add
resources "/users", UserController, only: [:index, :show, :new, :create]replacing other/users...routes -
doing
mix phx.routesgives fillowing routes relevant to us
user_path GET /users VideologueWeb.UserController :index
user_path GET /users/new VideologueWeb.UserController :new
user_path GET /users/:id VideologueWeb.UserController :show
user_path POST /users VideologueWeb.UserController :create
page_path GET / VideologueWeb.PageController :index
not limiting resources with
only:will give couple more routes for PUT, PATCH, DELETE, etc.
- add new.html.eex; here using Form Builder to generate required fields in association with Model struct
this introduces a CSRF token for input fields; add proper IDs, Names to them
available at /users/new.html
-
our Form submits data for new user and expects
VideologueWeb.UserController.create/2 -
add Videologue.Accounts
create_user/1to doRepo.insert; then addVideologueWeb.UserController.create/2to call this and redirect to Listing on success else render errors on create page if any
{:ok, user}gets returned on successful Repo insert;{:error, changeset}on erroneous with validation errors
:actionfield of changeset indicates action tried to perform on it; rendering form with any action we know it had validation errors
error_taggets used in form to display these errors at frontend
- in addition to validation errors; Changesets track changes; lets see some IEx work on it
iex(1)> alias Videologue.Accounts.User
Videologue.Accounts.User
iex(2)> changeset = User.changeset(%User{username: "hudson", name: "Mrs Hudson"}, %{})
#Ecto.Changeset<action: nil, changes: %{}, errors: [],
data: #Videologue.Accounts.User<>, valid?: true>
iex(3)> import Ecto.Changeset
Ecto.Changeset
iex(4)> cset = put_change(changeset, :username, "mrshudson")
#Ecto.Changeset<
action: nil,
changes: %{username: "mrshudson"},
errors: [],
data: #Videologue.Accounts.User<>,
valid?: true
>
iex(5)> cset.changes
%{username: "mrshudson"}
iex(6)> get_change(cset, :username)
"mrshudson"
iex(7)> get_change(cset, :name)
nil