Как реализуется связь Many-to-Many в реляционных БД и как это отражается в Go?

Ответ

Для реализации связи Many-to-Many (многие-ко-многим) в реляционных базах данных используется промежуточная таблица (также известная как связующая, ассоциативная или junction table). Эта таблица содержит внешние ключи (Foreign Keys) на обе связанные таблицы, а также может иметь свой собственный первичный ключ и дополнительные поля.

В Golang при работе с ORM, таким как GORM, это можно реализовать декларативно через структуры:

package main

import (
    "gorm.io/gorm"
    "fmt"
)

// User представляет пользователя
type User struct {
    gorm.Model
    Name      string
    // GORM автоматически создаст промежуточную таблицу 'user_languages'
    // и свяжет User с Language через нее.
    Languages []Language `gorm:"many2many:user_languages;"`
}

// Language представляет язык
type Language struct {
    gorm.Model
    Name  string
    // Аналогично, GORM свяжет Language с User.
    Users []User `gorm:"many2many:user_languages;"`
}

func main() {
    // Пример инициализации GORM (для демонстрации)
    // db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
    // if err != nil {
    //     panic("failed to connect database")
    // }

    // db.AutoMigrate(&User{}, &Language{})
    // При AutoMigrate GORM автоматически создаст 3 таблицы:
    // - users
    // - languages
    // - user_languages (промежуточная таблица с user_id и language_id)

    fmt.Println("Структуры для Many-to-Many определены.")
    fmt.Println("GORM автоматически создаст таблицы: users, languages, user_languages.")
}

Ключевые моменты реализации:

  • Промежуточная таблица: Содержит как минимум два внешних ключа, каждый из которых ссылается на первичный ключ одной из основных таблиц (например, user_id и language_id).
  • Первичный ключ промежуточной таблицы: Часто комбинация внешних ключей ((user_id, language_id)) формирует составной уникальный индекс или составной первичный ключ для обеспечения уникальности каждой связи. При использовании gorm.Model в промежуточной таблице также будет отдельный ID.
  • Дополнительные поля: В связующую таблицу можно добавлять дополнительные поля, описывающие саму связь (например, created_at для отметки времени создания связи, role для роли пользователя в проекте и т.д.).
  • Без ORM: При работе без ORM потребуется вручную создать все три таблицы в базе данных и выполнять сложные JOIN-запросы для выборки связанных данных, а также отдельные INSERT/DELETE запросы для управления связями в промежуточной таблице.

Ответ 18+ 🔞

А, ну вот, смотри, опять эти ваши связи «многие-ко-многим». Ну классика же, блядь! Как будто в первый раз. Ёпта, каждый раз одно и то же — без промежуточной таблицы нихуя не работает, как ни крути.

Представь себе, есть у тебя пользователи. И есть языки программирования. Ну или там, обычные языки, не суть. Один юзер может знать дохуя языков. И один язык может быть известен дохуя юзерам. Вот и получается эта самая, блядь, связь. А база данных-то реляционная, она тупо строки в таблицах хранит, ей похуй на твои высокие материи. Поэтому приходится выкручиваться.

И выкручиваемся мы так: создаём третью, промежуточную таблицу. Её ещё junction table или связующей обзывают. И в этой таблице всего две колонки по сути нужно: user_id и language_id. Каждая запись в ней — это факт: «вот этот конкретный юзер знает вот этот конкретный язык». И всё, пиздец. Проще некуда.

А теперь смотри, как это в Go с GORM'ом выглядит. Тут вообще красота, они за тебя почти всё делают, ленивые суки.

package main

import (
    "gorm.io/gorm"
    "fmt"
)

// User — это, блядь, наш чувак
type User struct {
    gorm.Model
    Name      string
    // Вот эта магия! GORM сам создаст промежуточную таблицу 'user_languages'
    // и всё прикрутит. Сиди и не парься.
    Languages []Language `gorm:"many2many:user_languages;"`
}

// Language — это, соответственно, язык
type Language struct {
    gorm.Model
    Name  string
    // И тут обратную связь прописываем, чтобы с той стороны тоже можно было ходить.
    Users []User `gorm:"many2many:user_languages;"`
}

func main() {
    // Ну тут типа инициализация БД, ты понял
    // db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
    // if err != nil {
    //     panic("failed to connect database")
    // }

    // db.AutoMigrate(&User{}, &Language{})
    // И вот эта волшебная команда создаст ТРИ таблицы:
    // - users (для юзеров)
    // - languages (для языков)
    // - user_languages (та самая промежуточная, где только user_id и language_id)

    fmt.Println("Структуры для Many-to-Many определены.")
    fmt.Println("GORM автоматически создаст таблицы: users, languages, user_languages.")
}

На что тут обратить внимание, чтобы не обосраться:

  • Промежуточная таблица — наше всё. Без неё — просто пиздец и карачун. Она хранит только пары IDшников.
  • Первичный ключ там какой? Чаще всего делают составной уникальный ключ из этих двух ID ((user_id, language_id)), чтобы одна и та же связь не повторялась дважды. А то получится, что один чувак один язык знает два раза — ебать он гений! Хотя GORM со своим gorm.Model может и отдельный ID туда воткнуть, но суть та же.
  • А если нужно больше, чем два ID? Например, дату, когда юзер язык выучил? Тогда, сука, придётся делать свою кастомную структуру для этой промежуточной таблицы и там уже поля добавлять. Это уже посложнее, но тоже решаемо.
  • А если без ORM? Ну тогда, дружок, welcome to hell. Придётся все три таблицы самому создавать, JOIN'ы писать длиннее, чем твоё хуйло, и за каждую операцию связи отдельно следить. Короче, запасайся кофе и терпением, которого, как известно, ноль ебать.

Вот и вся магия. Не так страшен чёрт, как его малюют. Главное — понять принцип: связь «многие-ко-многим» живёт не в двух, а в трёх таблицах. Всё остальное — технические детали.