Cross Module Transaction with Prisma



TL;DR



Prisma and Interactive Transaction

There isn’t any doubt that Prisma boosts your productiveness when coping with Databases in Node.js + TypeScript. However as you begin creating advanced software program, there are some circumstances you possibly can’t use Prisma the best way you’d prefer to out of the field. One in every of them is while you wish to use the interactive transaction throughout modules.

What I imply by cross module is a bit obscure. Let’s take a look at how one can write interactive transactions in Prisma. The next code is from the official docs.

await prisma.$transaction(async (prisma) => {
  // 1. Decrement quantity from the sender.
  const sender = await prisma.account.replace({
    information: {
      stability: {
        decrement: quantity,
      },
    },
    the place: {
      electronic mail: from,
    },
  })
  // 2. Confirm that the sender's stability did not go beneath zero.
  if (sender.stability < 0) {
    throw new Error(`${from} would not have sufficient to ship ${quantity}`)
  }
  // 3. Increment the recipient's stability by quantity
  const recipient = prisma.account.replace({
    information: {
      stability: {
        increment: quantity,
      },
    },
    the place: {
      electronic mail: to,
    },
  })
  return recipient
})
Enter fullscreen mode

Exit fullscreen mode

The purpose is that you simply name prisma.$transaction and also you cross a callback to it with the parameter prisma. Contained in the transaction, you utilize the prisma occasion handed because the callback to make use of it because the transaction prisma consumer. It is easy and simple to make use of. However what when you do not wish to present the prisma interface contained in the transaction code? Maybe you are working with a enterprise-ish app and have a layered structure and you aren’t allowed to make use of the prisma consumer in say, the applying layer.

It is most likely simpler to have a look at it in code. Suppose you wish to write some transaction code like this:

await $transaction(async () => {
  // name a number of repository strategies contained in the transaction
  // if both fails, the transaction will rollback
  await this.orderRepo.create(order);
  await this.notificationRepo.ship(
    `Efficiently created order: ${order.id}`
  );
});
Enter fullscreen mode

Exit fullscreen mode

There are a number of Repositories that disguise the implementation particulars(e.g. Prisma, SNS, and so forth.). You wouldn’t wish to present prisma inside this code as a result of it’s an implementation element. So how will you take care of this utilizing Prisma? It is truly not that simple since you’ll by some means must cross the Transaction Prisma Consumer to the Repository throughout modules with out explicitly passing it.



Making a customized TransactionScope

That is after I got here throughout this issue comment. It says you should use cls-hooked to create a thread-like native storage to briefly retailer the Transaction Prisma Consumer, after which get the consumer from someplace else through CLS (Continuation-Native Storage) afterwards.

After taking a look at how I can use cls-hooked, here’s a TransactionScope class I’ve created to create a transaction which can be utilized from any layer:

export class PrismaTransactionScope implements TransactionScope {
  personal readonly prisma: PrismaClient;
  personal readonly transactionContext: cls.Namespace;

  constructor(prisma: PrismaClient, transactionContext: cls.Namespace) {
    // inject the unique Prisma Consumer to make use of while you truly create a transaction
    this.prisma = prisma;
    // A CLS namespace to briefly save the Transaction Prisma Consumer
    this.transactionContext = transactionContext;
  }

  async run(fn: () => Promise<void>): Promise<void> {
    // try to get the Transaction Consumer
    const prisma = this.transactionContext.get(
      PRISMA_CLIENT_KEY
    ) as Prisma.TransactionClient;

    // if the Transaction Consumer
    if (prisma) {
      // exists, there isn't any must create a transaction and also you simply execute the callback
      await fn();
    } else {
      // doesn't exist, create a Prisma transaction 
      await this.prisma.$transaction(async (prisma) => {
        await this.transactionContext.runPromise(async () => {
          // and save the Transaction Consumer contained in the CLS namespace to be retrieved afterward
          this.transactionContext.set(PRISMA_CLIENT_KEY, prisma);

          attempt {
            // execute the transaction callback
            await fn();
          } catch (err) {
            // unset the transaction consumer when one thing goes unsuitable
            this.transactionContext.set(PRISMA_CLIENT_KEY, null);
            throw err;
          }
        });
      });
    }
  }
}
Enter fullscreen mode

Exit fullscreen mode

You may see that the Transaction Consumer is created inside this class and is saved contained in the CLS namespace. Therefore, the repositories who wish to use the Prisma Consumer can retrieve it from the CLS not directly.

Is that this it? Really, no. There’s yet one more level you must watch out when utilizing transactions in Prisma. It is that the prisma occasion contained in the transaction callback has differing types than the unique prisma occasion. You may see this within the kind definitions:

export kind TransactionClient = Omit<PrismaClient, '$join' | '$disconnect' | '$on' | '$transaction' | '$use'>
Enter fullscreen mode

Exit fullscreen mode

Bear in mind that the $transaction methodology is being Omitted. So, you possibly can see that at this second you can’t create nested transactions utilizing Prisma.

