WordPress: Many-to-Many Beziehungen zwischen Custom Post Types programmatisch erstellen – So geht's!

Zurück zur Blog-Übersicht
WordPress: Many-to-Many Beziehungen zwischen Custom Post Types programmatisch erstellen – So geht's!
Eric Menge

In meiner täglichen Arbeit mit WordPress stoße ich immer wieder auf die Notwendigkeit, komplexe Datenstrukturen abzubilden. Custom Post Types (CPTs) sind dabei ein Segen, aber was, wenn wir Beziehungen zwischen ihnen herstellen müssen, die über einfache Parent-Child-Verknüpfungen hinausgehen? Speziell Many-to-Many-Beziehungen, wie beispielsweise Autoren, die mehrere Bücher geschrieben haben, und Bücher, die von mehreren Autoren verfasst wurden, können eine Herausforderung darstellen. Viele greifen hier schnell zu mächtigen Plugins wie Advanced Custom Fields Pro (ACF Pro) oder Meta Box mit dem MB Relationships Add-on. Doch was, wenn man volle Kontrolle behalten, Abhängigkeiten reduzieren oder einfach die Interna verstehen möchte? Genau darum geht es in diesem Artikel: Wie erstelle ich programmatisch Many-to-Many-Beziehungen zwischen CPTs in WordPress?

Ich zeige Ihnen die gängigen Ansätze und wie Sie diese ohne zusätzliche Premium-Plugins umsetzen können. Das erfordert zwar etwas mehr Code, gibt Ihnen aber ultimative Flexibilität und ein tieferes Verständnis Ihrer WordPress-Installation.

Warum überhaupt programmatisch und ohne Plugins?

Bevor wir in den Code eintauchen, fragen Sie sich vielleicht, warum dieser Mehraufwand? Es gibt gute Gründe:

  • Performance: Weniger Plugins bedeuten oft weniger Overhead und potenziell schnellere Ladezeiten, da nur der Code geladen wird, den Sie wirklich benötigen.
  • Kontrolle: Sie haben die volle Kontrolle über die Datenbankstruktur, die Abfragen und die Darstellung. Keine Blackbox-Funktionalität.
  • Unabhängigkeit: Sie machen sich nicht von der Weiterentwicklung oder den Lizenzmodellen Dritter abhängig.
  • Lernfaktor: Sie verstehen die WordPress Core-Funktionen besser und erweitern Ihre Entwicklerkompetenzen.
  • Spezifische Anforderungen: Manchmal haben Projekte so spezielle Anforderungen, dass Standard-Plugins an ihre Grenzen stoßen.

Natürlich haben Plugins ihre Berechtigung und können viel Zeit sparen. Für Projekte, bei denen jedoch Granularität und Optimierung im Vordergrund stehen, ist der programmatische Weg oft der bessere.

Die Herausforderung: Many-to-Many verstehen

Eine Many-to-Many-Beziehung (oft als N:M-Beziehung bezeichnet) liegt vor, wenn ein Datensatz eines Typs mit mehreren Datensätzen eines anderen Typs verknüpft sein kann, und umgekehrt. Ein klassisches Beispiel:

  • Ein Buch kann von mehreren Autoren geschrieben worden sein.
  • Ein Autor kann mehrere Bücher geschrieben haben.

In einer relationalen Datenbank löst man dies typischerweise über eine dritte Tabelle, eine sogenannte Verbindungstabelle (oder Junction Table, Zwischentabelle), die die IDs der beiden zu verknüpfenden Entitäten speichert.

Lösungsansätze für programmatische Many-to-Many-Beziehungen in WordPress

Es gibt im Wesentlichen zwei Hauptansätze, um dies in WordPress programmatisch umzusetzen:

  1. Verwendung von Post Meta: Hierbei speichern wir Arrays von Post-IDs im Post-Meta-Feld eines oder beider CPTs.
  2. Verwendung einer eigenen Verbindungstabelle: Dies ist der "sauberere" datenbanktechnische Ansatz und skaliert oft besser.

Schauen wir uns beide genauer an.

Ansatz 1: Beziehungen über Post Meta speichern

Dieser Ansatz ist oft schneller zu implementieren, da er auf vorhandenen WordPress-Funktionen aufbaut. Nehmen wir unser Beispiel mit "Büchern" (CPT: buch) und "Autoren" (CPT: autor).

