-
Notifications
You must be signed in to change notification settings - Fork 11
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
The repository pattern #64
Comments
Hey. My solution to the problem was a complete language and DBMS switch. It probably doesn't fit here, but if you want to hear anyway, just ask. |
You mean regarding this entry about Doctrine ORM? @akomm Did you switch to a document database like ArangoDB? |
Yeah, typescript and arangodb. Aggregate Root, Value Objects, persistence & model separation all basically out of the box. Document = Aggregate Root and its basically transaction boundary, since you can't partially save document, you save it as a whole and many more things... |
// type Document not to confuse with general documents in document database. "Document" is purely an example type with some owned type "Page", could be anything
type DocumentId = string & {_: "DocumentId"}
type Document = {
id: DocumentId
name: string
pages: Page[]
}
type Page = {
/* ... */
}
addPage(doc: Document, /* page input */): Document
rename(doc: Document, newName: string): Document
function someAppLayerOpOnDocument(doc: Document, notifications: NotificationService /* input */ ) {
const modified = pipe(
doc,
addPage(/* page input */),
rename(/* rename input */)
)
documents.save(doc) // we save whole doc, basically transaction on the aggregate root boundary, which the doc is...
notifications.push({type: "some operation on document", /* ... */})
} |
The initial discussion can be found on twitter:
https://twitter.com/webdevilopers/status/1519600542544281602
My tweet was based on the following thread regarding the "repository pattern" opened by @kainiklas :
https://twitter.com/kniklas/status/1519270161429286914
The main thought behind this quote focused on the active record pattern. It allows an object to "save itself" and have awareness of other objects of the same class.
I first came in contact with active record many years ago when I started with Ruby on Rails. Those days I found the approach very convenient. Everything in one object, a graph with all related objects and even custom SQL.
After some years I discovered Doctrine ORM which introduced the repository pattern. Our object had annotations that showed the structure of the database and still you got the object graph with all relations. But the custom SQL was gone. Instead you got a Query Builder in your repository to add conditions for instance.
Projects grow - objects graphs grow. The N+1 issue occurs - more database memory and lazy loading is introduced. But in order to handle business logic you still need the data for instance for calculations.
Instead of using the related objects inside the object graph we moved calculations to repository methods with custom SQL.
And again things seemed to work fine.
But as more and more features were required it was harder to maintain the business logic of the main objects (Entites).
Those days I discovered Domain-Driven Design. And it solved a lot of problems!
Instead of an object graph holding all relations via lazy loading we created separated Entities and used IDs only for references.
The Entities were completely decoupled from the database. Event the inside class mapping went to an XML file.
The focus was now on the business logic of the Entity which was now an POPO - a "plain old PHP object".
A database is just an implementation detail inside the infrastructure layer. Thanks to - among other things - repositories.
This would not have been possible with the active record pattern.
But at that point of the lifetime of a project it was required! - if you are not at that point, you maybe don't NEED it.
A different point was reusability. As @kainiklas states:
Some people like to reuse code e.g. short snippets for SQL conditions - independent of raw SQL or query builder parts.
In Laravel I think these extra classes are called "Scopes". I guess they work similar to traits.
Personally I'm a not a fan of DRY if business logic e.g. "WHERE customer_status = foo" is used by multiple contexts. If one changes this condition it actually changes multiple usages. At least without tests I would not recommend this.
Anyways...
There is a pattern know as the "Specification Pattern" that tries to solve this. The SQL or query builder part is in a centralized class. But the business logic is made explicit by naming the class for instance "CustomerStatusSpecification".
A final thought on repositories:
A repository can extend a base class of the ORM e.g. EntityRepository in Doctrine ORM. By extending you gain default methods e.g. "findBy", "findAll" etc.. Personally I prefer to only implement methods that I really need. Instead of opening access to multiple ORM specific methods that could be mis-used by another developer or which could one disappear because you switch the database or ORM.
At least I would always write an Interface that ensures the methods required. Instead of re-using the "findby" method with magic parameters I would prefer "findById", "findByTenantId" etc..
Possibly related:
Maybe @akomm, @deeky666, @ScreamingDev and @J7mbo would like to add their thoughts.
The text was updated successfully, but these errors were encountered: