Problem

In vielen Fällen möchtest du nicht die interne Tabellen-ID als Teil der URL verwenden - z:b. aus Gründen der Sicherheit oder für bessere Suchmaschinenoptimierung. Hier findest du 4 mögliche Lösungen. Wann du welche Lösung nutzen solltest, findest du in der Diskussion.

Lösung

Die Beispiele 1-3 kannst du direkt in ein Changeset einfügen. Beispiel:

def changeset(%User{} = user, attrs) do
  user
  |> cast(attrs, [...])
  # ...
  |> unique_constraint(:slug)
  |> generate_slug()
end

defp generate_slug(%Ecto.Changeset{} = changeset) do
  # ...
end

Als Datenbank-Typ nutzt du dazu :string. Deine Migration sollte keine leeren Felder unterstützen und keine doppelten Werte zulassen.

alter table(:users) do
  add :slug, :string, null: false
end

create unique_index(:users, [:slug])

1. UUIDs als Slug

Beispiel-Ergebnis: f8bcadc7-6bc3-43d3-a77a-33d5baa0569c

Wenn du Kollisionen vermeiden möchtest, ist dies die einfachste Möglichkeit. Ecto selbst kann UUIDs generieren.

def generate_slug(%Ecto.Changeset{} = changeset) do
  if get_field(changeset, :slug) do
    changeset
  else
    changeset
    |> put_change(:slug, Ecto.UUID.generate())
  end
end

In Beispiel 4 findest du eine Alternative, um UUIDs direkt in der ID-Spalte zu nutzen.

2. Zahlen als Slug

Beispiel-Ergebnis: 1930984453

UUIDs ist der URL sind lang und kürzere URLs sind schöner. Deshalb nutzt das nächste Beispiel Zufallszahlen, aus denen ein Slug mit der Länge 10 ergibt.

defp generate_slug(%Ecto.Changeset{} = changeset) do
  if get_field(changeset, :slug) do
    changeset
  else
    random_number = Enum.random(1_000_000_000..9_999_999_999)

    changeset
    |> put_change(:slug, Integer.to_string(random_number))
  end
end

Bitte beachte, dass hier keine Kollisions-Überprüfung durchgeführt wird.

3. Titel als Slug

Beispiel-Ergebnis: kluski-slaskie-at-jalapeno-bilinguee

Diese Lösung zeigt dir, wie du suchenmaschinenfreundliche Slugs erstellst.

In diesem Fall greife ich auf das Hex-Paket Slugger zurück. Ich hatte zuerst eine Lösung selbst geschrieben. Aber eine wirklich gute Lösung ist aufwändig (Umlaute/UTF-8-Unterstützung). Folgendes Beispiel aus der Dokumentation verdeutlicht dies:

iex> Slugger.slugify_downcase("Kluski Śląskie @ Jalapeño Bilingüe")
"kluski-slaskie-at-jalapeno-bilinguee"

Der Code zur Generierung sieht folgendermaßen aus:

defp generate_slug(%Ecto.Changeset{} = changeset) do
  if get_field(changeset, :slug) do
    changeset
  else
    title = get_field(changeset, :title)
    put_change(changeset, :slug, Slugger.slugify_downcase(title))
  end
end

Tipp: Du kannst dieses Beispiel mit dem Vorherigen kombinieren, um doppelte Titel zu vermeiden. Beispiel: “kluski-slaskie-at-jalapeno-bilinguee-1234”

4. UUIDs als ID

Beispiel-Ergebnis: f8bcadc7-6bc3-43d3-a77a-33d5baa0569c

Da diese Option weniger gebräuchlich ist, rate ich davon ab. Die Dokumentation rät diese Option zu nutzen, wenn die Datenbank keine automatische Nummerierung unterstützt. Geh davon aus, dass auch viele Plugins diese Option nicht untersützen und in einen Fehler laufen.

Das Ergebnis hier ist das Gleiche wie im ersten Beispiel - allerdings ohne zusätzliche Datenbank-Spalte. Wir ersetzen den Integer (:bigserial) durch eine UUID als :id.

Ecto hat dazu eine Option, um den Primärschlüssel auf :binary_id umzustellen.

use Ecto.Schema
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id

In der Konfiguration könnt ihr dazu die Generierung umstellen:

config :app, App.Repo, migration_primary_key: [name: :uuid, type: :binary_id]

Diskussion

Wann nutze ich was?

  • Wenn du Lösung 4 nutzt und die ID-Spalte durch UUIDs ersetzt, solltest du diese in allen Tabellen nutzen.
  • Möchtest du z.B. eine URL erzeugen, mit der sich sich ein Benutzer einloggen kann, sollte diese so schwer zu erraten sein wie möglich. Nutze Beispiel 2.
  • Hast du URLs, die sich nicht mehr ändern sollen und von Benutzern geteilt werde , nutze Beispiel 2. Nehmen wir als Beispiel eine Veranstaltung. UUIDs sind lang und hässlich. Suchenmaschinenfreundliche URLs wären eigentlich schöner. Diese könnten wir aus dem Titel generieren. Allerdings könnte die Veranstaltung noch einmal umbenannt werden oder es schleicht sich Tipp-Fehler ein. Damit müsste man mit einem Slug leben, der nicht mehr zum Titel passt oder du dürftest Weiterleitungen pflegen.
  • In allen anderen Fällen bieten sich suchmaschinenoptimierte Slugs an ;)