Categories
程式開發

Golang領域模型-資源庫


前言: 作為領域模型中最重要的環節之一的Repository,其通過對外暴露接口屏蔽了內部的複雜性,又有其隱式寫時復制的巧妙代碼設計,完美的將DDD中的Repository的概念與代碼相結合!

資料庫

資源庫通常標識一個存儲的區域,提供讀寫功能。 通常我們將實體存放在資源庫中,之後通過該資源庫來獲取相同的實體,每一個實體都搭配一個資源庫。

如果你修改了某個實體,也需要通過資源庫去持久化。 當然你也可以通過資源庫去刪除某一個實體。

資源庫對外部是屏蔽了存儲細節的,資源庫內部去處理cache、es、db。

Golang領域模型-資源庫 1

操作流程

Repository解除了client的巨大負擔,使client只需與一個簡單的、易於理解的接口進行對話,並根據模型向這個接口提出它的請求。 要實現所有這些功能需要大量複雜的技術基礎設施,但接口卻很簡單,而且在概念層次上與領域模型緊密聯繫在一起。

隱式寫時復制

通常我們通過資源庫讀取一個實體後,再對這個實體進行修改。 那麼這個修改後的持久化是需要知道實體的哪些屬性被修改,然後再對應的去持久化被修改的屬性。

注意商品實體的changes,商品被修改某個屬性,對應的Repository就持久化相應的修改。 這麼寫有什麼好處呢? 如果不這麼做,那隻能在service裡調用orm指定更新列,但是這樣做的話,Repository的價值就完全被捨棄了!

可以說寫時復制是Repository和領域模型的橋樑!

//商品实体
type Goods struct {
changes map[string]interface{} //被修改的属性
Name string //商品名称
Price int // 价格
Stock int // 库存
}
// SetPrice .
func (obj *Goods) SetPrice(price int) {
obj.Price = price
obj.changes["price"] = price //写时复制
}

// SetStock .
func (obj *Goods) SetStock(stock int) {
obj.Stock = stock
obj.changes["stock"] = stock //写时复制
}

//示例
func main() {
goodsEntity := GoodsRepository.Get(1)
goodsEntity.SetPrice(1000)
GoodsRepositorySave(goodsEntity) //GoodsRepository 会内部处理商品实体的changes
}

工廠和創建

創建商品實體需要唯一ID和已知的屬性名稱等,可以使用實體工廠去生成唯一ID和創建,在交給資源庫去持久化,這也是<>的作者推薦的方式,但這種方式更適合文檔型數據庫,唯一ID是Key和實體序列化是值。

“底層技術可能會限制我們的建模選擇。例如,關係數據庫可能對複合對象結構的深度有實際的限制”(領域驅動設計:軟件核心複雜性應對之道Eric Evans)

但我們更多的使用的是關係型數據庫,這樣資源庫就需要創建的行為。 實體的唯一ID就是聚簇主鍵。 一個實體或許是多張表組成,畢竟我們還要考慮垂直分錶。 我認為DDD的範式和關係型數據庫範式,後者更重要。 有時候我們還要為Repository 實現一些統計select count

的功能。

根據所使用的持久化技術和基礎設施不同,Repository的實現也將有很大的變化。 理想的實現是向客戶隱藏所有內部工作細節(儘管不向客戶的開發人員隱藏這些細節),這樣不管數據是存儲在對像數據庫中,還是存儲在關係數據庫中,或是簡單地保持在內存中,客戶代碼都相同。 Repository將會委託相應的基礎設施服務來完成工作。 將存儲、檢索和查詢機制封裝起來是Repository實現的最基本的特性。

實踐https://github.com/8treenet/freedom/tree/master/example/fshop/adapter/repository

實體的緩存

// freedom.Entity
type Entity interface {
DomainEvent(string, interface{},...map[string]string)
Identity() string
GetWorker() Worker
SetProducer(string)
Marshal() []byte
}

// infra.EntityCache
type EntityCache interface {
//获取实体
GetEntity(freedom.Entity) error
//删除实体缓存
Delete(result freedom.Entity, async ...bool) error
//设置数据源
SetSource(func(freedom.Entity) error) EntityCache
//设置前缀
SetPrefix(string) EntityCache
//设置缓存时间,默认5分钟
SetExpiration(time.Duration) EntityCache
//设置异步反写缓存。默认关闭,缓存未命中读取数据源后的异步反写缓存
SetAsyncWrite(bool) EntityCache
//设置防击穿,默认开启
SetSingleFlight(bool) EntityCache
//关闭二级缓存. 关闭后只有一级缓存生效
CloseRedis() EntityCache
}

這個是緩存組件的接口,可以讀寫實體,實體的key 使用必須實現的Identity 方法。 – 一級緩存是基於請求的,首先會從一級緩存查找實體,生命週期是一個請求的開始和結束。 – 二級緩存是基於redis。 – 組件已經做了冪等的防擊穿處理。 – SetSource設置持久化的回調函數,當一、二級緩存未命中,會讀取回調函數,並反寫一、二級緩存。

package repository

import (
"time"

"github.com/8treenet/freedom/infra/store"

"github.com/8treenet/freedom/example/fshop/domain/po"
"github.com/8treenet/freedom/example/fshop/domain/entity"

"github.com/8treenet/freedom"
)

