Back
  • 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