让我们使用领域驱动的方式,构建一个简单的系统。
1. 需求新闻系统的需求如下:
- 创建新闻类别;
- 修改新闻类别,只能更改名称;
- 禁用新闻类别,禁用后的类别不能添加新闻;
- 启用新闻类别;
- 根据类别id获取类别信息;
- 指定新闻类别id,创建新闻;
- 更改新闻信息,只能更改标题和内容;
- 禁用新闻;
- 启用新闻;
- 分页查找给定类别的新闻,禁用的新闻不可见。
3. 起航 3.1. 项目准备大家觉得,针对上面需求,大概需要多长时间可以完成,可以先写下来。
3.1.1. 添加依赖构建项目,使用 http://start.spring.io 或使用模板工程,构建我们的项目(Sprin Boot 项目),在这就不多叙述。
首先,添加 gh-ddd-lite 相关依赖和插件。
3.1.2. 添加配置信息4.0.0 com.geekhalo gh-ddd-lite-demo1.0.0-SNAPSHOT com.geekhalo gh-base-parent1.0.0-SNAPSHOT demo gh-${service.name}-service v1 ${service.name} Api /${service.name}-api com.geekhalo gh-ddd-lite1.0.0-SNAPSHOT com.geekhalo gh-ddd-lite-spring1.0.0-SNAPSHOT com.geekhalo gh-ddd-lite-codegen1.0.1-SNAPSHOT provided com.querydsl querydsl-aptprovided org.springframework.boot spring-boot-starter-weborg.springframework.boot spring-boot-starter-data-jpaorg.springframework.boot spring-boot-starter-testtest org.hibernate.javax.persistence hibernate-jpa-2.1-apimysql mysql-connector-javaorg.flywaydb flyway-corejavax.servlet javax.servlet-apiprovided io.springfox springfox-swagger2io.springfox springfox-swagger-uiorg.springframework.boot spring-boot-maven-pluginrepackage true ZIP com.mysema.maven apt-maven-plugin1.1.3 process target/generated-sources/java com.querydsl.apt.jpa.JPAAnnotationProcessor
在 application.properties 文件中添加数据库相关配置。
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://127.0.0.1:3306/db_test?useUnicode=true&characterEncoding=utf8&useSSL=false spring.datasource.username=root spring.datasource.password= spring.application.name=ddd-lite-demo server.port=8090 management.endpoint.beans.enabled=true management.endpoint.conditions.enabled=true management.endpoints.enabled-by-default=false management.endpoints.web.exposure.include=beans,conditions,env3.1.3. 添加入口类
新建 UserApplication 作为应用入口类。
@SpringBootApplication
@EnableSwagger2
public class UserApplication {
public static void main(String... args){
SpringApplication.run(UserApplication.class, args);
}
}
使用 SpringBootApplication 和 EnableSwagger2 启用 Spring Boot 和 Swagger 特性。
3.2. NewsCategory 建模3.2.1. 建模 NewsCategory 状态首先,我们对新闻类型进行建模。
新闻类别状态,用于描述启用、禁用两个状态。在这使用 enum 实现。
@GenCodebasedEnumConverter public enum NewsCategoryStatus implements CodebasedEnum3.2.2. 建模 NewsCategory{ ENABLE(1), DISABLE(0); private final int code; NewsCategoryStatus(int code) { this.code = code; } @Override public int getCode() { return code; } }
3.2.2.1. 新建 NewsCategoryNewsCategory 用于描述新闻类别,其中包括状态、名称等。
@EnableGenForAggregate
@Data
@Entity
@Table(name = "tb_news_category")
public class NewsCategory extends JpaAggregate {
private String name;
@Setter(AccessLevel.PRIVATE)
@Convert(converter = CodebasedNewsCategoryStatusConverter.class)
private NewsCategoryStatus status;
}
3.2.2.2. 自动生成 base 代码
在命令行或ida中执行maven命令,以对项目进行编译,从而触发代码的自动生成。
mvn clean compile3.2.2.3. 建模 NewsCategory 创建逻辑
我们使用 NewsCategory 的静态工厂,完成其创建逻辑。
首先,需要创建 NewsCategoryCreator,作为工程参数。
public class NewsCategoryCreator extends baseNewsCategoryCreator{ }
其中 baseNewsCategoryCreator 为框架自动生成的,具体如下:
@Data public abstract class baseNewsCategoryCreator{ @Setter(AccessLevel.PUBLIC) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = "", name = "name" ) private String name; public void accept(NewsCategory target) { target.setName(getName()); } }
接下来,需要创建静态工程,并完成 NewsCategory 的初始化。
public static NewsCategory create(NewsCategoryCreator creator){
NewsCategory category = new NewsCategory();
creator.accept(category);
category.init();
return category;
}
private void init() {
setStatus(NewsCategoryStatus.ENABLE);
}
3.2.2.4. 建模 NewsCategory 更新逻辑
更新逻辑,只对 name 进行更新操作。
首先,创建 NewsCategoryUpdater 作为,更新方法的参数。
public class NewsCategoryUpdater extends baseNewsCategoryUpdater{ }
同样,baseNewsCategoryUpdater 也是框架自动生成,具体如下:
@Data public abstract class baseNewsCategoryUpdater{ @Setter(AccessLevel.PRIVATE) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = "", name = "name" ) private DataOptional name; public T name(String name) { this.name = DataOptional.of(name); return (T) this; } public T acceptName(Consumer consumer) { if(this.name != null){ consumer.accept(this.name.getValue()); } return (T) this; } public void accept(NewsCategory target) { this.acceptName(target::setName); } }
添加 update 方法:
public void update(NewsCategoryUpdater updater){
updater.accept(this);
}
3.2.2.5. 建模 NewsCategory 启用逻辑
启用,主要是对 status 的操作.
代码如下:
public void enable(){
setStatus(NewsCategoryStatus.ENABLE);
}
3.2.2.6. 建模 NewsCategory 禁用逻辑
禁用,主要是对 status 的操作。
代码如下:
public void disable(){
setStatus(NewsCategoryStatus.DISABLE);
}
至此,NewsCategory 的 Command 就建模完成,让我们总体看下 NewsCategory:
@EnableGenForAggregate
@Data
@Entity
@Table(name = "tb_news_category")
public class NewsCategory extends JpaAggregate {
private String name;
@Setter(AccessLevel.PRIVATE)
@Convert(converter = CodebasedNewsCategoryStatusConverter.class)
private NewsCategoryStatus status;
private NewsCategory(){
}
public static NewsCategory create(NewsCategoryCreator creator){
NewsCategory category = new NewsCategory();
creator.accept(category);
category.init();
return category;
}
public void update(NewsCategoryUpdater updater){
updater.accept(this);
}
public void enable(){
setStatus(NewsCategoryStatus.ENABLE);
}
public void disable(){
setStatus(NewsCategoryStatus.DISABLE);
}
private void init() {
setStatus(NewsCategoryStatus.ENABLE);
}
}
3.2.2.7. 建模 NewsCategory 查找逻辑
查找逻辑主要由 NewsCategoryRepository 完成。
新建 NewsCategoryRepository,如下:
@GenApplication
public interface NewsCategoryRepository extends baseNewsCategoryRepository{
@Override
Optional getById(Long aLong);
}
同样, baseNewsCategoryRepository 也是自动生成的。
interface baseNewsCategoryRepository extends SpringDataRepositoryAdapter, Repository , QuerydslPredicateExecutor { }
领域对象 NewsCategory 不应该暴露到其他层,因此,我们使用 DTO 模式处理数据的返回,新建 NewsCategoryDto,具体如下:
public class NewsCategoryDto extends baseNewsCategoryDto{
public NewsCategoryDto(NewsCategory source) {
super(source);
}
}
baseNewsCategoryDto 为框架自动生成,如下:
@Data
public abstract class baseNewsCategoryDto extends JpaAggregateVo implements Serializable {
@Setter(AccessLevel.PACKAGE)
@Getter(AccessLevel.PUBLIC)
@ApiModelProperty(
value = "",
name = "name"
)
private String name;
@Setter(AccessLevel.PACKAGE)
@Getter(AccessLevel.PUBLIC)
@ApiModelProperty(
value = "",
name = "status"
)
private NewsCategoryStatus status;
protected baseNewsCategoryDto(NewsCategory source) {
super(source);
this.setName(source.getName());
this.setStatus(source.getStatus());
}
}
3.2.3. 构建 NewsCategoryApplication
至此,领域的建模工作已经完成,让我们对 Application 进行构建。
@GenController("com.geekhalo.ddd.lite.demo.controller.baseNewsCategoryController")
public interface NewsCategoryApplication extends baseNewsCategoryApplication{
@Override
NewsCategory create(NewsCategoryCreator creator);
@Override
void update(Long id, NewsCategoryUpdater updater);
@Override
void enable(Long id);
@Override
void disable(Long id);
@Override
Optional getById(Long aLong);
}
自动生成的 baseNewsCategoryApplication 如下:
public interface baseNewsCategoryApplication {
Optional getById(Long aLong);
NewsCategory create(NewsCategoryCreator creator);
void update(@Description("主键") Long id, NewsCategoryUpdater updater);
void enable(@Description("主键") Long id);
void disable(@Description("主键") Long id);
}
得益于我们的 EnableGenForAggregate 和 GenApplication 注解,baseNewsCategoryApplication 包含我们想要的 Command 和 Query 方法。
接口已经准备好了,接下来,处理实现类,具体如下:
@Service
public class NewsCategoryApplicationImpl extends baseNewsCategoryApplicationSupport
implements NewsCategoryApplication {
@Override
protected NewsCategoryDto convertNewsCategory(NewsCategory src) {
return new NewsCategoryDto(src);
}
}
自动生成的 baseNewsCategoryApplicationSupport 如下:
abstract class baseNewsCategoryApplicationSupport extends AbstractApplication implements baseNewsCategoryApplication {
@Autowired
private DomainEventBus domainEventBus;
@Autowired
private NewsCategoryRepository newsCategoryRepository;
protected baseNewsCategoryApplicationSupport(Logger logger) {
super(logger);
}
protected baseNewsCategoryApplicationSupport() {
}
protected NewsCategoryRepository getNewsCategoryRepository() {
return this.newsCategoryRepository;
}
protected DomainEventBus getDomainEventBus() {
return this.domainEventBus;
}
protected List convertNewsCategoryList(List src,
Function converter) {
if (CollectionUtils.isEmpty(src)) return Collections.emptyList();
return src.stream().map(converter).collect(Collectors.toList());
}
protected Page convvertNewsCategoryPage(Page src,
Function converter) {
return src.map(converter);
}
protected abstract NewsCategoryDto convertNewsCategory(NewsCategory src);
protected List convertNewsCategoryList(List src) {
return convertNewsCategoryList(src, this::convertNewsCategory);
}
protected Page convvertNewsCategoryPage(Page src) {
return convvertNewsCategoryPage(src, this::convertNewsCategory);
}
@Transactional(
readonly = true
)
public Optional getById(Long aLong, Function converter) {
Optional result = this.getNewsCategoryRepository().getById(aLong);
return result.map(converter);
}
@Transactional(
readonly = true
)
public Optional getById(Long aLong) {
Optional result = this.getNewsCategoryRepository().getById(aLong);
return result.map(this::convertNewsCategory);
}
@Transactional
public NewsCategory create(NewsCategoryCreator creator) {
NewsCategory result = creatorFor(this.getNewsCategoryRepository())
.publishBy(getDomainEventBus())
.instance(() -> NewsCategory.create(creator))
.call();
logger().info("success to create {} using parm {}",result.getId(), creator);
return result;
}
@Transactional
public void update(@Description("主键") Long id, NewsCategoryUpdater updater) {
NewsCategory result = updaterFor(this.getNewsCategoryRepository())
.publishBy(getDomainEventBus())
.id(id)
.update(agg -> agg.update(updater))
.call();
logger().info("success to update for {} using parm {}", id, updater);
}
@Transactional
public void enable(@Description("主键") Long id) {
NewsCategory result = updaterFor(this.getNewsCategoryRepository())
.publishBy(getDomainEventBus())
.id(id)
.update(agg -> agg.enable())
.call();
logger().info("success to enable for {} using parm ", id);
}
@Transactional
public void disable(@Description("主键") Long id) {
NewsCategory result = updaterFor(this.getNewsCategoryRepository())
.publishBy(getDomainEventBus())
.id(id)
.update(agg -> agg.disable())
.call();
logger().info("success to disable for {} using parm ", id);
}
}
该类中包含我们想要的所有实现。
3.2.4. 构建 NewsCategoryControllerNewsInfoApplication 构建完成后,新建 NewsCategoryController 将其暴露出去。
新建 NewsCategoryController, 如下:
@RequestMapping("news_category")
@RestController
public class NewsCategoryController extends baseNewsCategoryController{
}
是的,核心逻辑都在自动生成的 baseNewsCategoryController 中:
abstract class baseNewsCategoryController {
@Autowired
private NewsCategoryApplication application;
protected NewsCategoryApplication getApplication() {
return this.application;
}
@ResponseBody
@ApiOperation(
value = "",
nickname = "create"
)
@RequestMapping(
value = "/_create",
method = RequestMethod.POST
)
public ResultVo create(@RequestBody NewsCategoryCreator creator) {
return ResultVo.success(this.getApplication().create(creator));
}
@ResponseBody
@ApiOperation(
value = "",
nickname = "update"
)
@RequestMapping(
value = "{id}/_update",
method = RequestMethod.POST
)
public ResultVo update(@PathVariable("id") Long id,
@RequestBody NewsCategoryUpdater updater) {
this.getApplication().update(id, updater);
return ResultVo.success(null);
}
@ResponseBody
@ApiOperation(
value = "",
nickname = "enable"
)
@RequestMapping(
value = "{id}/_enable",
method = RequestMethod.POST
)
public ResultVo enable(@PathVariable("id") Long id) {
this.getApplication().enable(id);
return ResultVo.success(null);
}
@ResponseBody
@ApiOperation(
value = "",
nickname = "disable"
)
@RequestMapping(
value = "{id}/_disable",
method = RequestMethod.POST
)
public ResultVo disable(@PathVariable("id") Long id) {
this.getApplication().disable(id);
return ResultVo.success(null);
}
@ResponseBody
@ApiOperation(
value = "",
nickname = "getById"
)
@RequestMapping(
value = "/{id}",
method = RequestMethod.GET
)
public ResultVo getById(@PathVariable Long id) {
return ResultVo.success(this.getApplication().getById(id).orElse(null));
}
}
3.2.5. 数据库准备
至此,我们的代码就完全准备好了,现在需要准备建表语句。
使用 Flyway 作为数据库的版本管理,在 resources/db/migration 新建 V1.002__create_news_category.sql 文件,具体如下:
create table tb_news_category ( id bigint auto_increment primary key, name varchar(32) null, status tinyint null, create_time bigint not null, update_time bigint not null, version tinyint not null );3.2.6. 测试
至此,我们就完成了 NewsCategory 的开发。
执行 maven 命令,启动项目:
mvn clean spring-boot:run
浏览器中输入 http://127.0.0.1:8090/swagger-ui.html , 通过 swagger 查看我们的成果。
可以看到如下
当然,可以使用 swagger 进行简单测试。
在 NewsCategory 的建模过程中,我们的主要精力放在了 NewsCategory 对象上,其他部分基本都是框架帮我们生成的。既然框架为我们做了那么多工作,为什么还需要我们新建 NewsCategoryApplication 和 NewsCategoryController呢?
答案,需要为复杂逻辑预留扩展点。
3.3.1. NewsInfo 建模整个过程,和 NewsCategory 基本一致,在此不在重复,只选择差异点进行说明。
NewsInfo 最终代码如下:
@EnableGenForAggregate
@Index("categoryId")
@Data
@Entity
@Table(name = "tb_news_info")
public class NewsInfo extends JpaAggregate {
@Column(name = "category_id", updatable = false)
private Long categoryId;
@Setter(AccessLevel.PRIVATE)
@Convert(converter = CodebasedNewsInfoStatusConverter.class)
private NewsInfoStatus status;
private String title;
private String content;
private NewsInfo(){
}
@GenApplicationIgnore
public static NewsInfo create(Optional category, NewsInfoCreator creator){
// 对 NewsCategory 的存在性和状态进行验证
if (!category.isPresent() || category.get().getStatus() != NewsCategoryStatus.ENABLE){
throw new IllegalArgumentException();
}
NewsInfo newsInfo = new NewsInfo();
creator.accept(newsInfo);
newsInfo.init();
return newsInfo;
}
public void update(NewsInfoUpdater updater){
updater.accept(this);
}
public void enable(){
setStatus(NewsInfoStatus.ENABLE);
}
public void disable(){
setStatus(NewsInfoStatus.DISABLE);
}
private void init() {
setStatus(NewsInfoStatus.ENABLE);
}
}
3.3.1.1. NewsInfo 创建逻辑建模
NewsInfo 的创建逻辑中,需要对 NewsCategory 的存在性和状态进行检查,只有存在并且状态为 ENABLE 才能添加 NewsInfo。
具体实现如下:
@GenApplicationIgnore public static NewsInfo create(Optionalcategory, NewsInfoCreator creator){ // 对 NewsCategory 的存在性和状态进行验证 if (!category.isPresent() || category.get().getStatus() != NewsCategoryStatus.ENABLE){ throw new IllegalArgumentException(); } NewsInfo newsInfo = new NewsInfo(); creator.accept(newsInfo); newsInfo.init(); return newsInfo; }
该方法比较复杂,需要我们手工处理。
在 NewsInfoApplication 中手工添加创建方法:
@GenController("com.geekhalo.ddd.lite.demo.controller.baseNewsInfoController")
public interface NewsInfoApplication extends baseNewsInfoApplication{
// 手工维护方法
NewsInfo create(Long categoryId, NewsInfoCreator creator);
}
在 NewsInfoApplicationImpl 添加实现:
@Autowired
private NewsCategoryRepository newsCategoryRepository;
@Override
public NewsInfo create(Long categoryId, NewsInfoCreator creator) {
return creatorFor(getNewsInfoRepository())
.publishBy(getDomainEventBus())
.instance(()-> NewsInfo.create(this.newsCategoryRepository.getById(categoryId), creator))
.call();
}
其他部分不需要调整。
3.3.2. NewsInfo 查找逻辑建模查找逻辑设计两个部分:
- 根据 categoryId 进行分页查找;
- 禁用的 NewsInfo 在查找中不可见。
在 NewsInfo 类上多了一个 @Index(“categoryId”) 注解,该注解会在 baseNewsInfoRepository 中添加以 categoryId 为维度的查询。
interface baseNewsInfoRepository extends SpringDataRepositoryAdapter, Repository , QuerydslPredicateExecutor { Long countByCategoryId(Long categoryId); default Long countByCategoryId(Long categoryId, Predicate predicate) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QNewsInfo.newsInfo.categoryId.eq(categoryId));; booleanBuilder.and(predicate); return this.count(booleanBuilder.getValue()); } List getByCategoryId(Long categoryId); List getByCategoryId(Long categoryId, Sort sort); default List getByCategoryId(Long categoryId, Predicate predicate) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QNewsInfo.newsInfo.categoryId.eq(categoryId));; booleanBuilder.and(predicate); return Lists.newArrayList(findAll(booleanBuilder.getValue())); } default List getByCategoryId(Long categoryId, Predicate predicate, Sort sort) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QNewsInfo.newsInfo.categoryId.eq(categoryId));; booleanBuilder.and(predicate); return Lists.newArrayList(findAll(booleanBuilder.getValue(), sort)); } Page findByCategoryId(Long categoryId, Pageable pageable); default Page findByCategoryId(Long categoryId, Predicate predicate, Pageable pageable) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QNewsInfo.newsInfo.categoryId.eq(categoryId));; booleanBuilder.and(predicate); return findAll(booleanBuilder.getValue(), pageable); } }
这样,并解决了第一个问题。
3.3.2.2. 默认方法查看 NewsInfoRepository 类,如下:
@GenApplication
public interface NewsInfoRepository extends baseNewsInfoRepository{
default Page findValidByCategoryId(Long categoryId, Pageable pageable){
// 查找有效状态
Predicate valid = QNewsInfo.newsInfo.status.eq(NewsInfoStatus.ENABLE);
return findByCategoryId(categoryId, valid, pageable);
}
}
通过默认方法将业务概念转为为数据过滤。
3.3.3. NewsInfo 数据库准备至此,整个结构与 NewsCategory 再无区别。
最后,我们添加数据库文件 V1.003__create_news_info.sql :
create table tb_news_info ( id bigint auto_increment primary key, category_id bigint not null, status tinyint null, title varchar(64) not null, content text null, create_time bigint not null, update_time bigint not null, version tinyint not null );3.3.4. NewsInfo 测试
4. 总结启动项目,进行简单测试。
你用了多长时间完成整个系统呢?
项目地址见:https://gitee.com/litao851025/geekhalo-ddd



