英文:
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
如评论中所提到的,sqlc
在sqlite
包中定义的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)
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论