Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Golang repostiory pattern

I try to implement repository pattern in Go app (simple web service) and try to find better way to escape code duplication.

Here is a code

Interfaces are:

type IRoleRepository interface {
    GetAll() ([]Role, error)
}

type ISaleChannelRepository interface {
    GetAll() ([]SaleChannel, error)
}

And implementation:

func (r *RoleRepository) GetAll() ([]Role, error) {
        var result []Role
        var err error
        var rows *sql.Rows

        if err != nil {
            return result, err
        }

        connection := r.provider.GetConnection()
        defer connection.Close()

        rows, err = connection.Query("SELECT Id,Name FROM Position")
        defer rows.Close()

        if err != nil {
            return result, err
        }

        for rows.Next() {
            entity := new(Role)
            err = sqlstruct.Scan(entity, rows)

            if err != nil {
                return result, err
            }

            result = append(result, *entity)
        }
        err = rows.Err()
        if err != nil {
            return result, err
        }

        return result, err
    }

    func (r *SaleChannelRepository) GetAll() ([]SaleChannel, error) {
        var result []SaleChannel
        var err error
        var rows *sql.Rows

        if err != nil {
            return result, err
        }

        connection := r.provider.GetConnection()
        defer connection.Close()

        rows, err = connection.Query("SELECT DISTINCT SaleChannel 'Name' FROM Employee")
        defer rows.Close()

        if err != nil {
            return result, err
        }

        for rows.Next() {
            entity := new(SaleChannel)
            err = sqlstruct.Scan(entity, rows)

            if err != nil {
                return result, err
            }

            result = append(result, *entity)
        }
        err = rows.Err()
        if err != nil {
            return result, err
        }

        return result, err
    }

As you can see differences are in a few words. I try to find something like Generics from C#, but didnt find.

Can anyone help me?

like image 635
solo12zw74 Avatar asked Nov 25 '15 17:11

solo12zw74


3 Answers

No, Go does not have generics and won't have them in the forseeable future¹.

You have three options:

  • Refactor your code so that you have a single function which accepts an SQL statement and another function, and:

    1. Queries the DB with the provided statement.
    2. Iterates over the result's rows.
    3. For each row, calls the provided function whose task is to scan the row.

    In this case, you'll have a single generic "querying" function, and the differences will be solely in "scanning" functions.

    Several variations on this are possible but I suspect you have the idea.

  • Use the sqlx package which basically is to SQL-driven databases what encoding/json is to JSON data streams: it uses reflection on your types to create and execute SQL to populate them.

    This way you'll get reusability on another level: you simply won't write boilerplate code.

  • Use code generation which is the Go-native way of having "code templates" (that's what generics are about).

    This way, you (usually) write a Go program which takes some input (in whatever format you wish), reads it and writes out one or more files which contain Go code, which is then compiled.

    In your, very simple, case, you can start with a template of your Go function and some sort of a table which maps SQL statement to the types to create from the data selected.


I'd note that your code indeed looks woefully unidiomatic.

No one in their right mind implements "repository patterns" in Go, but that's sort of okay so long it keeps you happy—we all are indoctrinated to a certain degree with the languages/environments we're accustomed to,—but your connection := r.provider.GetConnection() looks alarming: the Go's database/sql is drastically different from "popular" environments and frameworks so I'd highly recommend to start with this and this.


¹ (Update as of 2021-05-31) Go will have generics as the proposal to implement them has been accepted and the work implementing them is in progress.

like image 182
kostix Avatar answered Oct 18 '22 09:10

kostix


Forgive me if I'm misunderstanding, but a better pattern might be something like the following:

type RepositoryItem interface {
    Name() string // for example
}

type Repository interface {
    GetAll() ([]RepositoryItem, error)
}

At the moment, you essentially have multiple interfaces for each type of repository, so unless you're going to implement multiple types of RoleRepository, you might as well not have the interface.

Having generic Repository and RepositoryItem interfaces might make your code more extensible (not to mention easier to test) in the long run.

A contrived example might be (if we assume a Repository vaguely correlates to a backend) implementations such as MySQLRepository and MongoDBRepository. By abstracting the functionality of the repository, you're protecting against future mutations.

I would very much advise seeing @kostix's answer also, though.

like image 43
mattsch Avatar answered Oct 18 '22 11:10

mattsch


interface{} is the "generic type" in Go. I can imagine doing something like this:

package main

import "fmt"

type IRole struct {
    RoleId uint
}

type ISaleChannel struct {
    Profitable bool
}

type GenericRepo interface{
    GetAll([]interface{})
}

// conceptual repo to store all Roles and SaleChannels
type Repo struct {
    IRoles        []IRole
    ISaleChannels []ISaleChannel
}

func (r *Repo) GetAll(ifs []interface{}) {

    // database implementation here before type switch 

    for _, v := range ifs {
        switch v := v.(type) {
        default:
                fmt.Printf("unexpected type %T\n", v)
        case IRole:
                fmt.Printf("Role %t\n", v)            
                r.IRoles = append(r.IRoles, v)
        case ISaleChannel:
                fmt.Printf("SaleChannel %d\n", v)
                r.ISaleChannels = append(r.ISaleChannels, v)
        }
    }
}

func main() {

    getter := new(Repo)

    // mock slice
    data := []interface{}{
        IRole{1},
        IRole{2},
        IRole{3},
        ISaleChannel{true},
        ISaleChannel{false},
        IRole{4},
    }

    getter.GetAll(data)

    fmt.Println("IRoles: ", getter.IRoles)
    fmt.Println("ISaleChannels: ", getter.ISales)
}

This way you don't have to end up with two structs and/or interfaces for IRole and ISale

like image 28
Pandemonium Avatar answered Oct 18 '22 09:10

Pandemonium