Giter Site home page Giter Site logo

Support “Unit of Work” pattern about prisma HOT 13 OPEN

prisma avatar prisma commented on May 5, 2024 28
Support “Unit of Work” pattern

from prisma.

Comments (13)

cherifGsoul avatar cherifGsoul commented on May 5, 2024 15

Hello team, it would be great if Prisma will have this feature, here is a use case:

  • Given I have a quiz question entity
  • And this question entity have a list of answer options
  • When I add or remove some answer options from the question
  • And create or update the question
  • The question answer options should be updated

Right now developers have to track the changes by themselves like in the screenshot below (it is a JS code):

Screen Shot 2021-11-10 at 12 55 42

Update for my case

============================================================================
I updated the code to look like the screenshot below, I hope it can help:
Screen Shot 2023-03-06 at 14 04 12

from prisma.

cherifGsoul avatar cherifGsoul commented on May 5, 2024 7

@tareksalem Simplicity for who? Prisma users or Prisma developers that maintain the ORM?
The feature will add a lot of simplicity to the end user, however it could be more work for the ORM developers.

from prisma.

mbrookson avatar mbrookson commented on May 5, 2024 4

So, after a few hours of playing with it, it is definitely possible to achieve the unit of work pattern. I'm using NestJS to implement it, taking some inspiration for how I've previously worked with .NET and Entity Framework.

Planning on writing a blog post about it and also publishing an example repo once I tidy some things up. Will report back here with more details when I have them. 🙂

In terms of the DDD side of it, I think it's important to decouple the concepts of domain models and database persistence. With tools like Entity Framework these lines can become blurred due to EF's powerful modelling and change tracking. Prisma doesn't work like this but it's still possible to create rich domain models (aggregate, entities, value objects) and map them to and from Prisma models in repositories. (I have written a short ebook on DDD if you're interested).

from prisma.

mbrookson avatar mbrookson commented on May 5, 2024 2

Sorry, been rather busy and forgot about this. I never did get round to writing a blog post, but I started to throw together an example repo which contains a unit of work pattern with NestJS. It's far from finished, the tests don't work, but if you spin up a postgres db you can get it working locally and call the POST /users endpoint to see it in action. There are however a few considerations. I ended up trialling a pattern similar to this in my team at work but we ended up removing it and used a different (less ideal) approach. I'll explain why.

Here's the repo: https://github.com/mbrookson/nestjs-ddd

It contains:

This repo also implements a transactional outbox pattern, which can be used to store domain events in a transaction and then consume them regularly and run their relevant handlers in separate units of work.

The downside to this approach and the reason we decided not to do this was that we ran into a few issues.

  • The unit of work pattern is more familiar in platforms like .NET, where Entity Framework implements change tracking and transactions are created only at the point of saving data. As touched upon in the comment above, TS as a language doesn't really offer this ability as easily, and the tools and libraries in the ecosystem are certainly not as mature as anything in .NET for these kind of patterns.
  • Related to the above, the issue with this implementation is that the Prisma transaction is actually opened in the database at the point where the unit of work begins. This means the transaction is open for the duration of the operation. This is not good! It means holding onto transactions much longer than necessary, and in a high traffic system this could cause deadlocks and connection exhaustion. Entity Framework doesn't have the same issue because it tracks changes to entity object and only opens and commits the transaction at the point where SaveChanges is called. We ran into these long-lasting transaction issues at work and hence decided to not use this approach.

As a final note, at work we decided that we'd essentially consider each command handler as a unit of work. This means a command will read data and create an aggregate instance, do some domain logic, then perform any required changes in a Prisma transaction that we manually create within that command handler. It removed a lot of complexity that this example DDD repo introduces, stopped us having long-lasting transactions and resolve all issues we were having due to this.

It was a nice idea but sometimes keeping things simple and trying to embrace the tools you're using rather than forcing in concepts from previous experience can actually be a better path for success.

Hope this helps anyone who finds this. And I'm open to developing this more and working with anyone who wants to improve or expand on it.

from prisma.

stevieTheCoder avatar stevieTheCoder commented on May 5, 2024 1

We would also benefit from this at work.

from prisma.

sliemeobn avatar sliemeobn commented on May 5, 2024 1

I would love to see better support for DDD/"unit of work"-style applications as well! However, I would like offer a slightly different angle to this topic.

Unless these is some other way I haven't found yet, it seems really hard to properly execute domain logic on an "aggregate"(DDD term for a bunch of data that is always worked on together for internal consistency) with prisma.

to explain where I am coming from a bit better, what I usually want when I try to build clean stuff:

  • fetch a root model with a few defined relations included (ie: the aggregate)
  • pass the whole thing (maybe mapped somehow) to "the logic" (ideally totally separate from storage concerns and synchronous)
  • take the resulting data and - and here comes the missing part - "apply" it to prisma

I generally see two options for this, either

  • track all the changes (new stuff, changed fields, changes to relations, deleted stuff) - ie: traditional unit of work
  • or, keep a copy of the fetched aggregate and "diff" the one you want to save, therefore working out all necessary changes that need to be made to the database

I understand that is not entirely the same, but to me some sort of "compare to original and apply changes" feature feels a lot more natural to add/build on top of prisma than an entire change-tracking solution.

from prisma.

tareksalem avatar tareksalem commented on May 5, 2024 1

Back to this issue, the unit of work pattern could be implemented outside prisma easily, you could have multiple repositories wrap prisma operations like insert, delete, or update and you can build a wrapper class that puts those operations in a Prisma transaction, basically the unit of work is more than database integration because you can write those changed objects to a file, database or even on a network, so the unit of work should be implemented by yourself or a library/framework designed for domain driven design, from prisma perspective the unit of work pattern it has is the transaction mechanism

from prisma.

tareksalem avatar tareksalem commented on May 5, 2024

This would be a nice feature however I think Prisma is far away from this, UOW is usually used in DDD and provided in big persistence frameworks in JAVA and C# such as Hibernate, JPA, and Entity Framework.

Prisma is meant for simplicity, and it obviously takes another way of doing things

from prisma.

syllomex avatar syllomex commented on May 5, 2024

I was also looking for a solution to this and ended up developing something that I believe could be useful to someone.
Here is the example repository

from prisma.

cunninghamd avatar cunninghamd commented on May 5, 2024

So, after a few hours of playing with it, it is definitely possible to achieve the unit of work pattern. I'm using NestJS to implement it, taking some inspiration for how I've previously worked with .NET and Entity Framework.

Planning on writing a blog post about it and also publishing an example repo once I tidy some things up. Will report back here with more details when I have them. 🙂

@mbrookson Any update on that blog post?

Even an example repository to see how your solution compares to @syllomex would be nice.

from prisma.

vimmerru avatar vimmerru commented on May 5, 2024

I think TS asks significantly more complex thing than just coordination of transactions between repositories about repeating of Entity Framework (or ex. mikro-orm) experience when you have mix of Identity Map, UnitOfWork and managed entities tracking changes when you can get efficient implicit transactions on calling em.flush().

I suggest check https://mikro-orm.io/docs/unit-of-work

from prisma.

cunninghamd avatar cunninghamd commented on May 5, 2024

@mbrookson Wow, thanks for the excellent write-up! You could either add this write-up as a README, or make it into the blog post itself! :)

