Comments (13)
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):
Update for my case
============================================================================
I updated the code to look like the screenshot below, I hope it can help:
from prisma.
@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.
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.
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:
- A
UnitOfWorkProvider
implementation which uses the NestJS CLS service package, which in turn uses the Node.js AsyncLocalStorage feature to store and retrieve data across an asynchronous call stack. It's a pretty amazing library! It creates an interactive Prisma transaction and stores it in CLS so it can be retrieved later on. https://github.com/mbrookson/nestjs-ddd/blob/main/src/infrastructure/unit-of-work.provider.ts - A
UseUnitOfWork
decorator which can be used on any function to indicate the entry point for a unit of work. It invokes theUnitOfWorkProvider
to create a new transaction. https://github.com/mbrookson/nestjs-ddd/blob/main/src/infrastructure/unit-of-work.decorator.ts - An example controller and example event handler which both use the unit of work decorator
- An example user repository which uses a
TransactionProvider
to retrieve an active transaction. https://github.com/mbrookson/nestjs-ddd/blob/main/src/infrastructure/user.repository.ts
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.
We would also benefit from this at work.
from prisma.
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.
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.
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.
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.
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.
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.
@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.
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)
- [BUG] [PRISMA STUDIO] [PRISMA GENERATE]
- Promise.all in interactive $transaction unexpectedly merges operations
- Bug when compiling prisma using custom output
- Crash in command generate. internal error: entered unreachable code
- Postgres `Oid` fails with `relationJoins`
- internal error: entered unreachable code in end-to-end playwright tests run over sqlite database HOT 1
- Server has closed the connection in long transactions with ?pgbouncer=true
- Send custom batch/bulk queries
- "Not Authorised" when directly applying Prisma generated migrations to Cloudflare D1 with `PRAGMA foreign_key_check;` HOT 2
- i hosted three bots for VK on vds with help cron tab, two bots failed, just one work now easy project hehe, i see this stacktrace first in my life lol HOT 4
- Can NOT run prisma for a month, Windows 11, VSCode
- Crash when insert data to SQLite `createMany` using Bun - thread 'tokio-runtime-worker' panicked
- Prisma Client should never create empty SQLite databases
- Improve Developer Experience when working with multiple providers.
- Support for SQL-level `DISTINCT` operation for more database (MySQL, SQLite, SQL Server)
- Postgres: Allow using `= ANY($1)` for filtering instead of `IN ($1, $2, ...)` for large arrays
- primsa generate HOT 1
- Prisma 5.12.1 client has errors accessing postgress db when using with cloudflare workers and the new { previewFeatures = ["driverAdapters"] } HOT 5
- For JSON columns in postres the prisma client returns a string instead of an object for that column when performing update operation
- I get this error while querying data: `JSON.stringify cannot serialize BigInt.` HOT 3
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from prisma.