Wenn Sie ein Buch bearbeiten, könnten Sie eine Metabox anzeigen, in der Sie mehrere Autoren auswählen können. Die IDs der ausgewählten Autoren speichern Sie dann als Array in einem Post-Meta-Feld des Buches, z.B. _buch_autoren_ids.


// Beispielhaftes Speichern beim Aktualisieren eines 'buch' Posts
// Angenommen, $autor_ids ist ein Array von Autor-Post-IDs aus einer Metabox
// $post_id ist die ID des aktuellen Buches
update_post_meta($post_id, '_buch_autoren_ids', $autor_ids);

// Umgekehrt könnte man auch für jeden Autor die Bücher speichern
// update_post_meta($autor_id, '_autor_buecher_ids', $buch_ids);
// Dies führt jedoch zu Redundanz und ist fehleranfälliger.
// Besser ist es, die Beziehung nur von einer Seite zu speichern und
// bei Bedarf komplexer abzufragen.

Abfragen von verknüpften Posts:

Um alle Bücher eines bestimmten Autors zu finden (wenn die IDs im Buch gespeichert sind), wird es etwas kniffliger. Sie müssten alle Bücher durchgehen und prüfen, ob die Autoren-ID im Meta-Feld enthalten ist. Eine meta_query mit COMPARE = 'LIKE' ist hier möglich, aber nicht ideal für serialisierte Arrays. Besser ist es, wenn Sie jede ID einzeln speichern, falls Sie so abfragen müssen, oder die Beziehung von beiden Seiten pflegen (was wie gesagt zu Redundanz führen kann).

Wenn Sie alle Autoren eines Buches anzeigen wollen, ist es einfach:


// Autoren eines Buches ($buch_id) abrufen
$autor_ids = get_post_meta($buch_id, '_buch_autoren_ids', true); // true für einzelnes Ergebnis
if (!empty($autor_ids) && is_array($autor_ids)) {
  $args = array(
    'post_type' => 'autor',
    'post__in' => $autor_ids,
    'orderby' => 'title',
    'order' => 'ASC',
    'posts_per_page' => -1
  );
  $autoren_query = new WP_Query($args);
  // Loop durch $autoren_query->posts ...
}

Nachteile dieses Ansatzes:

  • Abfragen können komplex und weniger performant werden, besonders wenn Sie "rückwärts" suchen (z.B. alle Bücher eines Autors, wenn die IDs nur beim Buch gespeichert sind).
  • Serialisierte Daten in Meta-Feldern sind für Datenbankabfragen nicht optimal.
  • Metadaten wie "Rolle des Autors bei diesem spezifischen Buch" (z.B. Hauptautor, Co-Autor) sind schwer abzubilden.

Ansatz 2: Eigene Verbindungstabelle in der Datenbank

Dieser Ansatz ist robuster, skaliert besser und entspricht eher dem Standardvorgehen in relationalen Datenbanken. Wir erstellen eine eigene Tabelle, die die Verknüpfungen speichert.

Unsere Tabelle könnte wp_buch_autor_beziehungen heißen und folgende Spalten haben:

  • beziehung_id (Primary Key, AUTO_INCREMENT)
  • buch_id (BIGINT, Foreign Key zur wp_posts.ID)
  • autor_id (BIGINT, Foreign Key zur wp_posts.ID)
  • Optional: reihenfolge (INT, für geordnete Beziehungen)
  • Optional: meta_daten (TEXT, für zusätzliche Infos zur spezifischen Beziehung)

Erstellen der Tabelle (z.B. bei Plugin-/Theme-Aktivierung):


function meine_plugin_aktivierung() {
    global $wpdb;
    $table_name = $wpdb->prefix . 'buch_autor_beziehungen';
    $charset_collate = $wpdb->get_charset_collate();

    $sql = "CREATE TABLE $table_name (
        beziehung_id bigint(20) NOT NULL AUTO_INCREMENT,
        buch_id bigint(20) NOT NULL,
        autor_id bigint(20) NOT NULL,
        reihenfolge int(11) DEFAULT 0 NOT NULL,
        PRIMARY KEY  (beziehung_id),
        KEY buch_id (buch_id),
        KEY autor_id (autor_id)
    ) $charset_collate;";

    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    dbDelta($sql);
}
// register_activation_hook(__FILE__, 'meine_plugin_aktivierung'); // Wenn in einem Plugin
// add_action('after_switch_theme', 'meine_plugin_aktivierung'); // Wenn in einem Theme

