让我们使用领域驱动的方式,构建一个简单的系统。1. 需求新闻系统的需求如下:创建新闻类别;修改新闻类别,只能更改名称;禁用新闻类别,禁用后的类别不能添加新闻;启用新闻类别;根据类别id获取类别信息;指定新闻类别id,创建新闻;更改新闻信息,只能更改标题和内容;禁用新闻;启用新闻;分页查找给定类别的新闻,禁用的新闻不可见。2. 工期估算大家觉得,针对上面需求,大概需要多长时间可以完成,可以先写下来。3. 起航3.1. 项目准备构建项目,使用 http://start.spring.io 或使用模板工程,构建我们的项目(Sprin Boot 项目),在这就不多叙述。3.1.1. 添加依赖首先,添加 gh-ddd-lite 相关依赖和插件。<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.geekhalo</groupId> <artifactId>gh-ddd-lite-demo</artifactId> <version>1.0.0-SNAPSHOT</version> <parent> <groupId>com.geekhalo</groupId> <artifactId>gh-base-parent</artifactId> <version>1.0.0-SNAPSHOT</version> </parent> <properties> <service.name>demo</service.name> <server.name>gh-${service.name}-service</server.name> <server.version>v1</server.version> <server.description>${service.name} Api</server.description> <servlet.basePath>/${service.name}-api</servlet.basePath> </properties> <dependencies> <dependency> <groupId>com.geekhalo</groupId> <artifactId>gh-ddd-lite</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.geekhalo</groupId> <artifactId>gh-ddd-lite-spring</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.geekhalo</groupId> <artifactId>gh-ddd-lite-codegen</artifactId> <version>1.0.1-SNAPSHOT</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.hibernate.javax.persistence</groupId> <artifactId>hibernate-jpa-2.1-api</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> <configuration> <executable>true</executable> <layout>ZIP</layout> </configuration> </plugin> <plugin> <groupId>com.mysema.maven</groupId> <artifactId>apt-maven-plugin</artifactId> <version>1.1.3</version> <executions> <execution> <goals> <goal>process</goal> </goals> <configuration> <outputDirectory>target/generated-sources/java</outputDirectory> <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor> <!–<processor>com.querydsl.apt.QuerydslAnnotationProcessor</processor>–> </configuration> </execution> </executions> </plugin> </plugins> </build></project>3.1.2. 添加配置信息在 application.properties 文件中添加数据库相关配置。spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driverspring.datasource.url=jdbc:mysql://127.0.0.1:3306/db_test?useUnicode=true&characterEncoding=utf8&useSSL=falsespring.datasource.username=rootspring.datasource.password=spring.application.name=ddd-lite-demoserver.port=8090management.endpoint.beans.enabled=truemanagement.endpoint.conditions.enabled=truemanagement.endpoints.enabled-by-default=falsemanagement.endpoints.web.exposure.include=beans,conditions,env3.1.3. 添加入口类新建 UserApplication 作为应用入口类。@SpringBootApplication@EnableSwagger2public 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 自动生成 CodeBasedNewsCategoryStatusConverter 类 /@GenCodeBasedEnumConverterpublic enum NewsCategoryStatus implements CodeBasedEnum<NewsCategoryStatus> { ENABLE(1), DISABLE(0); private final int code; NewsCategoryStatus(int code) { this.code = code; } @Override public int getCode() { return code; }}3.2.2. 建模 NewsCategoryNewsCategory 用于描述新闻类别,其中包括状态、名称等。3.2.2.1. 新建 NewsCategory/* * EnableGenForAggregate 自动创建聚合相关的 Base 类 /@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<NewsCategoryCreator>{}其中 BaseNewsCategoryCreator 为框架自动生成的,具体如下:@Datapublic abstract class BaseNewsCategoryCreator<T extends BaseNewsCategoryCreator> { @Setter(AccessLevel.PUBLIC) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “name” ) private String name; public void accept(NewsCategory target) { target.setName(getName()); }}接下来,需要创建静态工程,并完成 NewsCategory 的初始化。/* * 静态工程,完成 NewsCategory 的创建 * @param creator * @return /public static NewsCategory create(NewsCategoryCreator creator){ NewsCategory category = new NewsCategory(); creator.accept(category); category.init(); return category;}/* * 初始化,默认状态位 ENABLE /private void init() { setStatus(NewsCategoryStatus.ENABLE);}3.2.2.4. 建模 NewsCategory 更新逻辑更新逻辑,只对 name 进行更新操作。首先,创建 NewsCategoryUpdater 作为,更新方法的参数。public class NewsCategoryUpdater extends BaseNewsCategoryUpdater<NewsCategoryUpdater>{}同样,BaseNewsCategoryUpdater 也是框架自动生成,具体如下:@Datapublic abstract class BaseNewsCategoryUpdater<T extends BaseNewsCategoryUpdater> { @Setter(AccessLevel.PRIVATE) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “name” ) private DataOptional<String> name; public T name(String name) { this.name = DataOptional.of(name); return (T) this; } public T acceptName(Consumer<String> consumer) { if(this.name != null){ consumer.accept(this.name.getValue()); } return (T) this; } public void accept(NewsCategory target) { this.acceptName(target::setName); }}添加 update 方法:/* * 更新 * @param updater /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 自动创建聚合相关的 Base 类 /@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(){ } /* * 静态工程,完成 NewsCategory 的创建 * @param creator * @return / public static NewsCategory create(NewsCategoryCreator creator){ NewsCategory category = new NewsCategory(); creator.accept(category); category.init(); return category; } /* * 更新 * @param updater / public void update(NewsCategoryUpdater updater){ updater.accept(this); } /* * 启用 / public void enable(){ setStatus(NewsCategoryStatus.ENABLE); } /* * 禁用 / public void disable(){ setStatus(NewsCategoryStatus.DISABLE); } /* * 初始化,默认状态位 ENABLE / private void init() { setStatus(NewsCategoryStatus.ENABLE); }}3.2.2.7. 建模 NewsCategory 查找逻辑查找逻辑主要由 NewsCategoryRepository 完成。新建 NewsCategoryRepository,如下:/* * GenApplication 自动将该接口中的方法添加到 BaseNewsCategoryRepository 中 /@GenApplicationpublic interface NewsCategoryRepository extends BaseNewsCategoryRepository{ @Override Optional<NewsCategory> getById(Long aLong);}同样, BaseNewsCategoryRepository 也是自动生成的。interface BaseNewsCategoryRepository extends SpringDataRepositoryAdapter<Long, NewsCategory>, Repository<NewsCategory, Long>, QuerydslPredicateExecutor<NewsCategory> {}领域对象 NewsCategory 不应该暴露到其他层,因此,我们使用 DTO 模式处理数据的返回,新建 NewsCategoryDto,具体如下:public class NewsCategoryDto extends BaseNewsCategoryDto{ public NewsCategoryDto(NewsCategory source) { super(source); }}BaseNewsCategoryDto 为框架自动生成,如下:@Datapublic 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 自动将该类中的方法,添加到 BaseNewsCategoryController 中 /@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<NewsCategoryDto> getById(Long aLong);}自动生成的 BaseNewsCategoryApplication 如下:public interface BaseNewsCategoryApplication { Optional<NewsCategoryDto> 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 方法。接口已经准备好了,接下来,处理实现类,具体如下:@Servicepublic 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 <T> List<T> convertNewsCategoryList(List<NewsCategory> src, Function<NewsCategory, T> converter) { if (CollectionUtils.isEmpty(src)) return Collections.emptyList(); return src.stream().map(converter).collect(Collectors.toList()); } protected <T> Page<T> convvertNewsCategoryPage(Page<NewsCategory> src, Function<NewsCategory, T> converter) { return src.map(converter); } protected abstract NewsCategoryDto convertNewsCategory(NewsCategory src); protected List<NewsCategoryDto> convertNewsCategoryList(List<NewsCategory> src) { return convertNewsCategoryList(src, this::convertNewsCategory); } protected Page<NewsCategoryDto> convvertNewsCategoryPage(Page<NewsCategory> src) { return convvertNewsCategoryPage(src, this::convertNewsCategory); } @Transactional( readOnly = true ) public <T> Optional<T> getById(Long aLong, Function<NewsCategory, T> converter) { Optional<NewsCategory> result = this.getNewsCategoryRepository().getById(aLong); return result.map(converter); } @Transactional( readOnly = true ) public Optional<NewsCategoryDto> getById(Long aLong) { Optional<NewsCategory> 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”)@RestControllerpublic 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<NewsCategory> 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<Void> 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<Void> 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<Void> 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<NewsCategoryDto> 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 进行简单测试。3.3. NewsInfo 建模在 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 创建 BaseNewsInfoApplication 时,忽略该方法,因为 Optional<NewsCategory> category 需要通过 逻辑进行获取 * @param category * @param creator * @return / @GenApplicationIgnore public static NewsInfo create(Optional<NewsCategory> 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 创建 BaseNewsInfoApplication 时,忽略该方法,因为 Optional<NewsCategory> category 需要通过 逻辑进行获取 * @param category * @param creator * @return */@GenApplicationIgnorepublic static NewsInfo create(Optional<NewsCategory> 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;}该方法比较复杂,需要我们手工处理。在 NewsInfoApplication 中手工添加创建方法:@GenController(“com.geekhalo.ddd.lite.demo.controller.BaseNewsInfoController”)public interface NewsInfoApplication extends BaseNewsInfoApplication{ // 手工维护方法 NewsInfo create(Long categoryId, NewsInfoCreator creator);}在 NewsInfoApplicationImpl 添加实现:@Autowiredprivate NewsCategoryRepository newsCategoryRepository;@Overridepublic 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 在查找中不可见。3.3.2.1. Index 注解在 NewsInfo 类上多了一个 @Index(“categoryId”) 注解,该注解会在 BaseNewsInfoRepository 中添加以 categoryId 为维度的查询。interface BaseNewsInfoRepository extends SpringDataRepositoryAdapter<Long, NewsInfo>, Repository<NewsInfo, Long>, QuerydslPredicateExecutor<NewsInfo> { 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<NewsInfo> getByCategoryId(Long categoryId); List<NewsInfo> getByCategoryId(Long categoryId, Sort sort); default List<NewsInfo> 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<NewsInfo> 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<NewsInfo> findByCategoryId(Long categoryId, Pageable pageable); default Page<NewsInfo> 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 类,如下:@GenApplicationpublic interface NewsInfoRepository extends BaseNewsInfoRepository{ default Page<NewsInfo> 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…