Search code examples
phoenix-frameworkecto

Looking up records by a field other than primary key


In Ecto I can easily do this:

Repo.get!(Kaderi.Forum, params["id"])

To look up a record by primary key, which by default is id.

However I'm looking at implementing pretty URLs with a slug instead of an ID. I have a slug field in my model and I can easily use it to generate URLs in Phoenix like so:

defmodule Kaderi.Forum do
  use Kaderi.Web, :model

  @derive {Phoenix.Param, key: :slug}
  ...

But there doesn't seem to be a simple way of automatically looking up records by the slug field.

I can do something like the following:

Repo.get_by!(Kaderi.Forum, slug: params["id"])

But it seems like there should be some nice way of configuring it in the model, so that I can automatically generate URLs to, and then lookup records by slugs without having to touch the controller. If I change how I generate pretty URLs in the future, I shouldn't have to go update the controller again.

Is there some neat Ecto/Phoenix trick I'm missing to do this easily?


Solution

  • You can do this if your slug includes the id (as in 3-my-page instead of just my-page.)

    This is an example from the Ecto.Type documentation copied here for convenience:

    defmodule Permalink do
      @behaviour Ecto.Type
      def type, do: :integer
    
      # Provide our own casting rules.
      def cast(string) when is_binary(string) do
        case Integer.parse(string) do
          {int, _} -> {:ok, int}
          :error   -> :error
        end
      end
    
      # We should still accept integers
      def cast(integer) when is_integer(integer), do: {:ok, integer}
    
      # Everything else is a failure though
      def cast(_), do: :error
    
      # When loading data from the database, we are guaranteed to
      # receive an integer (as databases are strict) and we will
      # just return it to be stored in the model struct.
      def load(integer) when is_integer(integer), do: {:ok, integer}
    
      # When dumping data to the database, we *expect* an integer
      # but any value could be inserted into the struct, so we need
      # guard against them.
      def dump(integer) when is_integer(integer), do: {:ok, integer}
      def dump(_), do: :error
    end
    

    You can then override the primary key for your schema:

    defmodule Post do
      use Ecto.Schema
    
      @primary_key {:id, Permalink, autogenerate: true}
      schema "posts" do
        ...
      end
    end
    

    Now when you call Repo.get(Page, "3-my-page") the string "3-my-page" will be cast to the integer 3 (which is the primary key for the model) and the page will be returned.

    If you don't have the integer in your slug then there is currently no simple way to do this, you would be best to continue using Repo.get_by.