Verknüpfungen hinzufügen, löschen und abfragen:

Hierfür verwenden wir $wpdb-Methoden.


global $wpdb;
$table_name = $wpdb->prefix . 'buch_autor_beziehungen';

// Beziehung hinzufügen
function add_buch_autor_beziehung($buch_id, $autor_id) {
    global $wpdb;
    $table_name = $wpdb->prefix . 'buch_autor_beziehungen';
    // Zuerst prüfen, ob Beziehung schon existiert, um Duplikate zu vermeiden
    $exists = $wpdb->get_var($wpdb->prepare(
        "SELECT beziehung_id FROM $table_name WHERE buch_id = %d AND autor_id = %d",
        $buch_id, $autor_id
    ));
    if (!$exists) {
        $wpdb->insert(
            $table_name,
            array(
                'buch_id' => $buch_id,
                'autor_id' => $autor_id,
            ),
            array('%d', '%d')
        );
    }
}

// Beziehungen für ein Buch löschen (z.B. vor dem Neuspeichern aus Metabox)
function delete_beziehungen_fuer_buch($buch_id) {
    global $wpdb;
    $table_name = $wpdb->prefix . 'buch_autor_beziehungen';
    $wpdb->delete($table_name, array('buch_id' => $buch_id), array('%d'));
}

// Alle Autoren für ein Buch abrufen
function get_autoren_fuer_buch($buch_id) {
    global $wpdb;
    $table_name = $wpdb->prefix . 'buch_autor_beziehungen';
    $autor_ids = $wpdb->get_col($wpdb->prepare(
        "SELECT autor_id FROM $table_name WHERE buch_id = %d ORDER BY reihenfolge ASC",
        $buch_id
    ));
    if (empty($autor_ids)) {
        return array();
    }
    // Jetzt die Post-Objekte der Autoren holen
    $args = array(
        'post_type' => 'autor',
        'post__in' => $autor_ids,
        'orderby' => 'post__in', // Behält die Reihenfolge aus $autor_ids bei, wenn $autor_ids sortiert war
        'posts_per_page' => -1
    );
    return get_posts($args);
}

// Alle Bücher für einen Autor abrufen
function get_buecher_fuer_autor($autor_id) {
    global $wpdb;
    $table_name = $wpdb->prefix . 'buch_autor_beziehungen';
    $buch_ids = $wpdb->get_col($wpdb->prepare(
        "SELECT buch_id FROM $table_name WHERE autor_id = %d ORDER BY reihenfolge ASC", // Annahme: reihenfolge ist hier relevant
        $autor_id
    ));
     if (empty($buch_ids)) {
        return array();
    }
    $args = array(
        'post_type' => 'buch',
        'post__in' => $buch_ids,
        'orderby' => 'post__in',
        'posts_per_page' => -1
    );
    return get_posts($args);
}

Implementierung im WordPress Backend: Metaboxen

Egal für welchen Ansatz Sie sich entscheiden, Sie benötigen eine Benutzeroberfläche im WordPress-Adminbereich, um die Verknüpfungen zu erstellen und zu bearbeiten. Metaboxen sind hier das Mittel der Wahl.

Mit add_meta_box() fügen Sie eine neue Box zur Bearbeitungsseite Ihres CPTs hinzu. Innerhalb dieser Metabox können Sie dann z.B. eine Multi-Select-Liste, Checkboxen oder ein ausgefeilteres Suchfeld anzeigen, um die zu verknüpfenden Posts auszuwählen.

Der Inhalt der Metabox könnte so aussehen (vereinfacht):


