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

The repository pattern #64

Open
webdevilopers opened this issue May 5, 2022 · 4 comments
Open

The repository pattern #64

webdevilopers opened this issue May 5, 2022 · 4 comments

Comments

@webdevilopers
Copy link
Owner

webdevilopers commented May 5, 2022

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

I am a domain modeller. I solve business problems. Me and my model do not (want) to know what a database is. Maybe a different layer should take care of persistence. Not me!

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:

15 years ago I wrote SQL statements and put them into methods so that I could reuse them whenever I need and write the SQL only once. You would call that pattern repo nowadays.

With eloquent I don’t need that approach in most cases.

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.

@akomm
Copy link

akomm commented May 5, 2022

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.

@webdevilopers
Copy link
Owner Author

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

#13 (comment)

Did you switch to a document database like ArangoDB?

@akomm
Copy link

akomm commented May 5, 2022

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...

@akomm
Copy link

akomm commented Aug 5, 2022

// 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 = {
  /* ... */
}
  • Page is owned by Document, no extra table/collection needed
  • No headache about uni- or bi-directional or how to update both sides correctly without adding "noise" API that is not part of domain
  • No transaction needed, you can .save(document) as a whole, the document is the aggregate root
  • Simple persistence/domain separation: transformation just take Document in, Document out (immutable). Pure functions like:
addPage(doc: Document, /* page input */): Document
rename(doc: Document, newName: string): Document
  • Persistence, side-effects, ...? App layer:
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", /* ... */})
}

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

2 participants