To take care of this, I’ve created a PrismaClientManager which returns a Transaction Prisma Consumer if it exists, and if not, returns the unique Prisma Consumer. Here is the implementation:

export class PrismaClientManager {
  personal prisma: PrismaClient;
  personal transactionContext: cls.Namespace;

  constructor(prisma: PrismaClient, transactionContext: cls.Namespace) {
    this.prisma = prisma;
    this.transactionContext = transactionContext;
  }

  getClient(): Prisma.TransactionClient {
    const prisma = this.transactionContext.get(
      PRISMA_CLIENT_KEY
    ) as Prisma.TransactionClient;
    if (prisma) {
      return prisma;
    } else {
      return this.prisma;
    }
  }
}
Enter fullscreen mode

Exit fullscreen mode

It is easy, however discover that the return kind is Prisma.TransactionClient. Because of this the Prisma Consumer returned from this PrismaClientManager at all times returns the Prisma.TransactionClient kind. Due to this fact, this consumer can’t create a transaction.

That is the constraint I made to be able to obtain this cross module transaction utilizing Prisma. In different phrases, you can’t name prisma.$transaction from inside repositories. As an alternative, you at all times use the TransactionScope class I discussed above.

It’ll create transactions if wanted, and will not if it is not crucial. So, from repositories, you possibly can write code like this:

export class PrismaOrderRepository implements OrderRepository {
  personal readonly clientManager: PrismaClientManager;
  personal readonly transactionScope: TransactionScope;

  constructor(
    clientManager: PrismaClientManager,
    transactionScope: TransactionScope
  ) {
    this.clientManager = clientManager;
    this.transactionScope = transactionScope;
  }

  async create(order: Order): Promise<void> {
    // you need not care when you're inside a transaction or not
    // simply use the TransactionScope
    await this.transactionScope.run(async () => {
      const prisma = this.clientManager.getClient();
      const newOrder = await prisma.order.create({
        information: {
          id: order.id,
        },
      });

      for (const productId of order.productIds) {
        await prisma.orderProduct.create({
          information: {
            id: uuid(),
            orderId: newOrder.id,
            productId,
          },
        });
      }
    });
  }
}
Enter fullscreen mode

Exit fullscreen mode

If the repository is used inside a transaction, no transaction can be created once more (because of the PrismaClientManager). If the repository is used exterior a transaction, a transaction can be created and consistency can be saved between the Order and OrderProduct information.

Lastly, with the facility of the TransactionScope class, you possibly can create a transaction from the applying layer as follows:

export class CreateOrder {
  personal readonly orderRepo: OrderRepository;
  personal readonly notificationRepo: NotificationRepository;
  personal readonly transactionScope: TransactionScope;
  constructor(
    orderRepo: OrderRepository,
    notificationRepo: NotificationRepository,
    transactionScope: TransactionScope
  ) {
    this.orderRepo = orderRepo;
    this.notificationRepo = notificationRepo;
    this.transactionScope = transactionScope;
  }

  async execute({ productIds }: CreateOrderInput) {
    const order = Order.create(productIds);

    // create a transaction scope contained in the Utility layer
    await this.transactionScope.run(async () => {
      // name a number of repository strategies contained in the transaction
      // if both fails, the transaction will rollback
      await this.orderRepo.create(order);
      await this.notificationRepo.ship(
        `Efficiently created order: ${order.id}`
      );
    });
  }
}
Enter fullscreen mode

Exit fullscreen mode

Discover that the OrderRepository and NotificationRepository are inside the identical transaction and due to this fact, if the Notification fails, you possibly can rollback the information which was saved from the OrderRepository (go away the structure choice for now 😂. you get the purpose.). Due to this fact, you do not have to combine the database obligations with the notification obligations.



Wrap up

I’ve proven how one can create a TransactionScope utilizing Prisma in Node.js. It isn’t supreme, however appears to be like prefer it’s working as anticipated. I’ve seen folks struggling about this structure and hope this publish is available in some form of assist.

Feedbacks are extraordinarily welcome!

It is a PoC to see if cross module transaction is feasible with Prisma.

Regardless of Prisma with the ability to use interactive transaction, it forces you to make use of a newly created Prisma.TransactionClient as follows:

// copied from official docs https://www.prisma.io/docs/ideas/elements/prisma-client/transactions#batchbulk-operations
await prisma.$transaction(async (prisma) => {
  // 1. Decrement quantity from the sender.
  const sender = await prisma.account.replace({
    information: {
      stability: {
        decrement: quantity,
      },
    },
    the place: {
      electronic mail: from,
    },
  });
  // 2. Confirm that the sender's stability did not go beneath zero.
  if (sender.stability < 0) {
    throw new Error(`${from} would not have sufficient to ship ${quantity}`);
  }
  // 3. Increment the recipient's stability by quantity
  const recipient =

Enter fullscreen mode

Exit fullscreen mode

Add a Comment

Your email address will not be published. Required fields are marked *