function meine_buch_autoren_metabox_callback($post) {
    // Nonce für Sicherheit
    wp_nonce_field('meine_buch_autoren_speichern', 'meine_buch_autoren_nonce');

    // Aktuell verknüpfte Autoren laden (Beispiel für Ansatz 2)
    global $wpdb;
    $table_name = $wpdb->prefix . 'buch_autor_beziehungen';
    $aktuelle_autor_ids = $wpdb->get_col($wpdb->prepare(
        "SELECT autor_id FROM $table_name WHERE buch_id = %d",
        $post->ID
    ));

    // Alle verfügbaren Autoren laden
    $alle_autoren = get_posts(array('post_type' => 'autor', 'posts_per_page' => -1, 'orderby' => 'title', 'order' => 'ASC'));

    echo '<p>Wählen Sie die Autoren für dieses Buch:</p>';
    echo '<select name="buch_autoren_ids[]" multiple="multiple" style="width:100%; min-height:150px;">';
    foreach ($alle_autoren as $autor) {
        $selected = in_array($autor->ID, $aktuelle_autor_ids) ? 'selected' : '';
        echo '<option value="' . esc_attr($autor->ID) . '" ' . $selected . '>' . esc_html($autor->post_title) . '</option>';
    }
    echo '</select>';
}

// Speichern der Metabox-Daten
function meine_buch_autoren_meta_speichern($post_id) {
    // Nonce prüfen, Autosave etc.
    if (!isset($_POST['meine_buch_autoren_nonce']) || !wp_verify_nonce($_POST['meine_buch_autoren_nonce'], 'meine_buch_autoren_speichern')) {
        return;
    }
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
        return;
    }
    if (!current_user_can('edit_post', $post_id)) { // 'edit_post' für den spezifischen Post-Typ anpassen
        return;
    }

    // Daten für Ansatz 2: Zuerst alte Beziehungen löschen
    global $wpdb;
    $table_name = $wpdb->prefix . 'buch_autor_beziehungen';
    $wpdb->delete($table_name, array('buch_id' => $post_id), array('%d'));

    if (isset($_POST['buch_autoren_ids']) && is_array($_POST['buch_autoren_ids'])) {
        $neue_autor_ids = array_map('intval', $_POST['buch_autoren_ids']);
        foreach ($neue_autor_ids as $autor_id) {
            // Hier die Funktion add_buch_autor_beziehung($post_id, $autor_id) aufrufen (siehe oben)
            // Oder direkt $wpdb->insert verwenden
             $wpdb->insert(
                $table_name,
                array('buch_id' => $post_id, 'autor_id' => $autor_id),
                array('%d', '%d')
            );
        }
    }
    // Für Ansatz 1: update_post_meta($post_id, '_buch_autoren_ids', $neue_autor_ids);
}
add_action('save_post_buch', 'meine_buch_autoren_meta_speichern'); // 'buch' ist der Slug unseres CPTs

Diese Metabox-Beispiele sind vereinfacht. Für eine bessere User Experience könnten Sie JavaScript für eine durchsuchbare Auswahlbox (wie Select2 oder TomSelect) integrieren.

Fazit und Ausblick

Das programmatische Erstellen von Many-to-Many-Beziehungen in WordPress ist definitiv aufwendiger als die Verwendung eines spezialisierten Plugins. Der Ansatz mit einer eigenen Verbindungstabelle ist dabei in den meisten Fällen die sauberste, performanteste und flexibelste Lösung, insbesondere bei großen Datenmengen oder wenn zusätzliche Metadaten zur Beziehung selbst gespeichert werden sollen.

Die Investition in diesen eigenen Code kann sich jedoch lohnen, wenn Sie maximale Kontrolle und Performance benötigen oder sehr spezifische Anforderungen haben, die über Standardfunktionalitäten hinausgehen. Ich hoffe, dieser tiefergehende Einblick hilft Ihnen bei Ihren nächsten anspruchsvollen WordPress-Projekten.

Wenn Sie Unterstützung bei der Umsetzung komplexer WordPress-Lösungen benötigen oder individuelle Anforderungen haben, die eine maßgeschneiderte Entwicklung erfordern, stehe ich Ihnen gerne mit meiner Expertise zur Verfügung. Gemeinsam finden wir die optimale Lösung für Ihr Projekt.

Haben Sie Fragen zu diesem Thema?

Ich berate Sie gerne persönlich und entwickle eine maßgeschneiderte Lösung für Ihr Projekt.

Jetzt Kontakt aufnehmen
Schreiben Sie uns auf WhatsApp