商品领域ddd_为 Gopher 打造 DDD 系列:领域模型-资源库

前言: 作为领域模型中最重要的环节之一的Repository,其通过对外暴露接口屏蔽了内部的复杂性,又有其隐式写时复制的巧妙代码设计,完美的将DDD中的Repository的概念与代码相结合!

Repository

资源库通常标识一个存储的区域,提供读写功能。通常我们将实体存放在资源库中,之后通过该资源库来获取相同的实体,每一个实体都搭配一个资源库。

如果你修改了某个实体,也需要通过资源库去持久化。当然你也可以通过资源库去删除某一个实体。

资源库对外部是屏蔽了存储细节的,资源库内部去处理 cache、es、db。

format,png数据操作流程

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 = priceobj.changes["price"] = price //写时复制}// SetStock .func (obj *Goods) SetStock(stock int) {obj.Stock = stockobj.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

实体的缓存

这个是缓存组件的接口,可以读写实体,实体的key 使用必须实现的Identity 方法。一级缓存是基于请求的,首先会从一级缓存查找实体,生命周期是一个请求的开始和结束。

二级缓存是基于redis。

组件已经做了幂等的防击穿处理。

SetSource设置持久化的回调函数,当一、二级缓存未命中,会读取回调函数,并反写一、二级缓存。// freedom.Entitytype Entity interface {DomainEvent(string, interface{},...map[string]string)Identity() stringGetWorker() WorkerSetProducer(string)Marshal() []byte}// infra.EntityCachetype 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}以下实现了一个商品的资源库package repositoryimport ("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 {return &Goods{}})})}// Goods .type Goods struct {freedom.Repository //资源库必须继承,这样是为了约束 db、redis、http等的访问Cache store.EntityCache //依赖注入实体缓存组件}// BeginRequestfunc (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 包含运行时和领域事件的producerrepo.InjectBaseEntity(goodsEntity)//读取缓存, Identity() 会返回 id,缓存会使用它当keyreturn 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 包含运行时和领域事件的producerrepo.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 domainimport ("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 {return &Goods{}})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)}

项目代码 https://github.com/8treenet/freedom/tree/master/example/fshop

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/558579.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

mysql5.7主从全备恢复_Mysql5.7—运维常用备份方式(超全)

小生博客&#xff1a;http://xsboke.blog.51cto.com小生 Q Q&#xff1a;1770058260-------谢谢您的参考&#xff0c;如有疑问&#xff0c;欢迎交流一、 Mysqldump备份结合binlog日志恢复使用mysqldump进行全库备份&#xff0c;并使用binlog日志备份&#xff0c;还原时&#xf…

客户端连接mysql 自动断开_MySql连接空闲8小时自动断开的原因及连接池配置方法...

数据库连接超时时间查询非交互式超时时间&#xff0c;如 JDBC 程序show global variables like wait_timeout;交互式超时时间&#xff0c;如数据库工具show global variables like interactive_timeout;MySQL服务器默认的“wait_timeout”是28800秒即8小时&#xff0c;意味着如…

docker 运行容器_Docker之运行 Django 容器

首先此篇笔记默认你已经安装好了 Docker&#xff0c;并了解 Docker 的基础概念&#xff0c;诸如镜像、容器、以及他们之间的关系等。如果不太了解&#xff0c;等我回头了解清楚以后&#xff0c;可以再写一篇文章阐述一下。&#xff08;狗头当然&#xff0c;对于这篇文章&#x…

mysql类似的数据库_MemSQL学习笔记-类似MySQL的数据库

http://gigaom.com/cloud/ex-facebookers-launch-memsql-to-make-your-database-fly/-- 多主-从http://www.mysqlops.com/2012/02/14/diy_multi_master_replication.htmlhttp://www.cnblogs.com/liuhao/archive/2012/06/26/2563702.html前facebook员工和前微软sql server工程师…

mysql8.0与mysql7.0_MySQL 5.7 vs 8.0,哪个性能更牛?

测试mysql5.7和mysql8.0分别在读写&#xff0c;选定&#xff0c;只写模式下不同并发时的性能(tps&#xff0c;qps)最早测试使用版本为mysql5.7.22和mysql8.0.15sysbench测试前先重启mysql服务&#xff0c;并清除os的缓存(避免多次测试时命中缓存)每次进行测试都是新生成测试数据…

linux+mysql+导出备份_Linux系统MySQL备份的导入导出的具体分析

问题描述如何对 ECS Linux 系统中的 MySQL 进行备份的导入和导出。处理办法MySQL 备份的导出MySQL 备份的导入MySQL 备份的导出注意&#xff1a;如果您使用的是帮助中心的一键环境配置&#xff0c;那么 MySQL 的安装目录是 /alidata/server/mysql。如果您将 MySQL 安装到其他目…

springmvc使用requestmapping无法访问控制类_研究人员称人类使用的新烟碱类杀虫剂让蜜蜂无法入睡...

来自布里斯托尔大学的科学家进行了研究&#xff0c;显示常见的杀虫剂可以阻止蜜蜂和苍蝇睡个好觉。就像人类一样&#xff0c;许多昆虫也需要睡眠才能正常工作。然而&#xff0c;如果它们接触过新烟碱类杀虫剂&#xff0c;它们的睡眠就会受到影响&#xff0c;新烟碱类杀虫剂是一…

