領域驅動設計,構建簡單的新聞系統,20分鐘夠嗎?

文心紫竹發表於2019-02-22

讓我們使用領域驅動的方式,構建一個簡單的系統。

1. 需求

新聞系統的需求如下:

  1. 建立新聞類別;
  2. 修改新聞類別,只能更改名稱;
  3. 禁用新聞類別,禁用後的類別不能新增新聞;
  4. 啟用新聞類別;
  5. 根據類別id獲取類別資訊;
  6. 指定新聞類別id,建立新聞;
  7. 更改新聞資訊,只能更改標題和內容;
  8. 禁用新聞;
  9. 啟用新聞;
  10. 分頁查詢給定類別的新聞,禁用的新聞不可見。

2. 工期估算

大家覺得,針對上面需求,大概需要多長時間可以完成,可以先寫下來。

3. 起航

3.1. 專案準備

構建專案,使用 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.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
複製程式碼
3.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 自動生成 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;
    }
}

複製程式碼
3.2.2. 建模 NewsCategory

NewsCategory 用於描述新聞類別,其中包括狀態、名稱等。

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 compile
複製程式碼
3.2.2.3. 建模 NewsCategory 建立邏輯

我們使用 NewsCategory 的靜態工廠,完成其建立邏輯。

首先,需要建立 NewsCategoryCreator,作為工程引數。

public class NewsCategoryCreator extends BaseNewsCategoryCreator<NewsCategoryCreator>{
}
複製程式碼

其中 BaseNewsCategoryCreator 為框架自動生成的,具體如下:

@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 的初始化。

/**
 * 靜態工程,完成 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 也是框架自動生成,具體如下:


@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 方法:

/**
 * 更新
 * @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 中
 */
@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());
  }
}

複製程式碼
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 方法。

介面已經準備好了,接下來,處理實現類,具體如下:

@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);
  }
}
複製程式碼

該類中包含我們想要的所有實現。

3.2.4. 構建 NewsCategoryController

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));
  }
}
複製程式碼
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 檢視我們的成果。

可以看到如下

領域驅動設計,構建簡單的新聞系統,20分鐘夠嗎?
當然,可以使用 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
 */
@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();
}
複製程式碼

其他部分不需要調整。

3.3.2. NewsInfo 查詢邏輯建模

查詢邏輯設計兩個部分:

  1. 根據 categoryId 進行分頁查詢;
  2. 禁用的 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 類,如下:

@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);
    }
}
複製程式碼

通過預設方法將業務概念轉為為資料過濾。

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 測試

啟動專案,進行簡單測試。

領域驅動設計,構建簡單的新聞系統,20分鐘夠嗎?

4. 總結

你用了多長時間完成整個系統呢?

專案地址見:gitee.com/litao851025…

相關文章