Search code examples
godesign-patternsdomain-driven-designddd-repositories

Repository pattern and Joining table in go


I'm currently trying to build my app around the Domain driven design , entities , services , repos,...

All the basic crud operation are simple where basically 1 entity => 1 table => 1 repository => 1 service

But i can't figure out the cleanest way to handle join table between two entities.

It would be possible to make 1 query by table inside of the join and it would be " clean" ( so to say ) but it wouldn't be efficient as a simple join would have resulted in one query.

Where do tables join live in this pattern ?

  • I have been thinking of building now entities that would encapsulate the answer but that would effectivitly create 1 entity + repository for just 1 query...

  • I'm also thinking that merging multiple entities into a single interface might partially solve it but it would result in many empty param of my entities ( it's rare that you require ALL field from ALL tabes when you're performing a join )

What is the proper way/pattern to solve this issue that would fit in the DDD or at least be clean ?

-- Edit example :

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"`
}

Here i have a simple user repository. I have something similar for the "Project" table. can create table , get all info on the project , delete , etc etc.

As you can see UserID has the foreign key of the project ID it belongs to.

My issue is when i need to retrieve all the information from the user and , say the " project name " and description. ( i reality the table/entity has much more parameters )

I need to do a simple join in user.project_id and project.id and retreive all the information of the user + project name + description in one query.

Sometimes it's more complex because there will be 3-4 entities linked like this. ( user , project , project_additional_information, roles , etc)

Ofcourse i could make N queryies, one per entity.

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

And that would "Work " but i'm trying to find a way to do it in one query. since a simple sql join on user.project_id and project.id would give me all data in on query.


Solution

  • As for join part your question is quite trivial to answer, however for DDD there are a lot of hurdles from current language possibilities. But I will give a try..

    Ok, let's imagine that we are developing an educational courses backend with multilanguage support where we need to join two tables and subsequently map to object. We have two tables (the first one contains language-independent data and second which contains language-dependent) If you are a repository advocate, then you will have something like that:

    // 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)
    }
    

    then implementing it for sql db we will have something like that:

    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
    }
    

    The same applies to different related objects. You just need patiently model a mapping of your object with underlying data. Let me give another example.

    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 {
            if err == sql.ErrNoRows {
              return nil, ErrNoCityEntity
            }
            return nil, fmt.Errorf("city repository / problem occurred while trying to retrieve city from database: %w", err)
        }
    
        return &city, nil
    }
    

    Now, everything looks clean until you realize that Go actually (as for now) does not support generics and in addition people in most situations discourage to utilize reflect functionality because it makes your program slower. In order to completely upset you imagine that from this moment you need transactional functionality....

    If you came from other languages, you can try achieving it with something like that:

    // 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
    

    I intentionally missed an implementation because it looks monstrous for sql and deserves its own question (the idea is that unit of work has its versions of repositories decorated with started tx under the hood) and after you beat this problem you will have more or less

    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)
    

    so here you reach the final and in most cases people started to say that you write code which seems not idiomatic referring something like that. The point is that concepts are written too abstract and it is a philosophical question whether materialized DDD is even applicable in Golang or you can just partially mimic it. If you want flexibility, choose database once and operate with pure db handle