Шаблон репозитория и таблица соединений в go - PullRequest
0 голосов
/ 27 мая 2020
• 1000 таблица => 1 репозиторий => 1 сервис

Но я не могу найти самый чистый способ обработки таблицы соединения между двумя объектами.

Можно было бы сделать 1 запрос по таблице внутри соединения, и это было бы «чистым» (так сказать), но не эффективным, поскольку простое соединение привело бы к одному запросу.

Где в этом шаблоне соединяются таблицы?

  • Сейчас я думал о создании объектов, которые инкапсулируют ответ, но эффективно создают 1 объект + репозиторий всего для 1 запроса ...

  • Я также думаю, что объединение нескольких сущностей в один интерфейс могло бы частично решить эту проблему, но это привело бы к появлению многих пустых параметров моих сущностей (редко когда вам требуется ВСЕ поле из ВСЕХ вкладок при выполнении соединения)

Каков правильный способ / шаблон для решения этой проблемы, который вписался бы в DDD или, по крайней мере, был бы чистым?

- Пример редактирования:

type User struct {
    ID          int       `db:"id"`
    ProjectID      int    `db:"project_id"`
    RoleID      int       `db:"role_id"`
    Email       string    `db:"email"`
    FirstName   string    `db:"first_name"`
    LastName    string    `db:"last_name"`
    Password    string    `db:"password"`
}

type UserRepository interface {
    FindById(int) (*User, error)
    FindByEmail(string) (*User, error)
    Create(user *User) error
    Update(user *User) error
    Delete(int) errorr
}

