Problem

Du möchtest einen Timestamp/ein Datum auf die aktuelle Zeit setzen. Diese Logik brauchst du an verschiedenen Stellen. Den Code dazu möchtest du deshalb vereinfachen.

Lösung

Über was sprechen wir genau? - Nehmen wir an, du möchtest dir merken, wann sich ein Benutzer zum letzten Mal eingeloggt hat. Dazu hast du eine Funktion, die folgendermaßen aussehen könnte:

def set_last_signed_at do
  now = DateTime.utc_now() |> DateTime.truncate(:second)

  user
  |> Ecto.Changeset.change({last_signed_in_at: now})
  |> update()
end

Ähnlichen Code findest du nun immer wieder, wenn dir neben inserted_at und updated_at merken möchtest, wann etwas geändert wurde. Wie können wir dies also eleganter lösen?

Wenn du Ecto verwendest, besitzt deine Applikation die Datei repo.ex, die in etwa so aussieht:

defmodule Platform.Repo do
  use Ecto.Repo, otp_app: :platform, adapter: Ecto.Adapters.Postgres
end

In diese Datei werden über ein Macro alle Funktionen eingefügt, die Ecto.Repo zur Verfügung stellt. Diese Datei kannst du weiteren sinnvollen Funktionen erweitern.

Im folgenden findest du du eine überarbeitete Version der obigen Funktion. Dieser kannst du ein Schema und das entsprechende Feld übergeben.

Du siehst übrigens, dass die aktuelle Zeit als Parameter übergeben werden kann. Damit kannst auch eine andere Zeit setzen. Dies ist besonders praktisch in Tests, wenn du nicht genau weißt, zu welcher Zeit die Funktion aufgerufen wurde.

defmodule Platform.Repo do
  use Ecto.Repo, otp_app: :platform, adapter: Ecto.Adapters.Postgres

  @doc """
  Sets the current time in utc for a given field.

  `Repo.touch_utc(%User{}, :last_signed_in_at)`
  """
  def touch_utc(%{__struct__: _} = schema, field, now \\ DateTime.utc_now()) when is_atom(field) do
    update_map = Map.put(%{}, field, DateTime.truncate(now, :second))

    schema
    |> Ecto.Changeset.change(update_map)
    |> update()
  end
end

Code

Ein möglicher Test dazu (mit Elixir 1.9 könnt ihr auch das ~U-Sigil nutzen):

defmodule Platform.RepoTest do
  use Platform.DataCase

  describe "touch_utc" do
    test "sets the current time for an attribute" do
      user = Factory.insert(:user)
      {:ok, now, 0} = DateTime.from_iso8601("2019-01-01T12:00:00Z")

      {:ok, touched_user} =
        user
        |> Repo.touch_utc(:last_signed_in_at, now)

      assert touched_user.last_signed_in_at == now
    end
  end
end