Java 如何在持久层和域对象之间正确映射

Java 如何在持久层和域对象之间正确映射,java,domain-driven-design,Java,Domain Driven Design,假设我有一个表示个人的域java类: class Person { private final String id; // government id private String name; private String status; private Person(String id, String name) { this.id = id; this.name = name; this.status = "

假设我有一个表示个人的域java类:

class Person {

    private final String id; // government id
    private String name;
    private String status;

    private Person(String id, String name) {
        this.id = id;
        this.name = name;
        this.status = "NEW";
    }

    Person static createNew(String id, String name) {
        return new Person(id, name);
    }

    void validate() {
        //logic
        this.status = "VALID";
    }


    public static final class Builder {

        private String id;
        private String name;
        private String status;

        private Builder() {
        }

        public static Builder aPerson() {
            return new Builder();
        }

        public Builder id(String id) {
            this.id = id;
            return this;
        }

        public Builder name(String name) {
            this.name = name;
            return this;
        }

        public Builder status(String status) {
            this.status = status;
            return this;
        }

        public Person build() {
            Person person = new Person(id, name);
            person.status = this.status;
            return person;
        }
    }
我将这个域类对象存储在一个数据库中,它是一个具有相同字段+getter和setter的常规类。当前,当我想存储对象时,我创建了新的
PersonDocument
(数据存储在mongo中),使用getter和setter并保存它。当我想从DB获取它时,它变得复杂了。我希望我的域对象只公开必要的内容,因为当前的业务逻辑只是创建和验证。简单地说:

Person p = Person.createNew("1234", "John");
p.validate();
repository.save(p); 
另一方面,它变得复杂,目前有一个生成器,允许在任何状态下创建对象。我们确实相信数据库中存储的数据有一个适当的状态,因此可以通过这种方式创建数据,但缺点是有一个可用的公共API,允许任何人做任何事情


最初的想法是使用MapStruct java映射库,但它确实使用setter来创建对象,应该避免在域类中公开setter(据我所知)


有什么建议如何正确地做吗

您的问题可能来自两个相互冲突的需求:

  • 您只想公开业务方法
  • 您也希望公开数据,因为您希望能够在对象外部实现序列化/反序列化
  • 其中一个必须给予。老实说,大多数面临这个问题的人都忽略了第一个问题,只引入了setter/getter。当然,另一种方法是忽略第二种方法,只在对象中引入序列化/反序列化

    例如,您可以将方法
    documenttodocument()
    引入到生成与Mongo兼容的json文档的对象中,还可以将
    Person fromDocument(Document)
    引入反序列化

    大多数人不喜欢这种解决方案,因为它将技术与对象“耦合”。这是好事还是坏事?取决于您的用例。您希望优化哪一个:更改业务逻辑还是更改技术?如果您不打算经常更改技术,也不打算在完全不同的应用程序中使用相同的类,那么就没有理由将技术分开。

    我这样做:

    作为域实体的人员具有状态(定义实体的实体字段,而不是您的“状态”字段)和行为(方法)

    数据库中存储的只是状态。然后我在域中创建一个“PersonStatus”接口(使用我们需要持久化的字段的getter方法),以便PersonRepository处理状态

    Person实体实现PersonStatus(或者,您可以放置一个返回状态的静态方法来代替它)

    在基础结构中,我也有一个PersonDB类实现PersonStatus,这是持久性模型

    因此:

    域模型:

    // ENTITY
    public class Person implements PersonStatus {
    
    // Fields that define status
    private String id;
    private String name;
    ...
    
    // Constructors and behaviour
    ...
    ...
    
    // Methods implementing PersonStatus
    @Override
    public String id() {
        return this.id;
    }
    @Override
    public String name() {
        return this.name;
    }
    ...
    }
    
    
    // STATUS OF ENTITY
    public interface PersonStatus {
        public String id(); 
        public String name();   
        ...
    }
    
    
    // REPOSITORY
    public interface PersonRepository {
        public void add ( PersonStatus personStatus );
        public PersonStatus personOfId ( String anId );
    }
    
    基础设施:

    public class PersonDB implements PersonStatus {
    
    private String id;
    private String name;
    ...
    
    public PersonDB ( String anId, String aName, ... ) {
        this.id = anId;
        this.name = aName;
        ...
    }
    
    @Override
    public String id() {
        return this.id;
    }
    
    @Override
    public String name() {
        return this.name;
    }
    ...
    }
    
    
    // AN INMEMORY REPOSITORY IMPLEMENTATION
    public class InmemoryPersonRepository implements PersonRepository {
    
        private Map<String,PersonDB> inmemorydb;
    
        public InmemoryPersonRepository() {
            this.inmemoryDb = new HashMap<String,PersonDB>();
        }
    
        @Override
        public void add ( PersonStatus personStatus );
            PersonDB personDB = new PersonDB ( personStatus.id(), personStatus.name(), ... );
            this.inmemoryDb.put ( personDB.id(), personDB );
        }
    
        @Override
        public PersonStatus personOfId ( String anId ) {
            return this.inmemoryDb.personOfId ( anId );
    }
    }
    

    Robert Bräutigam的句子很好:

    两个相互冲突的要求

    但艾伦·凯的另一句话更好:

    很抱歉,我很久以前就为这个话题创造了“对象”这个词 因为它让很多人把注意力集中在较小的想法上,而大的想法上 “这就是信息。”~Alan Kay

    因此,与其处理冲突,不如改变方法来避免冲突。我发现的最好的方法是采用函数方法,通过将域更改表示为事件来避免类中不必要的状态和突变

    为了将类(聚合、V.o.和/或实体)映射到持久性,我执行以下操作:

  • 使用所需的数据(V.O.和实体)构建聚合,以应用给定操作的聚合规则和不变量。这些数据来自持久性。聚合不公开getter而不是setter;只是行动
  • 以命令数据作为参数调用aggretate的操作。如果总体规则需要,这将调用内部实体操作。这允许职责分离和解耦,因为聚合根不必知道如何实现其内部实体()
  • 动作(在聚合根和内部实体中)不会修改其内部状态;相反,它们返回表示域更改的事件。聚合主操作协调并检查其内部实体返回的事件,以应用规则和不变量(聚合具有“大图”),并构建作为主操作调用输出的最终域事件
  • 对于必须处理的每个域事件(
    persistence.apply(Event)
    ),持久层都有一个apply方法。这样,你的坚持就知道发生了什么,也知道发生了什么;只要事件具有保持更改所需的所有数据;可以将更改应用到(如果需要,甚至可以使用行为!)
  • 发布您的域事件。让系统的其他部分知道刚刚发生了什么

  • 查看(值得在本博客中查看所有DDD系列)以查看类似的实现。

    基本上可以归结为两件事,这取决于您愿意在必要的基础设施上添加多少额外工作以及对ORM/持久性的约束程度

    使用CQRS+ES模式 在更大、更复杂的领域中使用的最明显的选择是使用CQRS(命令/查询责任分离)“事件源”模式。这意味着,每个可变操作都会生成一个持久化的事件

    加载聚合后,所有事件都将从数据库中加载,并按时间顺序应用。一旦应用,聚合将具有其当前状态

    CQR只是意味着,您可以将读操作和写操作分开。写入操作将通过创建事件(通过应用命令)在聚合中发生,这些事件通过事件源存储/读取

    其中“查询”是对投影数据的查询,它使用事件创建对象的当前状态,用于查询和rea
    ...
    Person person = new Person ( "1", "John Doe", ... );
    personRepository.add ( person );
    ...
    PersonStatus personStatus = personRepository.personOfId ( "1" );
    Person person = new Person ( personStatus.id(), personStatus.name(), ... );
    ...