Search code examples
gorepositoryrepository-pattern

Conditional repositories in Golang


I'm currently running a Go API, where I have repositories allowing me to get data from a MySQL database.

But I would like to implement the same repositories to fetch the same data but from a MongoDB database, which is currently difficult for me.

Here's my code:

// My vehicle repository file

// Repository handling Vehicle
type VehicleRepository struct {
    conn  *sqlx.DB
}

func (r *VehicleRepository) GetVehicles(ctx context.Context, brand string) (vehicle.Vehicle, error) {
    var vehicles []*vehicle.Vehicle
    param := map[string]interface{}{"brand": brand}

    query := "SELECT * FROM vehicles WHERE brand = :brand"

    rows, err := r.conn.NamedQueryContext(ctx, query, param)
    if err != nil {
        return nil, errors.Wrap(err, "query execution")
    }

    return vehicles, nil
}

// and in another file I create a repository object with all my repository (here only "Vehicle" but in reality I have plenty)
type Repositories struct {
    Vehicle    *VehicleRepository
}

func CreateRepositories(conn c.Connections) *Repositories {
    return &Repositories{
        Vehicles:      &VehicleRepository{conn, l},
    }
}

This code works well, but it only handle each case at a time (either in MySQL mode or in MongoDB mode), but I need to handle both MySQL and MongoDB now, and use the MongoDB repository depending of my API configuration that can be changed on fly.

I tried multiple things:

  1. Implementing different repositories

I tried to rename my vehicle.go repository vehicle.mysql.go and create a vehicle.mongodb.go implementing the mongodb repository. I renamed the VehicleRepository struct to VehicleMySQLRepository and create a VehicleMongoDBRepository into vehicle.mongodb.go, but when I try to link it to my Repositories in my CreateRepositories it breaks:

type Repositories struct {
    // Here I have `VehicleRepository` but it didn't exist anymore, and I can't set Vehicle to `VehicleMySQLRepository | VehicleMongoDBRepository`
    Vehicle    *VehicleRepository
}

func CreateRepositories(conn c.Connections, condition bool) *Repositories {
    if condition == true {
        return &Repositories{
            // Here I could do this:
            // Vehicles: &VehicleMySQLRepository{conn.Mysql, l},
            // Vehicles: &VehicleMongoDBRepository{conn.Mongo, l},
            // but it won't work because `Vehicles` will be declared multiple time, and I can't
            // have multiple `Vehicles` like `VehiclesMongo` & `VehiclesMysql`, etc...
            // because it will break all my calls on `Vehicles` in +50 files, and in reality
            // I've got over 30 repositories, if I need to duplicate each ones of them it won't do...

            Vehicles: &VehicleRepository{conn.Mongo, l},
        }
    }
    return &Repositories{
        Vehicles:     &VehicleRepository{conn.Mysql, l},
    }
}

  1. Conditional compiling

I came across this thinking I could create both vehicle.mysql.go and vehicle.mongodb.go, implementing their respective repository and only compile it with a go build -tags mongo and co, It works but I need to handle both repository at the same time, on my frontend users can chose if they use the MySQL or MongoDB database, so I can't really recompile my backend with the good implementation each time on fly.


  1. Keeping the same repositories

I also tried to don't change the repository, and handle MySQL and MongoDB connection in the function itself, like in GetVehicles() method the query string is set conditionally to the MySQL query or MongoDB query, it works but the implementation is quite gross, I would really like to make the option 1) working.


I don't know if my problem is clear enough, feel free to request more information if needed in comments.


Solution

  • VehicleMySQLRepository and VehicleMongoDBRepository are different structures. Therefore, in your code, it is not possible to have a member of Repository with both types as members.

    Use interfaces instead of structs as members of Repository. Here's an example created using your code as a reference.

    package main
    
    import (
        "context"
    )
    
    type Vehicle struct {
    }
    
    type VehicleRepository interface {
        GetVehicles(ctx context.Context, brand string) ([]Vehicle, error)
    }
    
    type VehicleMySQLRepository struct {
    }
    
    func (r *VehicleMySQLRepository) GetVehicles(ctx context.Context, brand string) ([]Vehicle, error) {
        var vehicles []Vehicle
        //...
        return vehicles, nil
    }
    
    type VehicleMongoRepository struct {
    }
    
    func (r *VehicleMongoRepository) GetVehicles(ctx context.Context, brand string) ([]Vehicle, error) {
        var vehicles []Vehicle
        //...
        return vehicles, nil
    }
    
    type Repositories struct {
        Vehicle VehicleRepository
    }
    
    type Connections struct {
    }
    
    func CreateRepositories(conn Connections, condition bool) *Repositories {
        if condition == true {
            return &Repositories{
                Vehicle: &VehicleMySQLRepository{},
            }
        }
        return &Repositories{
            Vehicle:     &VehicleMongoRepository{},
        }
    }
    
    func main() {
    
    }