type Project struct {
    ID          int       `db:"id"``
    Name   string    `db:"name"`
    Description    string    `db:"description"`
}

Вот у меня простой репозиторий пользователей. У меня что-то похожее есть для таблицы "Проект". может создать таблицу, получить всю информацию о проекте, удалить и т.д. c et c.

Как вы можете видеть, UserID имеет внешний ключ идентификатора проекта, которому он принадлежит.

Моя проблема заключается в том, что мне нужно получить всю информацию от пользователя и произнести «название проекта» и описание. (На самом деле таблица / объект имеет гораздо больше параметров)

Мне нужно сделать простое соединение в user.project_id и project.id и получить всю информацию о пользователе + имя проекта + описание в одном запросе.

Иногда это сложнее, потому что будет 3-4 объекта, связанных таким образом. (пользователь, проект, дополнительная_информация проекта, роли и т. д. c)

Конечно, я мог бы сделать N запросов, по одному на каждую сущность.

user := userRepo.Find(user_id)
project := projectRepo.FindByuser(user.deal_id)

И это будет «работать», но я пытаюсь найти способ сделать это одним запросом. поскольку простое соединение sql с user.project_id и project.id предоставит мне все данные по запросу.

Ответы [ 2 ]

1 голос
/ 27 мая 2020

В зависимости от данных, которые вы хотите прочитать, решение будет другим:

Если таблицы, которые вы хотите объединить, образуют единый агрегат, просто присоедините их в своем запросе и всегда возвращайте и сохраняйте полный агрегат . В этом случае у вас есть только репозиторий для объекта root. Скорее всего, это не ваш сценарий, потому что вы сказали, что у вас есть репозитории для других сущностей, к которым вы хотите присоединиться (если у вас нет проблем с дизайном).

Если таблицы, к которым вы хотите присоединиться, принадлежат разным ограниченным контекстам, вам не следует не присоединяюсь к ним. Лучше всего отправлять по одному запросу для каждого ограниченного контекста, чтобы они оставались разъединенными. Эти несколько запросов будут поступать из разных мест, в зависимости от вашей архитектуры: напрямую от клиента, от шлюза API, от какой-то службы приложений и т. Д. c.

Если таблицы принадлежат одному ограниченному контексту, но из нескольких агрегатов, то самый чистый подход - следовать CQRS (разделение команд / запросов). Проще говоря, вы определяете специальный интерфейс c для своего запроса с входами и выходами, которые вам нужны для реализуемого вами варианта использования. Это разделение освобождает вас от ограничений, которые вы обнаруживаете, пытаясь использовать инфраструктуру команд для запросов (имеющееся у вас отношение 1 к 1 объект / репозиторий). Наивной реализацией этого интерфейса запросов может быть запрос, который объединяет ваши существующие таблицы. Это быстро и легко реализовать, но это означает, что ваши команды и запросы разделены в коде, но не на уровне БД. В идеале вы должны создать (денормализованную) таблицу модели чтения в своей базе данных со всеми столбцами, которые вам нужны для этого запроса c и обновлять каждый раз, когда одна из исходных таблиц обновляется (обычно это делается с помощью событий домена) . Это позволяет оптимизировать таблицы для ваших запросов, используя правильные столбцы, формат данных и индексы, но, как недостаток, это привносит некоторую сложность и возможную согласованность между моделью записи и чтения.

1 голос
/ 27 мая 2020

Что касается части соединения, на ваш вопрос довольно тривиально ответить, однако для DDD существует множество препятствий из-за текущих языковых возможностей. Но я попробую ..

Хорошо, давайте представим, что мы разрабатываем бэкэнд образовательных курсов с многоязыковой поддержкой, где нам нужно соединить две таблицы и впоследствии сопоставить их с объектом. У нас есть две таблицы (первая содержит данные, не зависящие от языка, а вторая - данные, зависящие от языка). Если вы сторонник репозитория, то у вас будет что-то вроде этого:

// Course represents e.g. calculus, combinatorics, etc.
type Course struct {
    ID     uint   `json:"id" db:"id"`
    Name   string `json:"name" db:"name"`
    Poster string `json:"poster" db:"poster"`
}

type CourseRepository interface {
    List(ctx context.Context, localeID uint) ([]Course, error)
}

, а затем реализовать его для sql db у нас будет что-то вроде этого:

type courseRepository struct {
    db *sqlx.DB
}

func NewCourseRepository(db *sqlx.DB) (CourseRepository, error) {
    if db == nil {
        return nil, errors.New("provided db handle to course repository is nil")
    }

    return &courseRepository{db:db}, nil
}

func (r *courseRepository) List(ctx context.Context, localeID uint) ([]Course, error) {

    const query = `SELECT c.id, c.poster, ct.name FROM courses AS c JOIN courses_t AS ct ON c.id = ct.id WHERE ct.locale = $1`
    var courses []Course
    if err := r.db.SelectContext(ctx, &courses, query, localeID); err != nil {
        return nil, fmt.Errorf("courses repostory/problem while trying to retrieve courses from database: %w", err)
    }

    return courses, nil
}

То же самое относится и к разным связанным объектам. Вам просто нужно терпеливо смоделировать отображение вашего объекта с базовыми данными. Приведу еще один пример.

type City struct {
    ID                      uint            `db:"id"`
    Country                 Country         `db:"country"`
}

type Country struct {
    ID   uint  `db:"id"`
    Name string `db:"name"`
}

// CityRepository provides access to city store.
type CityRepository interface {
    Get(ctx context.Context, cityID uint) (*City, error)
}

// Get retrieve city from database by specified id
func (r *cityRepository) Get(ctx context.Context, cityID uint) (*City, error) {

    const query = `SELECT 
    city.id, country.id AS 'country.id', country.name AS 'country.name',
    FROM city JOIN country ON city.country_id = country.id WHERE city.id = ?`

    city := City{}
    if err := r.db.GetContext(ctx, &city, query, cityID); err != nil {
        return nil, fmt.Errorf("city repository / problem occurred while trying to retrieve city from database: %w", err)
    }

    return &city, nil
}

Теперь все выглядит чистым, пока вы не поймете, что Go на самом деле (на данный момент) не поддерживает дженерики, и, кроме того, люди в большинстве ситуаций отговаривают использовать функцию отражения, потому что это замедляет вашу программу. Чтобы окончательно расстроить вас, представьте, что с этого момента вам нужна транзакционная функциональность ....

Если вы пришли с других языков, вы можете попробовать добиться этого с помощью чего-то вроде этого:

// UnitOfWork is the interface that any UnitOfWork has to follow
// the only methods it as are to return Repositories that work
// together to achieve a common purpose/work.
type UnitOfWork interface {
    Entities() EntityRepository
    OtherEntities() OtherEntityRepository
}

// StartUnitOfWork it's the way to initialize a typed UoW, it has a uowFn
// which is the callback where all the work should be done, it also has the
// repositories, which are all the Repositories that belong to this UoW
type StartUnitOfWork func(ctx context.Context, t Type, uowFn UnitOfWorkFn, repositories ...interface{}) error

// UnitOfWorkFn is the signature of the function
// that is the callback of the StartUnitOfWork
type UnitOfWorkFn func(ctx context.Context, uw UnitOfWork) error

Я намеренно пропустил реализацию, потому что она выглядит чудовищно для sql и заслуживает отдельного вопроса (идея состоит в том, что у единицы работы есть свои версии репозиториев, украшенные запущенным tx под капотом), и после того, как вы преодолеете эту проблему, у вас будет больше или меньше

err = svc.startUnitOfWork(ctx, uow.Write, func(ctx context.Context, uw uow.UnitOfWork) error {

            // _ = uw.Entities().Store(entity)
            // _ = uw.OtherEntities().Store(otherEntity)

            return nil
        }, svc.entityRepository, svc.otherEntityRepository)

, так что здесь вы дошли до финала, и в большинстве случаев люди начали говорить, что вы пишете код, который кажется не идиоматическим c, имея в виду что-то вроде , что . Дело в том, что концепции написаны слишком абстрактно, и это философский вопрос, применимо ли материализованное DDD в Golang или вы можете просто частично имитировать c. Если вам нужна гибкость, выберите базу данных один раз и используйте чистый дескриптор db

...