讓咱們使用領域驅動的方式,構建一個簡單的系統。html
新聞系統的需求以下:java
你們以爲,針對上面需求,大概須要多長時間能夠完成,能夠先寫下來。
構建項目,使用 http://start.spring.io 或使用模板工程,構建咱們的項目(Sprin Boot 項目),在這就很少敘述。
首先,添加 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>
在 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,env
新建 UserApplication 做爲應用入口類。
@SpringBootApplication @EnableSwagger2 public class UserApplication { public static void main(String... args){ SpringApplication.run(UserApplication.class, args); } }
使用 SpringBootApplication 和 EnableSwagger2 啓用 Spring Boot 和 Swagger 特性。mysql
首先,咱們對新聞類型進行建模。
新聞類別狀態,用於描述啓用、禁用兩個狀態。在這使用 enum 實現。
/** * GenCodeBasedEnumConverter 自動生成 CodeBasedNewsCategoryStatusConverter 類 */ @GenCodeBasedEnumConverter public 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; } }
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; }
在命令行或ida中執行maven命令,以對項目進行編譯,從而觸發代碼的自動生成。
mvn clean compile
咱們使用 NewsCategory 的靜態工廠,完成其建立邏輯。
首先,須要建立 NewsCategoryCreator,做爲工程參數。git
public class NewsCategoryCreator extends BaseNewsCategoryCreator<NewsCategoryCreator>{ }
其中 BaseNewsCategoryCreator 爲框架自動生成的,具體以下:web
@Data public 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 的初始化。spring
/** * 靜態工程,完成 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); }
更新邏輯,只對 name 進行更新操做。
首先,建立 NewsCategoryUpdater 做爲,更新方法的參數。sql
public class NewsCategoryUpdater extends BaseNewsCategoryUpdater<NewsCategoryUpdater>{ }
一樣,BaseNewsCategoryUpdater 也是框架自動生成,具體以下:數據庫
@Data public 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 方法:apache
/** * 更新 * @param updater */ public void update(NewsCategoryUpdater updater){ updater.accept(this); }
啓用,主要是對 status 的操做.
代碼以下:api
/** * 啓用 */ public void enable(){ setStatus(NewsCategoryStatus.ENABLE); }
禁用,主要是對 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); } }
查找邏輯主要由 NewsCategoryRepository 完成。
新建 NewsCategoryRepository,以下:
/** * GenApplication 自動將該接口中的方法添加到 BaseNewsCategoryRepository 中 */ @GenApplication public 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 爲框架自動生成,以下:
@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()); } }
至此,領域的建模工做已經完成,讓咱們對 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 方法。
接口已經準備好了,接下來,處理實現類,具體以下:
@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 <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); } }
該類中包含咱們想要的全部實現。
NewsInfoApplication 構建完成後,新建 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<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)); } }
至此,咱們的代碼就徹底準備好了,如今須要準備建表語句。
使用 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 );
至此,咱們就完成了 NewsCategory 的開發。
執行 maven 命令,啓動項目:
mvn clean spring-boot:run
瀏覽器中輸入 http://127.0.0.1:8090/swagger-ui.html , 經過 swagger 查看咱們的成果。
能夠看到以下
固然,可使用 swagger 進行簡單測試。
在 NewsCategory 的建模過程當中,咱們的主要精力放在了 NewsCategory 對象上,其餘部分基本都是框架幫咱們生成的。既然框架爲咱們作了那麼多工做,爲何還須要咱們新建 NewsCategoryApplication 和 NewsCategoryController呢?
答案,須要爲複雜邏輯預留擴展點。
整個過程,和 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); } }
NewsInfo 的建立邏輯中,須要對 NewsCategory 的存在性和狀態進行檢查,只有存在而且狀態爲 ENABLE 才能添加 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; }
該方法比較複雜,須要咱們手工處理。
在 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(); }
其餘部分不須要調整。
查找邏輯設計兩個部分:
在 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); } }
這樣,並解決了第一個問題。
查看 NewsInfoRepository 類,以下:
@GenApplication public 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); } }
經過默認方法將業務概念轉爲爲數據過濾。
至此,整個結構與 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 );
啓動項目,進行簡單測試。
你用了多長時間完成整個系統呢?