Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to persist changes back to the storage layer #52

Closed
djmattyg007 opened this issue Apr 22, 2017 · 8 comments
Closed

How to persist changes back to the storage layer #52

djmattyg007 opened this issue Apr 22, 2017 · 8 comments

Comments

@djmattyg007
Copy link

I'm not really sure if this is the right place to ask this, as it isn't exactly an issue with the package per se, but it is something that I've been wondering since coming across this a few days ago.

When mapping your persistence model to your domain model (following the example set out here: https://github.com/atlasphp/Atlas.Orm/blob/1.x/docs/domain.md#map-from-persistence-to-domain), how do you go about persisting changes to the domain model back to the storage layer?

For example, assume there's a class in your domain model that accepts many properties through its constructor, and has some methods that mutate several properties of the object at once. This class doesn't expose all of its properties through getter methods, to avoid it being abused somewhere else in the application.

Now that I've finished mutating an instance of this class, I pass it back to the repository to be persisted. How does the repository get the data out of the object? Should it use reflection? Should the domain model class provide getter methods that aren't part of an interface, and trust that they won't be abused?

@pmjones
Copy link
Contributor

pmjones commented Apr 25, 2017

how do you go about persisting changes to the domain model back to the storage layer
...
This class doesn't expose all of its properties through getter methods, to avoid it being abused somewhere else in the application.

My general presumption is that getter methods, not being able to change the state of the domain object, is abuse-resistant. The furthest extent I can imagine is that someone might query the state of the object and then use that to decide on a course of action (the opposite of "tell don't ask"). Personally, that's not been a problem I've experienced; being able to "get" the various entities that make up an aggregate, for example, has not seemed at odds with DDD -- but then I don't claim to be a DDD expert so I might have been breaking some rules.

Perhaps providing code examples might help me (and others) understand the problem better ... ?

@Federkun
Copy link

I was wondering the same thing @djmattyg007. I came out with this:

// PM
class RecordDiscussion
{
    public $id;
    public $authorName;
    public $close;
}

// DM
class Discussion
{
    /** @var DiscussionId */
    private $id;
    /** @var Author */
    private $author;
    /** @var bool */
    private $isClosed = false;
    
    // Make sure that you can create new Discussions only using named constructors
    private function __construct() {}
    
    public static function createFrom(DiscussionId $id, Author $author): self
    {
        $discussion = new self();
        $this->id = $id;
        $this->author = $author;
        
        return $discussion;
    }

    public function close(): void
    {
        if ($this->isClosed) {
            throw new Exception('This discussion is already closed..');
        }
        
        $this->isClosed = true;
    }
    
    public function open(): void
    {
        if (!$this->isClosed) {
            throw new Exception('This discussion is already open..');
        }
        
        $this->isClosed = false;
    }
    
    // No getters here. I'm moving towards cqrs.
    
    protected static function reconstituteFromDiscussionRecord(RecordDiscussion $record): self
    {
        $discussion = new self();
        $discussion->id = DiscussionId::fromString($record->id);
        $discussion->author = Author::fromName($record->authorName);
        $discussion->isClosed = (bool) $record->close;
        
        return $discussion;
    }
}

The question is: "how can we map the DM and PM?" What do you suggest? Something like this?

class DiscussionTranslator // I can't think of a better name right now
{
    public function fromPersistenceModelToDomainModel(RecordDiscussion $record): Discussion
    {
        $class = new ReflectionClass(Discussion::class);
        $method = $class->getMethod('reconstituteFromDiscussionRecord');
        $method->setAccessible(true);
        
        return $method->invokeArgs(null, [$record]);
    }
    
    public function fromDomainModelToPersistenceModel(Discussion $discussion): RecordDiscussion
    {
        $record = new RecordDiscussion();
        $record->id = $this->getProperty($discussion, 'id')->toString();
        $record->authorName = $this->getProperty($discussion, 'author')->toString();
        $record->close = $this->getProperty($discussion, 'isClosed');

        return $record;
    }
    
    // ugly hack
    private function getProperty($obj, $prop)
    {
        $reflection = new ReflectionClass($obj);
        $property = $reflection->getProperty($prop);
        $property->setAccessible(true);
        return $property->getValue($obj);
    }
}

If I don't want apply event sourcing, I'm constrained with this?

Finally:

class DiscussionRepository
{
    private $mapper;
    private $translator;
    
    public function __construct(DiscussionMapper $mapper, DiscussionTranslator $translator)
    {
        $this->mapper = $mapper;
        $this->translator = $translator;
    }

    public function save(Discussion $discussion)
    {
        $record = $this->translator->fromDomainModelToPersistenceModel($discussion);

        // $this->mapper->update? $this->mapper->insert?
    }
    
    public function fetch(DiscussionId $id)
    {
        $record = $this->mapper->select(['id' => $id->toString()])
            ->with(['authorName', 'close']);
     
        return $this->translator->fromPersistenceModelToDomainModel($record);
    }
}

Full example: https://3v4l.org/oL5OO

@Federkun
Copy link

Federkun commented Apr 30, 2017

This is how I have chosen to do it: https://gist.github.com/Federkun/a8e007fc7d9445f100981fb5ccc57407
It's not without of problems, but it's good enough.

@kevinsmith
Copy link

I'm running into this same issue myself. Not an Atlas-specific thing per se, just not often seen in many of the other popular ORMs because so many of them couple the persistence layer to the domain layer.

If we've followed the guidance to fully separate persistence from domain (https://github.com/atlasphp/Atlas.Orm/blob/1.x/docs/domain.md#map-from-persistence-to-domain), how do we then take the data in our POPO and get Atlas to update the appropriate row in the DB?

The only solution I've been able to come up with is essentially keeping a cache of previously retrieved Atlas Record instances in an array on a class property of your repository. Then when you inject the POPO (the Thread instance in the example) into your repository's save() method, the repository matches it up with the cached Atlas Record instance (by primary key), updates the values on the Record accordingly, and passes the Record object into $atlas->update.

It feels like there's got to be a better way and I just can't see it.

@pmjones
Copy link
Contributor

pmjones commented Dec 22, 2017

@djmattyg007 @Federkun @kevinsmith --

I suppose this is as good a time as any to reveal Transit, a persistence-to-domain system for Atlas that I've been developing off-and-on for several weeks now:

https://github.com/atlasphp/Atlas.Transit

It is experimental, unfinished, and unsuitable for production. I can guarantee breaking changes, thus the 0.x version. Of note, it lacks Value Object support.

Even though it is not done, it may give you ideas of your own. And of course if you have PRs, send them along.

@djmattyg007
Copy link
Author

Christmas has come early :)

@pmjones
Copy link
Contributor

pmjones commented Feb 2, 2018

With Atlas.Transit as at least an example of how to do persist changes back to the storage layer, I am closing this issue. Please comment here if you find reason to re-open it. Thanks all!

@pmjones pmjones closed this as completed Feb 2, 2018
@kevinsmith
Copy link

The only solution I've been able to come up with is essentially keeping a cache of previously retrieved Atlas Record instances in an array on a class property of your repository. Then when you inject the POPO (the Thread instance in the example) into your repository's save() method, the repository matches it up with the cached Atlas Record instance (by primary key), updates the values on the Record accordingly, and passes the Record object into $atlas->update.

Just coming back to this, and after walking through the code with xdebug, it appears that there was no need for me to do this as Atlas already does this through IdentityMap. Just fetch the record through Atlas as normal and if it was retrieved in the same request, it won't hit the DB again.

Is that right, @pmjones?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants