-
Phoenix has a convention for seeding data, at seeds.exs. Use Repo to directly add the data we need, then use mix commands.
-
Seed script should be capable of not generating duplicates, as might run multiple times.
- could use something like
Repo.get_by(Category, name: name) || Repo.insert!(%Category{name: name}); although this doesn't take care of Race conditions of being run by multiple users simultaneously
- Upsert is an acceptable tactic; we'll do
on_conflict: :nothingwhich is:raiseby default
add
create_category!/1to Videologue.Multimediaadd default category seed script to
seeds.exs& executemix run priv/repo/seeds.exs
fetch all category names+ids; sort by name, pass them to view via Select input
-
using
import Ecto.Queryallows using macros likefrom, formulate queries likefrom c in Category, select: c.nameand pass it toRepo.allfor a custom query -
Ecto Queries are composable as below 2 chunks are similar in action
Repo.all from c in Category,
order_by: c.name,
select: {c.name, c.id}
query = Category
query = from c in query, order_by: c.name
query = from c in query, select: {c.name, c.id}
Repo.all(query)
- composable form works due to Ecto's
queryableprotocol which acts as base for every new query; allowing usage of Pipes to create Complex queries from Simple
add
alphabetical/1to Videologue.Multimedia.Category addingfrom c in query, order_by: c.nameas queryable and updating queryadd
list_alphabetical_categories/0to Videologue.Multimedia usingalphabetical/1add
category_select_options/1to VideologueWeb.VideoView mapping categories of Assigns sent to it as params into{name, id}tuple listcall this
category_select_optionsin form.html.eex as value toselectfield for category-idbrowse New/Edit, and select to choose category is there now
- when formulating Queries use
^variablefor injecting value/expression for query interpolation
iex(1)> import Ecto.Query
iex(2)> username = "sherlock"
iex(3)> Videologue.Repo.one(from u in Videologue.Accounts.User, where: u.username == username)
** (Ecto.Query.CompileError) unbound variable `username` in query. If you are attempting to interpolate a value, use ^var
(ecto 3.6.1) expanding macro: Ecto.Query.where/3
iex:3: (file)
(ecto 3.6.1) expanding macro: Ecto.Query.from/2
iex:3: (file)
iex(3)> Videologue.Repo.one(from u in Videologue.Accounts.User, where: u.username == ^username)
%Videologue.Accounts.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
id: 1,
inserted_at: ~N[2021-05-28 06:51:43],
name: "Sherlock Holmes",
password: nil,
password_hash: "$pbkdf2-sha512$160000$UGmpw3Qld5JgX6Cv49VNAQ$.m8nqJ.VLyCHulruhlIOr6ic2xUNjNVg4T1CWnxTVOLBuCF3/l5KXuN4KHySFs1D46ORCVBbsuzbkv4UdyQuKA",
updated_at: ~N[2021-05-28 06:51:43],
username: "sherlock"
}
-
Ecto queries do normalization at compile time for performance leveraging info in schemas to cast values at runtime; so a wrong type for
usernamein above example will giveEcto.Query.CastError -
^varis interpolated by Ecto scrubbing & safely placing them to use trying minimize SQL-injection risks -
Ecto Query API supports wide operator range, doc
Comparison:
==, !=, <-, >=, <, >Boolean:
and, or, notInclusion:
inSearch:
like, ilikeNull check:
is_nilAggregates:
count, avg, sum, min, maxDate/time intervals:
datetime_add, date_addGeneral:
fragment, field, type
- moving
Repo.one(from u in User, select: count(u.id), where: ilike(jc.username, ^"j%") or ilike(jc.username, ^"c%"))to Keywords Syntax
users_count = from u in User, select: count(u.id)
j_or_c_users = from jc in users_count, where: ilike(jc.username, ^"j%") or ilike(jc.username, ^"c%")
Repo.one(j_or_c_users)
User
|> select([u], count(u.id))
|> where([u], ilike(jc.username, ^"j%") or ilike(jc.username, ^"c%"))
|> Repo.one()
- Ecto's
query fragmentsends part of query to DB, but allows construct it safely
to lookup user by username in a case-insensitive way; using a fragment allow construct a SQL fragment for Query & safely interpolate
from u in User,
where: fragment("lower(username) = ?", ^String.downcase(name))
- if nothing works, can just run direct SQL using
Ecto.Adapters.SQL.query
Repo |> Ecto.Adapters.SQL.query("SELECT power($1, $2)", [2, 10])
- we don't always need to
Repo.preloadas a separate step; Ecto allows preload associations as part of Query as following
video = Videologue.Repo.one(
from v in Videologue.Multimedia.Video,
limit: 1,
preload: [:user]
)
- can join on associations inside queries
Videologue.Repo.all from v in Video,
join: u in assoc(v, :user),
join: c in assoc(v, :category),
where: c.name == "elixir",
select: {u, v}
- allow use underlying relational DB features to help maintain DB integrity
like check if Category exist before updating Video for provided category-id
our migration ensure
usernameto be unqiue at DB layer; if breached givingEcto.ConstraintError
-
changing Ecto ConstraintError into Changeset error message to allow informing User ina subtle way
-
adding
unique_constraint(:username)to changeset Pipe in user.ex
this ensures
has already been takenerror to be shown in the constraint is breached
- adding
assoc_constraint(:category)toVideo.changesetpipes converts Foreign Key constraint error into human-readable error message alongwith guarantee for valid category-id being used
iex(1)> import Ecto.Query
iex(2)> video = Videologue.Repo.one(from v in Videologue.Multimedia.Video, limit: 1)
iex(3)> changeset = Videologue.Multimedia.Video.changeset(video, %{category_id: 101})
iex(4)> {:error, changeset} = Videologue.Repo.update(changeset)
[debug] QUERY ERROR db=4.9ms queue=0.6ms idle=1984.5ms
UPDATE "videos" SET "category_id" = $1, "updated_at" = $2 WHERE "id" = $3 [101, ~N[2021-05-31 19:01:10], 1]
{:error,
#Ecto.Changeset<
action: :update,
changes: %{category_id: 101},
errors: [
category: {"does not exist",
[constraint: :assoc, constraint_name: "videos_category_id_fkey"]}
],
data: #Videologue.Multimedia.Video<>,
valid?: false
>}
- so use Constraints with Changesets to your easy bug-free UX advantage
Repo.Deleteaccepts Changesets, can useforeign_key_constraintto ensure no Records with to-be deleted Record's key Exist; else provide an error
like Showing user why they can't delete a Category, if there are Videos related to it
- in case of Category, Foreign Key constraint reside at Video table so need to be explicit in Constraint
iex(1)> import Ecto.Changeset
iex(2)> category = Videologue.Repo.get_by(Videologue.Multimedia.Category, name: "erlang")
iex(3)> Videologue.Repo.delete(category)
** (Ecto.ConstraintError) constraint error when attempting to delete struct:
* videos_category_id_fkey (foreign_key_constraint)
...
iex(3)> changeset = change(category)
iex(4)> changeset = foreign_key_constraint(changeset, :videos, name: :videos_category_id_fkey, message: "some videos are using this")
#Ecto.Changeset<action: nil, changes: %{}, errors: [],
data: #Videologue.Multimedia.Category<>, valid?: true>
iex(5)> Videologue.Repo.delete(changeset)
{:error,
#Ecto.Changeset<
action: :delete,
changes: %{},
errors: [
videos: {"some videos are using this",
[constraint: :foreign, constraint_name: "videos_category_id_fkey"]}
],
data: #Videologue.Multimedia.Category<>,
valid?: false
>}
- could configure DB references to either
cascadethe deletions or simply makevideos.category_idtoNULLondeletewith a new migration as
add :category_id, references(:categories, on_delete: :nilify_all)
:nothingis default toon_delete, there is also option:delete_allto delete all linked Foreign recordscan have an external call triggered in Pipe, returning Changeset; adding Foreign field mapping to a non-related TABLE mapping for REVIEW in case the relations are sensitive
if a category is deleted, here shall just show an error to pick something else when loaded
- when nothing can be done by user to fix the error, let it crash with a decent Error Message and landing page; log it to check if something needs fixing or just purging