英文:
Golang repostiory pattern
问题
我尝试在Go应用程序(简单的Web服务)中实现存储库模式,并尝试找到更好的方法来避免代码重复。
以下是代码:
接口:
type IRoleRepository interface {
GetAll() ([]Role, error)
}
type ISaleChannelRepository interface {
GetAll() ([]SaleChannel, error)
}
实现:
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
}
如你所见,区别仅在于几个单词。我尝试找到类似于C#中的泛型,但没有找到。
有人可以帮助我吗?
英文:
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?
答案1
得分: 23
不,Go语言没有泛型,在可预见的未来也不会有泛型。
你有三个选择:
-
重构你的代码,使其有一个接受SQL语句和另一个函数的单一函数,并且:
- 使用提供的语句查询数据库。
- 迭代结果的行。
- 对于每一行,调用提供的函数来扫描该行。
在这种情况下,你将拥有一个通用的“查询”函数,而差异仅在于“扫描”函数。
当然,还有其他几种变体,但我猜你已经有了这个想法。
-
使用
sqlx
包,它基本上就像encoding/json
对JSON数据流一样,对于基于SQL的数据库,它使用反射来创建和执行SQL语句以填充数据。这样,你将在另一个层面上获得可重用性:你只需编写少量样板代码。
-
使用代码生成,这是Go语言本地的实现“代码模板”的方式(这就是泛型的作用)。
这种方式通常是编写一个Go程序,它接受某种格式的输入,读取并写出一个或多个包含Go代码的文件,然后进行编译。
在你这个非常简单的情况下,你可以从你的Go函数模板开始,以及一个将SQL语句映射到要从所选数据创建的类型的表。
我要注意到你的代码确实看起来不符合惯用写法。
在Go语言中,没有人会以正常的思维方式实现“仓储模式”,但只要它让你满意,这也没什么问题。我们都在一定程度上受到我们习惯的语言/环境的影响。但是你的connection := r.provider.GetConnection()
看起来令人担忧:Go语言的database/sql
与“流行”的环境和框架有着显著的区别,所以我强烈建议你从这里和这里开始。
(更新截至2021-05-31)Go语言将会引入泛型,因为实现泛型的提案已经被接受,并且正在进行实现工作。
英文:
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:
- Queries the DB with the provided statement.
- Iterates over the result's rows.
- 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 whatencoding/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.
答案2
得分: 2
如果我理解正确的话,一个更好的模式可能是以下这样:
type RepositoryItem interface {
Name() string // 例如
}
type Repository interface {
GetAll() ([]RepositoryItem, error)
}
目前,你实际上为每种类型的存储库都有多个接口,所以除非你要实现多种类型的RoleRepository
,否则最好不要有这个接口。
使用通用的Repository
和RepositoryItem
接口可能会使你的代码更具可扩展性(更容易进行测试)。
一个假设Repository
大致对应于后端的例子是MySQLRepository
和MongoDBRepository
等实现。通过抽象存储库的功能,你可以防止未来的变化。
我也强烈建议参考@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.
答案3
得分: 1
interface{}
是 Go 语言中的“泛型类型”。我可以想象做这样的事情:
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.ISaleChannels)
}
这样你就不必为 IRole
和 ISaleChannel
分别创建两个结构体和/或接口了。
英文:
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
答案4
得分: 0
Golang自1.18版本开始支持泛型,因此可以使用泛型实现存储库模式。以下是带有接口的代码:
接口:
type IRepository[T any] interface {
GetAll() ([]T, error)
}
实现:
实现代码看起来与初始代码非常相似:
func (r *RoleRepository) GetAll() ([]Role, error) {
// 在这里编写代码
}
func (r *SaleChannelRepository) GetAll() ([]SaleChannel, error) {
// 在这里编写代码
}
英文:
Golang has generics since version 1.18 so it is possible to implement the repository pattern with them. Below the code with the interface:
Interface:
type IRepository[T any} interface {
GetAll() ([]T, error)
}
Implementation:
The implementation looks pretty similar to the initial code:
func (r *RoleRepository) GetAll() ([]Role, error) {
// Code here
}
func (r *SaleChannelRepository) GetAll() ([]SaleChannel, error) {
// Code here
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论