I really appreciate the background, and admission that you're not using it due to the extra overhead and other issues.

In the name of expediency, I ended up with more of a Repository Factory pattern. It doesn't provide the UnitOfWork benefits (namely, the ability to .save() on multiple entities at once, like EntityFramework), but it does obfuscate my repositories further away from my business logic, and provides the ability to inject a single factory in order to get access to all my repositories.

It feels like ok design, mixed with -- as you noted -- keeping things simple.

Truncated example, for reference:

@Injectable()
export class RepositoryFactory {
  private prismaService: PrismaService = new PrismaService();

  userRepository: UserRepository = new UserRepository(this.prismaService);
  otherRepository: OtherRepository = new OtherRepository(this.prismaService);
}

usage:

export class UserService {
  private userRepository: UserRepository;

  constructor(private readonly repositoryFactory: RepositoryFactory) {
    this.userRepository = this.repositoryFactory.userRepository;
  }
}

It somewhat breaks SOLID principles by instantiating PrismaService, but the RepositoryFactory itself gets injected everywhere else, and this creates the opporunity for me to quickly ferret out code smells from other engineers, as PrismaService should only ever be used within the RepositoryFactory.

from prisma.

mbrookson avatar mbrookson commented on May 5, 2024

Seems okay to me 👍 Why instantiate the PrismaService yourself and not also inject it? I believe it's recommended to only create one instance of the Prisma client in the lifetime of an application.

We're using CQRS and command handlers, but this is comparable to your service pattern. With our implementation we decided to have repositories which can return a Prisma operation. This means if we have multiple entities that needs updating we can call into multiple repositories and use a Prisma transaction in our command handler.

class UserCreatedCommandHandler implements ICommandHandler<UserCreatedEvent> {
  constructor(
    private readonly prisma: PrismaProvider,
    private readonly userRepository: UserRepository,
    private readonly otherRepository: OtherRepository
  ) {}

  handle(event: UserCreatedEvent) {
    const user = User.create({
      // user properties....
    });

    const otherEntity = new Something();

    await this.prisma.$transaction([
      this.userRepository.create(user),
      this.otherRepository.doSomething()
    ]);
  }
}
class UserRepository {
  constructor(private readonly prisma: PrismaProvider) {}

  create(user: User) {
    return this.prisma.user.create({
      data: {
        // map aggregate properties to db create...
      }
    })
  }
}

Dumb example but hopefully demonstrates the point. Quite liking this solution as a half-way house. Still gives us a good separation of concerns, kind-of units of work and nice simple code to follow and maintain.

from prisma.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.