概念验证
图片来自 Rajan Alwan 在 Unsplash
经过数月甚至数年的讨论、实现和概念验证,我们终于迎来了我们所钟爱的编程语言的革命。全新的 Go 1.18 版本已经发布。
即使在泛型最终发布之前,我们已经知道它将会对 Go 代码库产生巨大的变化。
多年来,每当我们想提供一些通用性和抽象性时,我们都会在 Go 中使用代码生成器。学习什么是“Go语言的方式”对我们中的许多人来说是一个困难的时期,但这也给了我们许多突破。这是值得的。
现在,新的牌在桌面上了。许多新的包出现了,为我们提供了一些关于如何用可重用的代码丰富 Go 生态系统的想法,这也是我的灵感来源,让我制作了基于GORM库的小型概念验证。现在,让我们来看看它。
通过我的推荐链接加入 Medium — Marko Milojevic
源代码
当我撰写本文时,它依赖于 GitHub 上的 Git 仓库 。代码作为一个 Go 库,是我进一步开发的概念验证。然而,它还没有准备好在生产中使用(当然,我也没有计划在那时提供任何生产支持)。
你可以在链接上看到当前的功能,其中以下代码片段是一个较小的示例:
package main
import (
"github.com/ompluscator/gorm-generics"
// some imports
)
// Product is a domain entity
type Product struct {
// some fields
}
// ProductGorm is DTO used to map Product entity to database
type ProductGorm struct {
// some fields
}
// ToEntity respects the gorm_generics.GormModel interface
func (g ProductGorm) ToEntity() Product {
return Product{
// some fields
}
}
// FromEntity respects the gorm_generics.GormModel interface
func (g ProductGorm) FromEntity(product Product) interface{} {
return ProductGorm{
// some fields
}
}
func main() {
db, err := gorm.Open(/* DB connection string */)
// handle error
err = db.AutoMigrate(ProductGorm{})
// handle error
// initialize a new Repository with by providing
// GORM model and Entity as type
repository := gorm_generics.NewRepository[ProductGorm, Product](db)
ctx := context.Background()
// create new Entity
product := Product{
// some fields
}
// send new Entity to Repository for storing
err = repository.Insert(ctx, &product)
// handle error
fmt.Println(product)
// Out:
// {1 product1 100 true}
single, err := repository.FindByID(ctx, product.ID)
// handle error
fmt.Println(single)
// Out:
// {1 product1 100 true}
}
代码片段,演示了这个概念验证库的实际用法
为什么选择 ORM 进行概念验证?
作为一名从事传统面向对象编程语言(如 Java、C# 和 PHP)的软件开发人员出身,我在 Google 上做的第一次搜索之一就是寻找适合 Golang 的 ORM。请原谅我当时的年轻,但这是我的期望。
并不是说我不能没有 ORM 就无法生存。我并不特别欣赏在代码中看到原始的 MySQL 查询。所有的字符串拼接看起来很丑陋。
另一方面,我总是喜欢立即跳入编写业务逻辑,并且我几乎不投入时间考虑底层存储。有时我会在实现过程中改变想法,并切换到其他类型的存储。这就是 ORM 让我的生活更轻松的地方。
简而言之,ORM 给了我:
- 更清晰的代码
- 更灵活地选择底层存储类型
- 将整个重心放在业务逻辑上,而不是技术细节上
在 Golang 中有许多解决方案可以使用 ORM,我已经使用了其中的大部分。毫不奇怪,GORM 是我使用最多的一个,因为它覆盖了大部分功能。是的,它缺少一些众所周知的模式,例如Identity Map、Unit of Work和Lazy Load,但我可以没有它们。
然而,我一直在想念存储库模式,因为我有时会遇到类似或相同的代码块的重复(我讨厌重复自己)。
为此,我有时会使用 GNORM 库,其模板逻辑使我能够自由地生成存储库结构。虽然我喜欢 GNORM 提供的思路(那种好的 Go 语言的方式!),但是为了提供新功能给存储库,不断更新模板并不好看。
我尝试提供我的实现,以反射为基础,并将其提供给开源社区。我不能更失败了。它工作了,但是维护库非常痛苦,而且性能也不是很好。最后,我从 GitHub 中删除了 git 仓库。
当我放弃 Go 中的 ORM 升级时,泛型出现了。哦,天啊。 哦,天啊! 我立即回到了起点。
实现
我的背景之一是领域驱动设计。这意味着我喜欢将领域层与基础设施层解耦。一些 ORM 将 Entity 模式视为Row Data Gateway或Active Record。但是,由于其名称引用了 DDD 模式 Entity,我们在翻译中可能会遇到困难,我们往往会在同一类中存储业务逻辑和技术细节。这样就会产生一个怪物。
Entity 模式与数据库表方案映射无关。它与底层存储无关。
因此,我总是在领域层使用实体(Entity),在基础设施层使用数据传输对象(Data Transfer Object)。我的存储库的签名只支持实体,但是在内部,它们使用DTO将数据映射到和从数据库中提取和存储它们到实体。这是为了保证我们有一个功能性的防腐层(Anti-Corruption Layer)。
在这种情况下,我可以识别出三个接口和结构的三元组(如下图所示):
- 实体(Entity)作为领域层上业务逻辑的持有者
- GormModel作为DTO用于将数据从实体映射到数据库
- GormRepository作为查询和持久化数据的编排者
UML图表示了实现的高级概述。
两个主要部分,GormModel和GormRepository,都希望使用定义其方法签名的通用类型。使用泛型允许我们将GormRepository定义为结构体并将实现泛化:
func (r *GormRepository[M, E]) Insert(ctx context.Context, entity *E) error {
// map the data from Entity to DTO
var start M
model := start.FromEntity(*entity).(M)
// create new record in the database
err := r.db.WithContext(ctx).Create(&model).Error
// handle error
// map fresh record's data into Entity
*entity = model.ToEntity()
return nil
}
func (r *GormRepository[M, E]) FindByID(ctx context.Context, id uint) (E, error) {
// retrieve a record by id from a database
var model M
err := r.db.WithContext(ctx).First(&model, id).Error
// handle error
// map data into Entity
return model.ToEntity(), nil
}
func (r *GormRepository[M, E]) Find(ctx context.Context, specification Specification) ([]E, error) {
// retreive reords by some criteria
var models []M
err := r.db.WithContext(ctx).Where(specification.GetQuery(), specification.GetValues()...).Find(&models).Error
// handle error
// mapp all records into Entities
result := make([]E, 0, len(models))
for _, row := range models {
result = append(result, row.ToEntity())
}
return result, nil
}
GormRepository的实现。
在代码片段中,我们可以看到GormRepository结构支持插入新记录以及通过标识检索或通过规范查询。规范模式是领域驱动设计中的另一种模式,我们可以用它来查询存储中的数据。
这里提供的概念验证定义了一个Specification接口,它提供了一个WHERE子句和在其中使用的值。它需要一些使用可比较运算符的泛型,它是未来查询对象的可能前身:
规范实现的示例
type Specification interface {
GetQuery() string
GetValues() []any
}
// joinSpecification is the real implementation of Specification interface.
// It is used fo AND and OR operators.
type joinSpecification struct {
specifications []Specification
separator string
}
// GetQuery concats all subqueries
func (s joinSpecification) GetQuery() string {
queries := make([]string, 0, len(s.specifications))
for _, spec := range s.specifications {
queries = append(queries, spec.GetQuery())
}
return strings.Join(queries, fmt.Sprintf(" %s ", s.separator))
}
// GetQuery concats all subvalues
func (s joinSpecification) GetValues() []any {
values := make([]any, 0)
for _, spec := range s.specifications {
values = append(values, spec.GetValues()...)
}
return values
}
// And delivers AND operator as Specification
func And(specifications ...Specification) Specification {
return joinSpecification{
specifications: specifications,
separator: "AND",
}
}
// notSpecification negates sub-Specification
type notSpecification struct {
Specification
}
// GetQuery negates subquery
func (s notSpecification) GetQuery() string {
return fmt.Sprintf(" NOT (%s)", s.Specification.GetQuery())
}
// Not delivers NOT operator as Specification
func Not(specification Specification) Specification {
return notSpecification{
specification,
}
}
// binaryOperatorSpecification defines binary operator as Specification
// It is used for =, >, <, >=, <= operators.
type binaryOperatorSpecification[T any] struct {
field string
operator string
value T
}
// GetQuery builds query for binary operator
func (s binaryOperatorSpecification[T]) GetQuery() string {
return fmt.Sprintf("%s %s ?", s.field, s.operator)
}
// GetValues returns a value for binary operator
func (s binaryOperatorSpecification[T]) GetValues() []any {
return []any{s.value}
}
// Not delivers = operator as Specification
func Equal[T any](field string, value T) Specification {
return binaryOperatorSpecification[T]{
field: field,
operator: "=",
value: value,
}
}
该包的Specification部分提供了一种可能性,即为存储库提供自定义标准并检索满足其标准的数据。使用它可以组合标准、否定它们并进一步扩展它们。
结果
该实现最终包装了这个概念验证的最终目标,即提供一个通用的接口,用于从数据库查询记录:
err := repository.Insert(ctx, &Product{
Name: "product2",
Weight: 50,
IsAvailable: true,
})
// error handling
err = repository.Insert(ctx, &Product{
Name: "product3",
Weight: 250,
IsAvailable: false,
})
// error handling
many, err := repository.Find(ctx, gorm_generics.And(
gorm_generics.GreaterOrEqual("weight", 90),
gorm_generics.Equal("is_available", true)),
)
// error handling
fmt.Println(many)
// Out:
// [{1 product1 100 true}]
概念验证的最终目标。
就我的愿望而言,上述代码片段以简洁易读的形式提供了一种快速优雅的检索数据的方式。而且没有显著影响性能。
结论
在Go 1.18正式发布后第一次接触泛型是一次重要的更新。最近我一直在面临一些挑战,给予新思路的机会是我需要的。
此外,在长时间休息后继续博客写作是我必须做的事情。再次公开表达观点感觉很好,我期待着你们能给出的所有反馈。
评论(0)