重构领域模型:如何摆脱@PersistenceCreator等基础设施注解依赖?
第一种思路:用工厂类封装领域对象的重建逻辑
核心是把从持久化层加载Ticket的逻辑,从领域类本身挪到领域层的工厂类里。仓库只负责获取数据库里的原始数据(比如用DTO或者Map),然后交给工厂来创建Ticket实例,这样领域类完全不需要任何持久化相关的注解。
调整后的领域类Ticket
把原来的私有全参构造器换成包级私有构造器,专门给同包下的工厂使用,同时保留创建新工单的公开构造器:
package com.kaue.ticketservice.domain.model; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; import lombok.ToString; import java.time.Instant; import java.time.OffsetDateTime; @ToString public class Ticket { @Schema(description = "User email") @Getter final private String requesterEmail; @Schema(description = "Assignee / Agent email") @Getter @Setter private String assigneeEmail; @Schema(description = "Title of the ticket") @Getter final private String title; @Schema(description = "Ticket description") @Getter @Setter private String description; @Schema(description = "Create date") @Getter final private Instant createDate; @Getter @Setter private Instant updatedDate; @Schema(description = "Resolution date") @Getter @Setter private OffsetDateTime resolutionDate; // 创建新工单的公开构造器 public Ticket(String requesterEmail, String title, String description) { this.requesterEmail = requesterEmail; this.title = title; this.description = description; this.createDate = Instant.now(); } // 包级私有构造器,仅给同包工厂使用 Ticket(String requesterEmail, String title, String description, Instant createDate) { this.requesterEmail = requesterEmail; this.title = title; this.description = description; this.createDate = createDate; } }
领域层的Ticket工厂类
专门负责从持久化数据重建Ticket:
package com.kaue.ticketservice.domain.model; import java.time.Instant; import java.time.OffsetDateTime; public class TicketFactory { public static Ticket fromPersistentData(String requesterEmail, String assigneeEmail, String title, String description, Instant createDate, Instant updatedDate, OffsetDateTime resolutionDate) { Ticket ticket = new Ticket(requesterEmail, title, description, createDate); ticket.setAssigneeEmail(assigneeEmail); ticket.setDescription(description); ticket.setUpdatedDate(updatedDate); ticket.setResolutionDate(resolutionDate); return ticket; } }
基础设施层的仓库实现
用MongoTemplate查询出持久化文档,再通过工厂转成领域类:
package com.kaue.ticketservice.infrastructure.persistence; import com.kaue.ticketservice.domain.model.Ticket; import com.kaue.ticketservice.domain.model.TicketFactory; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Query; import java.util.List; import java.util.stream.Collectors; public class TicketRepositoryImpl { private final MongoTemplate mongoTemplate; public TicketRepositoryImpl(MongoTemplate mongoTemplate) { this.mongoTemplate = mongoTemplate; } public List<Ticket> findAll() { List<TicketDocument> documents = mongoTemplate.find(new Query(), TicketDocument.class); return documents.stream() .map(doc -> TicketFactory.fromPersistentData( doc.getRequesterEmail(), doc.getAssigneeEmail(), doc.getTitle(), doc.getDescription(), doc.getCreateDate(), doc.getUpdatedDate(), doc.getResolutionDate() )) .collect(Collectors.toList()); } }
这里的TicketDocument是基础设施层的专属实体,只和MongoDB交互,领域层完全不依赖它。
第二种思路:用持久化DTO做中间层,隔离领域类和数据库
创建一个专门的持久化DTO(比如TicketDocument),让Spring Data直接映射到这个DTO上,然后在仓库实现里把DTO转换成领域类Ticket。这样领域类完全不用碰任何持久化相关的代码,保持纯净。
基础设施层的TicketDocument
package com.kaue.ticketservice.infrastructure.persistence; import lombok.Getter; import lombok.Setter; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.mongodb.core.mapping.Document; import java.time.Instant; import java.time.OffsetDateTime; @Document(collection = "tickets") @Getter @Setter public class TicketDocument { @Id private String id; private String requesterEmail; private String assigneeEmail; private String title; private String description; private Instant createDate; @LastModifiedDate private Instant updatedDate; private OffsetDateTime resolutionDate; }
仓库的转换逻辑
在仓库实现中,把查询到的TicketDocument转换成领域类Ticket,这里可以直接用工厂方法:
package com.kaue.ticketservice.infrastructure.persistence; import com.kaue.ticketservice.domain.model.Ticket; import com.kaue.ticketservice.domain.repository.TicketRepository; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Query; import org.springframework.stereotype.Repository; import java.util.List; import java.util.stream.Collectors; @Repository public class MongoTicketRepository implements TicketRepository { private final MongoTemplate mongoTemplate; public MongoTicketRepository(MongoTemplate mongoTemplate) { this.mongoTemplate = mongoTemplate; } @Override public List<Ticket> findAll() { List<TicketDocument> docs = mongoTemplate.find(new Query(), TicketDocument.class); return docs.stream() .map(doc -> TicketFactory.fromPersistentData( doc.getRequesterEmail(), doc.getAssigneeEmail(), doc.getTitle(), doc.getDescription(), doc.getCreateDate(), doc.getUpdatedDate(), doc.getResolutionDate() )) .collect(Collectors.toList()); } }
第三种思路:调整构造器,让Spring Data自动识别,不用加注解
如果不想搞工厂或DTO,也可以调整领域类的构造器,让Spring Data MongoDB自动匹配。Spring Data会优先找参数名称和数据库字段名一致的构造器,只要编译时保留参数名称,就不用加@PersistenceCreator注解。
修改领域类构造器
去掉@PersistenceCreator注解,保留私有全参构造器:
// 移除@PersistenceCreator注解 private Ticket(String requesterEmail, String assigneeEmail, String title, String description, Instant createDate, Instant updatedDate, OffsetDateTime resolutionDate) { this.requesterEmail = requesterEmail; this.assigneeEmail = assigneeEmail; this.title = title; this.description = description; this.createDate = createDate; this.updatedDate = updatedDate; this.resolutionDate = resolutionDate; }
配置编译参数,保留参数名称
Spring Data需要通过参数名称匹配字段,所以编译时要加上-parameters参数:
Maven配置
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <compilerArgs> <arg>-parameters</arg> </compilerArgs> </configuration> </plugin> </plugins> </build>
Gradle配置
tasks.withType(JavaCompile) { options.compilerArgs.add('-parameters') }
这样Spring Data MongoDB就能自动识别这个全参构造器,领域类也不用依赖持久化注解了。
内容的提问来源于stack exchange,提问作者Kaue