linux 监控mysql脚本_Linux系统MySQL主从同步监控shell脚本

操作系统&#xff1a;CentOS系统目的&#xff1a;定时监控MySQL数据库主从是否同步&#xff0c;如果不同步&#xff0c;记录故障时间&#xff0c;并执行命令使主从恢复同步状态1、创建脚本文件vi /home/crontab/check_mysql_slave.sh #编辑&#xff0c;添加下面代码#!/bin/sh…

mysql 1066解决方法_MySQL查询语法帮助:错误#1066-表格/别名不唯...

我有四个表,user,user_billingprofile,user_shippingprofile和user_address.用户&#xff1a;userId,dateCreateduser_billingprofile&#xff1a;userId,地址user_shippingprofile&#xff1a;userId,地址user_address&#xff1a;随机地址废话这是我必须一目了然地获取用户帐…

python创建新进程_Python:创建新进程

我是Python新手。我应该创建一个有多个菜单的GUI。单击特定菜单时&#xff0c;应启动一个新进程&#xff0c;并且不应挂起用户界面。但我不能做到这一点。在网上搜索之后&#xff0c;我做了一个类似的代码。在在这段代码中&#xff0c;我的目标是使“print deep”语句处于活动状…

python协成_Python协程(上)

几个概念&#xff1a;event_loop 事件循环&#xff1a;程序开启一个无限的循环&#xff0c;程序员会把一些函数注册到事件循环上。当满足事件发生的时候&#xff0c;调用相应的协程函数。coroutine 协程&#xff1a;协程对象&#xff0c;指一个使用async关键字定义的函数&#…

python参数检验框架_基于python的人员识别检测框架研究

import numpy as np # 数据处理的库 Numpyimport cv2 # 图像处理的库 OpenCvimport osimport shutilimport _threadimport wximport csvfrom importlib import reloadfrom skimage import io as iioimport face_recognize_punchcardimport sys# 创建 cv2 摄像头对象#…

js父元素获取子元素img_css,前端_父标签div中包含一个子元素img标签,子元素div标签,为什么img要加上浮动,子元素div才会处于正常位置?,css,前端 - phpStudy...

父标签div中包含一个子元素img标签&#xff0c;子元素div标签&#xff0c;为什么img要加上浮动&#xff0c;子元素div才会处于正常位置&#xff1f;dom结构如图img加上float 子元素div显示正常。不加float div显示错位。附上我写的一个dome测试用的&#xff0c;大家可本地看下究…

android运营商获取本机号码_一键登录已成大势所趋,Android端操作指南来啦!

根据极光(Aurora Mobile)发布的《2019年Q2移动互联网行业数据研究报告》&#xff0c;2019年第二季度&#xff0c;移动网民人均安装APP总量已达56款。面对如此繁多的APP&#xff0c;想在用户的手机中占据一席之地&#xff0c;移动开发者们就不得不努力提升用户体验。而现实却是&…

mysql数据库中删除数据用什么语句_mysql数据库删除数据语句

{"moduleinfo":{"card_count":[{"count_phone":1,"count":1}],"search_count":[{"count_phone":4,"count":4}]},"card":[{"des":"阿里云数据库专家保驾护航&#xff0c;为用户…

spring批量写入mysql数据库_MyBatis-spring和spring JDBC批量插入Mysql的效率比较

工具框架用spring-batch&#xff0c;数据库是mysql(未做特殊优化)。比较数据框架mybatis和spring jdbc的插入效率。Mybatis三种实现&#xff1a;1、mybatis的官方写法Java代码publicvoidbatchInsert1(List poilist)throwsException {SqlSession sqlSession sqlSessionFactory.…

mysql启动触发器_MYSQL中禁用/启动触发器

在使用MYSQL过程中&#xff0c;经常会使用到触发器&#xff0c;但是有时使用不当会造成一些麻烦。有没有一种办法可以控制触发器的调用呢&#xff1f;触发器顾名思义就是数据库在一定的调条件自动调用的SQL语句&#xff0c;触发器拒绝了人工调用的过程&#xff0c;由数据库MYSQ…

mysql数据库权威指南_MySQL_MySQL权威指南读书笔记(三),第二章:MYSQL数据库里面的数 - phpStudy...

MySQL权威指南读书笔记(三)第二章&#xff1a;MYSQL数据库里面的数据用想用好MYSQL&#xff0c;就必须透彻理解MYSQL是如何看待和处理数据的。本章主要讨论了两个问题&#xff1a;一是&#xff33;&#xff31;&#xff2c;所能处理的数据值的类型&#xff1b;二是这些数据类型…

mysql 批量替换 所有表_[收藏]批量替换一个数据库中所有表中所有记录

/***********批量替换一个数据库中所有表中所有记录************/declare delStr nvarchar(500)set delStr这里是要替换的字符/**********以下为操作实体************/set nocount ondeclare tableName nvarchar(100),columnName nvarchar(100),tbID int,iRow int,iResult intd…

java偶数和_Java编程计算1-100之间所有偶数的和。

展开全部 public class Demo05ShiCao{public static void main(String[] args){int sum1 = 0; for(int i = 1; i <= 100; i++){if(i % 2 == 0){sum1 +=i; } } System.out.println("结果e5a48de588b63231313335323631343130323136353331333366303138是:" + sum1);…