如何处理在golang中接口和实现它的类型之间不兼容的具体返回类型?

huangapple go评论71阅读模式
英文:

How to handle incompatible concrete return types between an interface and types that implement it in golang?

问题

我正在学习Go语言,并决定在接口中使用领域驱动设计。我希望能够在运行时根据执行环境选择支持mysql、postgresql和sqlite等不同的数据库引擎。由于无论使用哪个引擎,所有的数据库操作都是相同的,所以我决定使用sqlc来生成数据库代码。

现在我遇到了一个问题,我试图创建一个边界接口Storer,用于连接消费者和引擎,并向消费者暴露数据库操作。但由于Storer暴露的返回类型与引擎针对每个操作(方法)返回的类型不兼容,所以它无法正常工作。

下面是一个示例,展示了FindUserById操作的sqlite实现,该操作应该返回一个user对象:

package sqlite

/* Begin sqlc generated code */
type User struct {
	ID        string
	FirstName string
	LastName  sql.NullString
	CreatedAt sql.NullTime
}

// findUserById is an sql statement
func (q *Queries) FindUserById(ctx context.Context, id string) (User, error) {
	row := q.db.QueryRowContext(ctx, findUserById, id)
	var i User
	err := row.Scan(
		&i.ID,
		&i.FirstName,
		&i.LastName,
		&i.CreatedAt,
	)
	return i, err
}

/* end sqlc generated code */

func (u *User) GetFirstName() string {
    return u.FirstName
}


func NewUserStorer() *Queries { ... }

这是具有UserStorer接口的storage包:

package storage
import "time.Time"

type User struct {
	ID        string
	FirstName string
	LastName  string
	CreatedAt time.Time
}
func (u *User) GetFirstName() string {
    return u.FirstName
}
type UserStorer interface {
    FindUserById(ctx context.Context, id string) (User, error)
}

最后,这是包含UserStorer消费者的主包:

package main

import "MyProject/storage"
import "MyProject/storage/sqlite"
import "fmt"

func ShowFirstName(s storage.UserStorer, id string) {
    u, _ := s.FindUserById(id) //假设用户存在
    fmt.Println(u.GetFirstName())
}

func main(){
    store := sqlute.NewUserStorer()
    id := "abc123"
    ShowFirstName(store, id)
}

现在当我尝试调用ShowFirstName时,出现了以下错误:

cannot use store (variable of type *sqlite.Queries) as storage.UserStorer value in argument to ShowFirstName: *sqlite.Queries does not implement storage.UserStorer(wrong type for method FindUserById)
		have FindUserById(context.Context, string) (sqlite.User, error)
		want FindUserById(context.Context, sql.NullString) (storage.User, error)compilerInvalidIfaceAssign

我尝试将storage.User类型更改为仅包含GetFirstName()方法的接口,但没有成功。我该如何确保返回类型相同,并且可以在不更改消费者代码的情况下切换sqlite和psql?我不能更改sqlc生成的代码和文件以适应类型要求,并且出于可维护性和避免与SQL相关的错误和漏洞的原因,我不想手动编写它生成的代码。

英文:

I am still learning Go and I decided to use Domain Driven Design with interfaces. I want to support mysql, postgresql and sqlite by choosing the engine at run time depending on the execution environment. Since all the db actions are the same regardless of the engine, I decided to use sqlc to generate the db code.

Now I am having a bit of a problem, where I am trying to create an boundary interface Storer to sit between the consumers and engines and expose db actions to the consumers. It is not working because of incompatible return types that the Storer exposes and those that the engines return for each action (method).

Here is an example sqlite implementation of FindUserById action that is supposed to return a user object:

package sqlite

/* Begin sqlc generated code */
type User struct {
	ID        string
	FirstName string
	LastName  sql.NullString
	CreatedAt sql.NullTime
}

// findUserById is an sql statement
func (q *Queries) FindUserById(ctx context.Context, id string) (User, error) {
	row := q.db.QueryRowContext(ctx, findUserById, id)
	var i User
	err := row.Scan(
		&i.ID,
		&i.FirstName,
		&i.LastName,
		&i.CreatedAt,
	)
	return i, err
}

