How to properly map between persistence layer and domain object

折月煮酒 提交于 2019-12-23 16:44:40

问题


Let's say I have a domain java class representing a person:

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;
        }
    }

I store this domain class object in a database, regular class with the same field + getters and setters. Currently when I want to store object I create new PersonDocument (data is stored in mongo), use getters and setters and save it. It gets complicated when I want to fetch it from DB. I would like my domain object to expose only what is necessary, for the business logic currently it is only creation and validation. Simply:

Person p = Person.createNew("1234", "John");
p.validate();
repository.save(p); 

The other way it gets complicated, currently there is a builder which allows creation of object in any state. We do believe that data stored in DB has a proper state so it can be created that way but the downside is that there is a public API available, letting any one to do anything.

The initial idea was to use MapStruct java mapping library but it does use setters to create objects and exposing setters in the domain class (as far as I can tell) should be avoided.

Any suggestions how to do it properly?


回答1:


Your problem likely comes from two conflicting requirements:

  1. You want to expose only business methods.
  2. You want to expose data too, since you want to be able to implement serialization/deserialization external to the object.

One of those has to give. To be honest, most people faced with this problem ignore the first one, and just introduce setter/getters. The alternative is of course to ignore the second one, and just introduce the serialization/deserialization into the object.

For example you can introduce a method Document toDocument() into the objects that produces the Mongo compatible json document, and also a Person fromDocument(Document) to deserialize.

Most people don't like this sort of solution, because it "couples" the technology to the object. Is that a good or bad thing? Depends on your use-case. Which one do you want to optimize for: Changing business logic or changing technologies? If you're not planning to change technologies very often and don't plan using the same class in a completely different application, there's no reason to separate technology.




回答2:


Robert Bräutigam sentence is good:

Two conflicting requirements

But the is another sentence by alan Kay that is better:

“I’m sorry that I long ago coined the term “objects” for this topic because it gets many people to focus on the lesser idea. The big idea is messaging.” ~ Alan Kay

So, instead of dealing with the conflict, let's just change the approach to avoid it. The best way I found is to take a functional aproach and avoid unnecessary states and mutations in classes by expresing the domain changes as events.

Instead to map classes (aggregates, V.o.'s and/or entities) to persistence, I do this:

  1. Build an aggregate with the data needed (V.O.'s and entities) to apply aggregate rules and invariants given an action. This data comes from persistence. The aggregate do not expose getters not setters; just actions.
  2. Call the aggretate's action with command data as parameter. This will call inner entities actions in case the overal rules need it. This allows responsibility segregation and decoupling as the Aggregate Root does not have to know how are implemeted their inner entities (Tell, don't ask).
  3. Actions (in Aggregate roots and inner entities) does not modify its inner state; they instead returns events expressing the domain change. The aggregate main action coordinate and check the events returned by its inner entities to apply rules and invariants (the aggregate has the "big picture") and build the final Domain Event that is the output of the main Action call.
  4. Your persistence layer has an apply method for every Domain Event that has to handle (Persistence.Apply(event)). This way your persistence knows what was happened and; as long as the event has all the data needed to persist the change; can apply the change into (even with behaviour if needed!).
  5. Publish your Domain Event. Let the rest of your system knows that something has just happenend.

Check this post (it worth chek all DDD series in this blog) to see a similar implementation.




回答3:


I do it this way:

The person as a domain entity have status (in the sense of the entity fields that define the entity, not your "status" field) and behaviour (methods).

What is stored in the db is just the status. Then I create a "PersonStatus" interface in the domain (with getter methods of the fields that we need to persist), so that PersonRepository deals with the status.

The Person entity implements PersonStatus (or instead of this, you can put a static method that returns the state).

In the infraestructure I have a PersonDB class implementing PersonStatus too, which is the persistence model.

So:

DOMAIN MODEL:

// 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 );
}

INFRAESTRUCTURE:

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 );
}
}

APPLICATION LAYER:

...
Person person = new Person ( "1", "John Doe", ... );
personRepository.add ( person );
...
PersonStatus personStatus = personRepository.personOfId ( "1" );
Person person = new Person ( personStatus.id(), personStatus.name(), ... );
...



回答4:


It basically boils down to two things, depending on how much you are willing to add extra work in on the necessary infrastructure and how constraining your ORM/persistence is.

Use CQRS+ES pattern

The most obvious choice that's used in bigger and complex domains is to use the CQRS (Command/Query Responsibility Segregation) "Event Sourcing" pattern. This means, that each mutable actions generates an event, that is persisted.

When your aggregate is loaded, all the events will be loaded from the database and applied in chronological order. Once applied, your aggregate will have its current state.

CQRS just means, that you separate read and write operations. Write operations would happen in the aggregate by creating events (by applying commands) which are stored/read via Event Sourcing.

Where the "Query" would be queries on projected data, which uses the events to create a current state of the object, that's used for querying and reading only. Aggregates still read by reapplying all the events from the event sourcing storage.

Pros

  • You have an history of all changes that were done on the aggregate. This can be seen as added value to the business and auditing
  • if your projected database is corrupted or in an invalid state, you can restore it by replaying all the events and generate the projection from anew.
  • It's easy to revert to a previous state in time (i.e. by applying compensating events, that does opposite of what a previous event did)
  • Its easy to fix a bug (i.e. when calculating the the state of the aggregate) and then reply all the events to get the new, corrected value.

    Assume you have a BankingAccount aggregate and calculate the balance and you used regular rounding instead of "round-to-even". Here you can fix the calculation, then reapply all the events and you get the new and correct account balance.

Cons

  • Aggregates with 1000s of events can take some time to materialize (Snapshots/Mememto pattern can be used here to load a snapshot and apply the events after that snapshot)
  • Initially more time to implement the necessary infrastructure
  • You can't query event sourced aggregates w/o a read store; Requires projection and a message queue to publish the event sourcing events so they can be processed and applied to a projection (SQL or document table) which can be used for queries

Map directly to Domain Entities

Some ORM and Document database providers allow you to directly map to backing fields, i.e. via reflection.

In MongoDb C# Driver it can be done via something like in the linked answer.

Same applies to EF Core ORM. I'm sure theres something similar in the Java world too.

This may limit your database persistence library and technology usage, since it will require you to use one which supports such APIs via fluent or code configuration. You can't use attributes/annotations for this, because these are usually database specific and it would leak persistence knowledge into your domain.

It also MAY limit your ability to use the strong typed querying API (Linq in C#, Streams in Java), because that generally requires getters and setters, so you may have to use magic strings (with names of the fields or properties in the storage) in the persistence layer.

It may be acceptable for smaller/less complex domains. But CQRS+ES should always be preferred, if possible and within budget/timeline since its most flexible and works with all persistence storage and frameworks (even with key-value stores).

Pros

  • Not necessary to leverage more complex infrastructure (CQRS, ES, Pub/Sub messaging/queues)
  • No leaking of persistence knowledge into your models and no need to break encapsulation

Cons

  • No history of changes
  • No way to restore a previous state
  • May require magic strings when querying in the persistence layer (depends on framework/orm)
  • Can require a lot of fluent/code configuration in the persistence layer, to map it to the backing field
  • May break, when you rename the backing field


来源:https://stackoverflow.com/questions/53260217/how-to-properly-map-between-persistence-layer-and-domain-object

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!