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

Consider exposing lower level transaction primitives #193

Closed
craiglabenz opened this issue Apr 1, 2023 · 1 comment · Fixed by #194 or #195
Closed

Consider exposing lower level transaction primitives #193

craiglabenz opened this issue Apr 1, 2023 · 1 comment · Fixed by #194 or #195

Comments

@craiglabenz
Copy link

First of all, thanks again so much for creating this amazing library!

I'm running into an issue with the client.$transaction(...) API which makes it tricky to build an abstraction layer around the raw PrismaClient itself. Consider a method that attempts to create a new user after validation.

class DbAbstraction {
  DbAbstraction() : _client = PrismaClient(...);
  final PrismaClient _client;

  Future<User?> createUser({required String email, ...}) async {
    return _client.$transaction((transaction) async {
      final user = transaction.user.findFirst(email: ...);
      if (user != null) {
        // Cannot create user if email already exists
        return null;
      }
   
      // more checks?

      return transaction.user.create(...);
    });
  }
}

Now consider surrounding code which attempts to use this.

import 'package:dartz/dartz.dart';

class AuthService {
  AuthService(DbAbstraction db) : _db = db;
  final DbAbstraction _db;

  Future<Either<AuthError, User>> createUser({required String email, ....}) async {
    final User? user = await _db.createUser(email: email, ...);
    if (user == null) {
      // which check failed? what went wrong? we don't know!
    }
    return Right(user);
  }
}

--

The issue here stems from the fact that the PrismaClient.$transaction method accepts a callback which itself accepts another PrismaClient - meaning nothing one layer higher than the PrismaClient can be aware of transactions without also being aware of the implementation details of the layer below it - aka, the PrismaClient.

Alternatively, if the PrismaClient exposed startTransaction, commitTransaction, and rollbackTransaction, then all of the methods in DbAbstraction could be dead simple - one query each - and the abstraction layer above it (AuthService), could decide when to start a transaction, confirm a new user's email address is available, create them, and commit the transaction all on its own - without ever seeing implementation details of the PrismaClient.

It could look something like this:

import 'package:dartz/dartz.dart';

class DbAbstraction {
  DbAbstraction() : _regularClient = PrismaClient(...);
  final PrismaClient _regularClient;
  PrismaClient? _transactionClient;

  PrismaClient get _client => _transactionClient ?? _regularClient;

  Future<User?> createUser({required String email, ...}) async {
    return _client.user.create(...);
  }
  
  Future<User?> getUserByEmail({required String email, ...}) async {
    return _client.user.findFirst(...);
  }

  void startTransaction() {
    // Internally, this calls `_regularClient._engine.startTransaction();`
    _transactionClient = _regularClient.startTransaction();
  }

  void commitTransaction() {
    // Internally, this calls `_regularClient._engine.commitTransaction();`
    _transactionClient.commitTransaction();
    // whatever cleanup is appropriate
    _transactionClient.close();
    _transactionClient = null;
  }

  void rollbackTransaction() {
    // Internally, this calls `_regularClient._engine.rollbackTransaction();`
    _transactionClient.rollbackTransaction();
    // whatever cleanup is appropriate
    _transactionClient.close();
    _transactionClient = null;
  }
}


class AuthService {
  AuthService(DbAbstraction db) : _db = db;
  final DbAbstraction _db;

  Future<Either<AuthError, User>> createUser({required String email, ....}) async {
    _db.startTransaction();
    final User? existingUser = _db.users.findWhere(email: ...);
    if (exstingUser == null) {
      _db.rollbackTransaction();
      return Left(UnavailableEmailError());
    }

    final User user = await _db.createUser(email: email, ...);
    return Right(user);
  }
}
medz added a commit that referenced this issue Apr 5, 2023
@medz medz linked a pull request Apr 5, 2023 that will close this issue
@medz medz closed this as completed in #194 Apr 5, 2023
@medz
Copy link
Owner

medz commented Apr 5, 2023

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants