- Checking Processing Time of a Function
Erlang provides the :timer module for handling various timing-related operations. One of its useful functions is tc/1, which allows you to measure the execution time of a function. It returns the time taken by the function in microseconds and the result.
def you_custom_function do {time, result} = :timer.tc(fn -> Your function logic.... end) time_sec = time / 1_000_000 Logger.info("Processing time: #{Float.round(time_sec, 2)}s") result end
- Implementing Custom Validation in Ecto: Requiring At Least One Field
How to create changeset validations in Ecto that ensure at least one of multiple fields is present.
import Ecto.Changeset schema "posts" do field :title, :string field :body, :string field(:deleted_at, :utc_datetime) timestamps(type: :utc_datetime) end @doc false def changeset(posts, attrs) do posts |> cast(attrs, [:title, :body]) |> validate_required_one_of([:title, :body]) end def validate_required_one_of(changeset, fields) do # Determines whether a field is missing in a changeset. missing_fields = Enum.filter(fields, &field_missing?(changeset, &1)) case missing_fields do # If missing fields returns the same fields, it means not one of them were filled. # So we should return a changeset error. missing_fields when missing_fields == fields -> # Adding errors to each field add_errors(changeset, fields, fields) _ -> changeset end end def add_errors(changeset, _fields, []), do: changeset def add_errors(changeset, fields, [head | tail]) do new_changeset = add_error(changeset, head, "at least one of #{Enum.map_join(fields, " or ", &Atom.to_string(&1))} must be present") add_errors(new_changeset, fields, tail) end
- Soft deletes with Ecto and PostgreSQL
With soft deletion rule, doing Repo.delete() will only mark your record as deleted through delete_at column, instead of effectively deleting it from the database. This tutorial only manually exclude deleted records, if you don't want to do that you can create a separate view following instructions here.
✅ Migration alter table(:your_records) do add :deleted_at, :utc_datetime end execute """ CREATE OR REPLACE RULE soft_deletion AS ON DELETE TO your_records DO INSTEAD UPDATE your_records SET deleted_at = NOW() WHERE id = OLD.id AND deleted_at IS NULL; """, """ DROP RULE IF EXISTS soft_deletion ON your_records; """ ✅ Schema schema "your_records" do ...other fields field(:deleted_at, :utc_datetime) <- Add this end # Query def delete_your_record(%YourRecord{} = your_record) do # Since the deletion does not happen, PostgreSQL returns the information that zero rows were affected. # you can opt-in and say that stale entries are expected: Repo.delete(your_record, allow_stale: true) end ✅ Manually excluding records with deleted_at value. def list_your_records do YourRecord |> deleted?(false) |> Repo.all() end def get_your_record!(id) do YourRecord |> deleted?(false) |> Repo.get!(id) end def deleted?(query, false) do from u in query, where: is_nil(u.deleted_at) end def deleted?(query, _), do: query
- Use FunWithFlags, or other feature flag libraries.
Developers should know how to use feature flagging tools like funwithflags rather than manually disabling features. But in both cases please don't forget to enable it before or while going live.
- N+1 Query problem in Elixir
Occurs when you execute one query to retrieve a list of records then, for each record, you execute another query. To avoid/solve this, use Ecto's preload or join functions to load associated data in a single query.
alias MyApp.{ Author, Post } ❌ EXAMPLE: # Fetch all authors authors = Repo.all(Author) # For each author, fetch their posts (N+1 queries) authors_with_posts = authors |> Enum.map(fn author -> %{author | posts: Repo.all(from p in Post, where: p.author_id == ^author.id)} end) ✅ DO THIS INSTEAD: # Fetch all authors with their posts in a single query authors_with_posts = Repo.all(from a in Author, preload: [:posts]) OR authors_with_posts = from(a in Author, join: p in Post, on: p.author_id == a.id) |> Repo.all()
- The right way to update Elixir structs (and how not to do it)
Since structs are maps, we might be tempted to use Map.put/3, it works until you make a typo. more info.
iex> user = %User{name: "Alvin Rapada", admin: true} ✅ DO THIS: iex> user |> Map.replace!(:amdin, false) ** (KeyError) key :amdin not found in: %User{name: "Alvin Rapada", admin: true}. Did you mean: * :admin * ❌ INSTEAD OF THIS: iex> user |> Map.put(:amdin, false) # <- TYPO!! %{name: "Alvin Rapada", admin: true, amdin: false}
- Goodbye IO.Inspect(). Hello dbg()
Elixir 1.14 introduced dbg, a debugging helper that is equally easy to user as IO.inspect, but much more powerful. dbg is also aware of Elixir code, so it can inspect an entire pipelines.
params = %{name: "alvin", email: "alvin@email.com"} ✅ DO THIS: iex> dbg(params) [iex:11: (file)] params #=> %{name: "alvin", email: "alvin@email.com"} ❌ INSTEAD OF THIS: iex> IO.inspect(params) %{name: "alvin", email: "alvin@email.com"}
- If a function requires more than two parameters, use Map instead.
Having to hop back and forth to remember the order of parameters on a function is not ideal. Key-value pairs in a map do not follow any order.
✅ DO THIS: create_user(%{first_name: "Alvin", last_name: "Rapada", email: "alvnrapada@gmail.com"}) def create_user(%{email: email, last_name: last_name, first_name: first_name}) do # function body... end ❌ INSTEAD OF THIS: create_user("Alvin", "Rapada", "alvnrapada@gmail.com") def create_user(first_name, last_name, email) do # function body... end