Routing with a custom slug

I have a record where users can provide an optional slug which is a unique_index to provide a friendly name for the URL, how can I go about using this slug if it’s defined otherwise falling back to the resource’s UUID?

live "/records/:id", RecordsLive.Show

IE: live/records/slug-is-defined or live/records//77?

Hey @travisf you’d simply do something like instead of:

Thing |> where([t], t.id == ^params.id) |> Repo.one

you

Thing |> where([t], t.id == ^params.id or t.slug == ^params.id) |> Repo.one
1 Like

Ecto will likely raise an error if you try to cast a plain string as UUID so I suggest to determine the datatype on the application level:

live "/records/:opaque_id", RecordsLive.Show

def get_record(opaque_id) do
  Record
  |> where(^opaque_id_to_query(opaque_id))
  |> Repo.one()
end

defp opaque_id_to_query(opaque_id) do
  cond do
    match?({_, ""}, Integer.parse(opaque_id)) -> [id: opaque_id]
    match?({:ok, _}, Ecto.UUID.cast(opaque_id)) -> [uuid: opaque_id]
    true -> [slug: opaque_id]
  end
end
1 Like

Maybe my understanding is off but doesn’t using or on different fields cause index not to be used for the second field? Would it not be faster to do a lookup on one then the other?

Repo.get(Thing, id: slug_or_id) || Repo.get(Thing, slug: slug_or_id)

Or does Ecto optimize this?

Not necessarily, using or on different field with index usually result in query optimizer combine these two different index in bitwise or operation. Usually it’s not faster to do lookup one then the other because it cause two DB request network latency.

2 Likes

Oh interesting. What my my old colleague going on about then? lol. I tried an explain with an OR on the same column and it still gave me a sequence scan, so I guess I don’t understand like I thought I did and I have some more RTFMing to do.

Ah, so you’d recommend implementing get_record/1 when the record is saved/updated and then using that field as the param in the route?

This makes sense, but how does that translate into the route? Like if I had a list of Things what would I put in the Routes.live_path/3 as a param?

Can you elaborate what you mean? Are you trying to have a route that contains many ids / slugs ? Or a list of different links each with its own id / slug ?

Sorry that wasn’t clear, I was just unsure of how your solution would translate into Routes.live_path(@socket, MyAppWeb.ThingsLive.Show, ??) that’s where I was confused if either the id or the slug would work as a param there.

Have a look at Phoenix.Param. You could do something like:

defimpl Phoenix.Param, for: YourApp.SomeContext.SomeSchema do
  def to_param(schema) do
    Map.get(schema, :slug, schema.id)
  end
end

To offer some unsolicited advice, I think it’ll be far less of a headache in the future if you make slugs non-nullable fields and auto-generate them based off of some other field as to not force users to specify one. Take it or leave it, of course!

1 Like

It’s been a minute, but I finally got back around to this feature and this solved the problem!

1 Like