/* end sqlc generated code */

func (u *User) GetFirstName() string {
    return u.FirstName
}


func NewUserStorer() *Queries { ... }

and here is the storage package that has the UserStorer interface:

package storage
import "time.Time"

type User struct {
	ID        string
	FirstName string
	LastName  string
	CreatedAt time.Time
}
func (u *User) GetFirstName() string {
    return u.FirstName
}
type UserStorer interface {
    FindUserById(ctx context.Context, id string) (User, error)
}

Lastly, here is the main package with aUserStorer consumer:

package main

import "MyProject/storage"
import "MyProject/storage/sqlite"
import "fmt"

func ShowFirstName(s storage.UserStorer, id string) {
    u, _ := s.FindUserById(id) //assuming the user exists
    fmt.Println(u.GetFirstName())
}

func main(){
    store := sqlute.NewUserStorer()
    id := "abc123"
    ShowFirstName(store, id)
}

Now when I try to call ShowFirstName, I get this error:

cannot use store (variable of type *sqlite.Queries) as storage.UserStorer value in argument to ShowFirstName: *sqlite.Queries does not implement storage.UserStorer(wrong type for method FindUserById)
		have FindUserById(context.Context, string) (sqlite.User, error)
		want FindUserById(context.Context, sql.NullString) (storage.User, error)compilerInvalidIfaceAssign

I tried changing the storage.User type to be an interface with only the GetFirstName() method, but that did not work.
How do I ensure that the return types are the same, and I can swap out sqlite for psql without changing the consumers? I cannot change the code and files generated by sqlc to fit the type requirements, and I don't want to write the code it generates manually for maintainability reasons and to avoid sql related errors and bugs.

答案1

得分: 1

如评论中所提到的,sqlcsqlite包中定义的User类型与storage包中定义的User类型不同。这是因为sqlc生成了自己的User类型,其字段与数据库表中的列匹配,而storage包中的User类型的字段与您的领域模型匹配。

为了解决这个问题,我们可以在应用程序的领域模型中定义一个单独的User类型,将其映射到sqlc生成的User类型。然后,您可以在UserStorer接口和应用程序的代码中使用这个领域模型的User类型。在这里,我们正在将一个接口适配到另一个接口,以便使两个不兼容的接口可以一起工作(适配器模式)。

package main

// 在这里写入导入语句

// 定义一个领域模型User类型(storage pkg),将其映射到sqlc生成的User类型
type User struct {
	ID        string
	FirstName string
	LastName  sql.NullString
	CreatedAt sql.NullTime
}

// 定义一个UserMapper接口,用于映射sqlc的User类型和领域模型的User类型
type UserMapper interface {
	FromDB(u *sqlite.User) *storage.User
	ToDB(u *storage.User) *sqlite.User
}

// 实现UserMapper接口
type sqliteUserMapper struct{}

func (m *sqliteUserMapper) FromDB(u *sqlite.User) *storage.User {
	return &storage.User{
		ID:        u.ID,
		FirstName: u.FirstName,
		LastName:  u.LastName.String,
		CreatedAt: u.CreatedAt.Time,
	}
}
func (m *sqliteUserMapper) ToDB(u *storage.User) *sqlite.User {
	return &sqlite.User{
		ID:        u.ID,
		FirstName: u.FirstName,
		LastName:  sql.NullString{String: u.LastName, Valid: u.LastName != ""},
		CreatedAt: sql.NullTime{Time: u.CreatedAt, Valid: !u.CreatedAt.IsZero()},
	}
}

// 修改UserStorer接口,使用领域模型的User类型
type UserStorer interface {
	FindUserById(ctx context.Context, id string) (*User, error)
}

// 更新FindUserById的sqlite实现,使用领域模型的User类型和UserMapper
func (q *Queries) FindUserById(ctx context.Context, id string) (*storage.User, error) {
	row := q.db.QueryRowContext(ctx, findUserById, id)
	var u sqlite.User
	err := row.Scan(&u.ID, &u.FirstName, &u.LastName, &u.CreatedAt)
	if err != nil {
		return nil, err
	}
	return q.mapper.FromDB(&u), nil
}