func init() {
freedom.Prepare(func(initiator freedom.Initiator) {
//绑定创建资源库函数到框架,框架会根据客户的使用做依赖倒置和依赖注入的处理。
initiator.BindRepository(func() *Goods {
//创建 Goods资源库
return &Goods{}
})
})
}
// Goods .
type Goods struct {
freedom.Repository //资源库必须继承,这样是为了约束 db、redis、http等的访问
Cache store.EntityCache //依赖注入实体缓存组件
}

// BeginRequest
func (repo *Goods) BeginRequest(worker freedom.Worker) {
repo.Repository.BeginRequest(worker)

//设置缓存的持久化数据源,旁路缓存模型,如果缓存未有数据,将回调该函数。
repo.Cache.SetSource(func(result freedom.Entity) error {

return findGoods(repo, result)
})
//缓存30秒, 不设置默认5分钟
repo.Cache.SetExpiration(30 * time.Second)
//设置缓存前缀
repo.Cache.SetPrefix("freedom")
}

// Get 通过id 获取商品实体.
func (repo *Goods) Get(id int) (goodsEntity *entity.Goods, e error) {
goodsEntity = &entity.Goods{}
goodsEntity.Id = id
//注入基础Entity 包含运行时和领域事件的producer
repo.InjectBaseEntity(goodsEntity)

//读取缓存, Identity() 会返回 id,缓存会使用它当key
return goodsEntity, repo.Cache.GetEntity(goodsEntity)
}

// Save 持久化实体.
func (repo *Goods) Save(entity *entity.Goods) error {
_, e := saveGoods(repo, entity) //写库,saveGoods是脚手架生成的函数,会做写时复制的处理。
//清空缓存
repo.Cache.Delete(entity)
return e
}

func (repo *Goods) FindsByPage(page, pageSize int, tag string) (entitys []*entity.Goods, e error) {
build := repo.NewORMDescBuilder("id").NewPageBuilder(page, pageSize) //创建分页器
e = findGoodsList(repo, po.Goods{Tag: tag}, &entitys, build)
if e != nil {
return
}
//注入基础Entity 包含运行时和领域事件的producer
repo.InjectBaseEntitys(entitys)
return
}

func (repo *Goods) New(name, tag string, price, stock int) (entityGoods *entity.Goods, e error) {
goods := po.Goods{Name: name, Price: price, Stock: stock, Tag: tag, Created: time.Now(), Updated: time.Now()}

_, e = createGoods(repo, &goods) //写库,createGoods是脚手架生成的函数。
if e != nil {
return
}
entityGoods = &entity.Goods{Goods: goods}
repo.InjectBaseEntity(entityGoods)
return
}

以下實現了一個商品的資源庫

package domain

import (
"github.com/8treenet/freedom/example/fshop/domain/dto"
"github.com/8treenet/freedom/example/fshop/adapter/repository"
"github.com/8treenet/freedom/example/fshop/domain/aggregate"
"github.com/8treenet/freedom/example/fshop/domain/entity"
"github.com/8treenet/freedom/infra/transaction"

"github.com/8treenet/freedom"
)

func init() {
freedom.Prepare(func(initiator freedom.Initiator) {
//绑定创建领域服务函数到框架,框架会根据客户的使用做依赖倒置和依赖注入的处理。
initiator.BindService(func() *Goods {
//创建 Goods领域服务
return &Goods{}
})
//控制器客户使用需要明确使用 InjectController
initiator.InjectController(func(ctx freedom.Context) (service *Goods) {
initiator.GetService(ctx, &service)
return
})
})
}

// Goods 商品领域服务.
type Goods struct {
Worker freedom.Worker //依赖注入请求运行时对象。
GoodsRepo repository.Goods //依赖注入商品仓库
}

// New 创建商品
func (g *Goods) New(name string, price int) (e error) {
g.Worker.Logger().Info("创建商品")
_, e = g.GoodsRepo.New(name, entity.GoodsNoneTag, price, 100)
return
}

// Items 分页商品列表
func (g *Goods) Items(page, pagesize int, tag string) (items []dto.GoodsItemRes, e error) {
entitys, e := g.GoodsRepo.FindsByPage(page, pagesize, tag)
if e != nil {
return
}

for i := 0; i < len(entitys); i++ { items = append(items, dto.GoodsItemRes{ Id: entitys[i].Id, Name: entitys[i].Name, Price: entitys[i].Price, Stock: entitys[i].Stock, Tag: entitys[i].Tag, }) } return } // AddStock 增加商品库存 func (g *Goods) AddStock(goodsId, num int) (e error) { entity, e := g.GoodsRepo.Get(goodsId) if e != nil { return } entity.AddStock(num) //增加库存 entity.DomainEvent("Goods.Stock", entity) //发布增加商品库存的领域事件 return g.GoodsRepo.Save(entity) }

領域服務使用倉庫

目錄

golang領域模型-開篇golang領域模型-六邊形架構golang領域模型-實體golang領域模型-資源庫golang領域模型-依賴倒置golang領域模型-聚合根golang領域模型-CQRSgolang領域模型-領域事件 項目代碼https://github.com/8treenet/freedom/tree/master/example/fshop

PS:加我微信:stargaze111,拉你加入DDD交流群,一起切磋DDD與代碼的藝術!