// 更新ShowFirstName函数,使用领域模型的User类型
func ShowFirstName(s storage.UserStorer, id string) {
	u, _ := s.FindUserById(context.Background(), id) //假设用户存在
	fmt.Println(u.GetFirstName())
}

// 使用UserMapper实例化sqlite的UserStorer
func NewUserStorer(mapper storage.UserMapper) *Queries {
	return &Queries{
		db:     db,
		mapper: mapper,
	}
}

func main() {
	// main包中的所有函数以了解实现的逻辑顺序
	mapper := &sqliteUserMapper{}
	store := sqlite.NewUserStorer(mapper)
	id := "abc123"
	ShowFirstName(store, id)
}
英文:

As mentioned in the comments, User type defined by sqlc in the sqlite package is not the same as the User type defined in the storage package. This is because sqlc generates its own User type with fields that match the columns in the database table, whereas the User type in the storage package has fields that match your domain model.

To solve this problem, we can define a separate User type in your application's domain model that maps to the User type generated by sqlc. You can then use this domain model User type in your UserStorer interface and in your application's code. Here, we are adapting one interface to another so that two incompatible interfaces can work together (Adapter Pattern).

package main

// Write imports here

// Define a domain model User type (storage pkg) that maps to the User type generated by sqlc
type User struct {
	ID        string
	FirstName string
	LastName  sql.NullString
	CreatedAt sql.NullTime
}

// Define a UserMapper interface that maps between sqlc User type and domain model User type
type UserMapper interface {
	FromDB(u *sqlite.User) *storage.User
	ToDB(u *storage.User) *sqlite.User
}

// Implement the UserMapper interface
type sqliteUserMapper struct{}

func (m *sqliteUserMapper) FromDB(u *sqlite.User) *storage.User {
	return &storage.User{
		ID:        u.ID,
		FirstName: u.FirstName,
		LastName:  u.LastName.String,
		CreatedAt: u.CreatedAt.Time,
	}
}
func (m *sqliteUserMapper) ToDB(u *storage.User) *sqlite.User {
	return &sqlite.User{
		ID:        u.ID,
		FirstName: u.FirstName,
		LastName:  sql.NullString{String: u.LastName, Valid: u.LastName != ""},
		CreatedAt: sql.NullTime{Time: u.CreatedAt, Valid: !u.CreatedAt.IsZero()},
	}
}

// Modify your UserStorer interface to use the domain model User type
type UserStorer interface {
	FindUserById(ctx context.Context, id string) (*User, error)
}

// Update your sqlite implementation of FindUserById to use the domain model User type and the UserMapper
func (q *Queries) FindUserById(ctx context.Context, id string) (*storage.User, error) {
	row := q.db.QueryRowContext(ctx, findUserById, id)
	var u sqlite.User
	err := row.Scan(&u.ID, &u.FirstName, &u.LastName, &u.CreatedAt)
	if err != nil {
		return nil, err
	}
	return q.mapper.FromDB(&u), nil
}

// Update your ShowFirstName function to use the domain model User type
func ShowFirstName(s storage.UserStorer, id string) {
	u, _ := s.FindUserById(context.Background(), id) //assuming the user exists
	fmt.Println(u.GetFirstName())
}

// Instantiate your sqlite UserStorer with a UserMapper
func NewUserStorer(mapper storage.UserMapper) *Queries {
	return &Queries{
		db:     db,
		mapper: mapper,
	}
}

func main() {
    // all functions in pkg main to understand the logical order of implementation
	mapper := &sqliteUserMapper{}
	store := sqlite.NewUserStorer(mapper)
	id := "abc123"
	ShowFirstName(store, id)
}

huangapple
  • 本文由 发表于 2023年5月6日 21:40:25
  • 转载请务必保留本文链接:https://go.coder-hub.com/76189201.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定