Skip to content

Instantly share code, notes, and snippets.

@7h3kk1d
Created October 28, 2022 01:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 7h3kk1d/ad7c6fae91ab20c38d4b1081aa8b568c to your computer and use it in GitHub Desktop.
Save 7h3kk1d/ad7c6fae91ab20c38d4b1081aa8b568c to your computer and use it in GitHub Desktop.
Monadic SQL Transactions
import java.sql.Date;
public record Dog(String name, Date birthdate) {}
public record Person(String name) {
}
import com.jnape.palatable.lambda.adt.Either;
import com.jnape.palatable.lambda.adt.Unit;
import com.jnape.palatable.lambda.io.IO;
import java.sql.Connection;
import java.sql.Date;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.LocalDate;
import static com.jnape.palatable.lambda.adt.Either.left;
import static com.jnape.palatable.lambda.adt.Either.right;
import static com.jnape.palatable.lambda.adt.Unit.UNIT;
import static com.jnape.palatable.lambda.io.IO.io;
import static com.jnape.palatable.lambda.io.IO.pureIO;
import static com.jnape.palatable.lambda.monad.transformer.builtin.ReaderT.readerT;
public class SQLExample {
public static void main(String[] args) {
String fileName = "sample.db";
setupDatabase(fileName);
SQLTransactor sqlTransactor = new SQLTransactor(fileName);
PeopleService peopleService = new PeopleService();
DogService dogService = new DogService();
Transactional<IO<?>, Throwable, Connection, Long> createPersonAndDog = peopleService.addPerson(new Person("Alex"))
.flatMap(alexId -> dogService.dogService(alexId, new Dog("Tobi", Date.valueOf(LocalDate.of(2018, 1, 1)))));
IO<Unit> runFirstTransaction = sqlTransactor
.runTransactional(createPersonAndDog)
.flatMap(e -> io(() -> System.out.println(e)));
Transactional<IO<?>, Throwable, Connection, Long> rollbackableTransaction =
peopleService.addPerson(new Person("Rollbackable Person"))
.flatMap(personId -> dogService.dogService(-1, // This is going to fail because no human exists with -1 as an id
new Dog("Fake dog",
Date.valueOf(LocalDate.of(2035, 1, 1)))));
IO<Unit> runSecondTransaction = sqlTransactor
.runTransactional(rollbackableTransaction)
.flatMap(e -> io(() -> System.out.println(e)));
IO<Unit> intentionalRollback =
sqlTransactor.runTransactional(
peopleService.addPerson(new Person("Another Person"))
.flatMap(__ -> new Transactional<IO<?>, Throwable, Connection, Long>(pureIO(), readerT(conn -> io(left(new RuntimeException("Explicit Rollback")))))))
.flatMap(e -> io(() -> System.out.println(e)));
runFirstTransaction
.discardL(runSecondTransaction)
.discardL(intentionalRollback)
.unsafePerformIO();
}
// This is just setup I wouldn't actually do the schema changes internally to the app
public static void setupDatabase(String fileName) {
String url = "jdbc:sqlite:./" + fileName;
try (Connection conn = DriverManager.getConnection(url); Statement stmt = conn.createStatement()) {
stmt.execute("PRAGMA foreign_keys = ON;");
stmt.execute("""
CREATE TABLE IF NOT EXISTS people (
id integer PRIMARY KEY,
name text
);
""");
stmt.execute("""
CREATE TABLE IF NOT EXISTS dogs (
id integer PRIMARY KEY,
name text NOT NULL,
birthdate date,
human_id INTEGER NOT NULL,
FOREIGN KEY (human_id) REFERENCES people (id)
);
""");
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public static class PeopleService {
Transactional<IO<?>, Throwable, Connection, Long> addPerson(Person person) {
String sql = "INSERT INTO people(name) VALUES(?);";
return new Transactional<>(pureIO(),
readerT(connection -> io(() -> {
PreparedStatement callableStatement = connection.prepareStatement(sql);
callableStatement.setString(1, person.name());
callableStatement.execute();
ResultSet generatedKeys = callableStatement.getGeneratedKeys();
return right(generatedKeys.getLong(1));
})));
}
Transactional<IO<?>, Throwable, Connection, Unit> updateName(Long id, String newName) {
String sql = "UPDATE people SET name=? WHERE ID=?;";
return new Transactional<>(pureIO(),
readerT(connection -> io(() -> {
PreparedStatement callableStatement = connection.prepareStatement(sql);
callableStatement.setString(1, newName);
callableStatement.setLong(2, id);
callableStatement.execute();
int updateCount = callableStatement.getUpdateCount();
return updateCount == 1 ? right(UNIT) : left(new RuntimeException("Update updated " + updateCount + " records"));
})));
}
}
public static class DogService {
Transactional<IO<?>, Throwable, Connection, Long> dogService(long personId, Dog dog) {
String sql = "INSERT INTO dogs(name, birthdate, human_id) VALUES(?, ?, ?);";
return new Transactional<>(pureIO(),
readerT(connection -> io(() -> {
PreparedStatement callableStatement = connection.prepareStatement(sql);
callableStatement.setString(1, dog.name());
callableStatement.setDate(2, dog.birthdate());
callableStatement.setLong(3, personId);
callableStatement.execute();
ResultSet generatedKeys = callableStatement.getGeneratedKeys();
return right(generatedKeys.getLong(1));
})));
}
}
}
import com.jnape.palatable.lambda.adt.Either;
import com.jnape.palatable.lambda.io.IO;
import com.jnape.palatable.lambda.monad.Monad;
import org.sqlite.SQLiteConfig;
import java.sql.Connection;
import java.sql.DriverManager;
import static com.jnape.palatable.lambda.functions.builtin.fn1.Constantly.constantly;
import static com.jnape.palatable.lambda.io.IO.io;
// You could pool these or wrap it. You want to make sure that consumers aren't calling commit/rollback directly so customize to your use case.
public final class SQLTransactor implements Transactor<IO<?>, Connection, Throwable> {
String filename;
public SQLTransactor(String filename) {
this.filename = filename;
}
@Override
public <A> IO<Either<Throwable, A>> runTransactional(Transactional<IO<?>, Throwable, Connection, A> transactional) {
return createConnection()
.flatMap(connection -> transactional.run(connection)
.<IO<Either<Throwable, A>>>coerce()
.safe().fmap(Monad::join) // Catch exceptions
.flatMap(e -> e.match(t -> io(() -> connection.rollback()),
a -> io(connection::commit))
.fmap(constantly(e)))
.discardR(IO.io(connection::close)));
}
private IO<Connection> createConnection() {
return io(() -> {
String url = "jdbc:sqlite:./" + filename;
SQLiteConfig config = new SQLiteConfig();
config.enforceForeignKeys(true);
Connection conn = DriverManager.getConnection(url, config.toProperties());
boolean execute = conn.createStatement().execute("PRAGMA foreign_keys = ON;");
conn.setAutoCommit(false); // Don't autocommit
return conn;
});
}
}
import com.jnape.palatable.lambda.adt.Either;
import com.jnape.palatable.lambda.functions.Fn1;
import com.jnape.palatable.lambda.functions.recursion.RecursiveResult;
import com.jnape.palatable.lambda.functions.specialized.Lift;
import com.jnape.palatable.lambda.functions.specialized.Pure;
import com.jnape.palatable.lambda.monad.Monad;
import com.jnape.palatable.lambda.monad.MonadRec;
import com.jnape.palatable.lambda.monad.transformer.MonadT;
import com.jnape.palatable.lambda.monad.transformer.builtin.EitherT;
import com.jnape.palatable.lambda.monad.transformer.builtin.ReaderT;
import static com.jnape.palatable.lambda.adt.Either.right;
import static com.jnape.palatable.lambda.monad.transformer.builtin.EitherT.eitherT;
public final class Transactional<M extends MonadRec<?, M>, Cancel, Context, A> implements MonadT<M, A, Transactional<M, Cancel, Context, ?>, Transactional<?, Cancel, Context, ?>> {
private final Pure<M> pure;
private final ReaderT<Context, M, Either<Cancel, A>> run;
public Transactional(Pure<M> pure, ReaderT<Context, M, Either<Cancel, A>> run) {
this.pure = pure;
this.run = run;
}
@Override
public <B> Transactional<M, Cancel, Context, B> trampolineM(Fn1<? super A, ? extends MonadRec<RecursiveResult<A, B>, Transactional<M, Cancel, Context, ?>>> fn) {
return new Transactional<>(pure, eitherT(run)
.trampolineM(a -> {
Transactional<M, Cancel, Context, RecursiveResult<A, B>> coerce = fn.apply(a)
.coerce();
return eitherT(coerce.run);
}).runEitherT());
}
@Override
public <B> Transactional<M, Cancel, Context, B> flatMap(Fn1<? super A, ? extends Monad<B, Transactional<M, Cancel, Context, ?>>> f) {
EitherT<ReaderT<Context, M, ?>, Cancel, B> readerTCancelBEitherT = eitherT(run).flatMap(a -> {
Transactional<M, Cancel, Context, B> apply = f.apply(a).coerce();
ReaderT<Context, M, Either<Cancel, B>> run1 = apply.run;
return eitherT(run1);
});
return new Transactional<>(pure, readerTCancelBEitherT.runEitherT());
}
@Override
public <B> Transactional<M, Cancel, Context, B> pure(B b) {
Pure<ReaderT<Context, M, ?>> readerTPure = ReaderT.pureReaderT(pure);
ReaderT<Context, M, Either<Cancel, B>> apply = readerTPure.apply(right(b));
return new Transactional<>(pure, apply);
}
@Override
public <B, N extends MonadRec<?, N>> Transactional<N, Cancel, Context, B> lift(MonadRec<B, N> mb) {
Lift<ReaderT<Context, ?, ?>> readerTLift = ReaderT.liftReaderT();
return new Transactional<>(mb::pure, readerTLift.<B, N, ReaderT<Context, N, B>>apply(mb)
.fmap(Either::right));
}
public MonadRec<Either<Cancel, A>, M> run(Context context) {
return run.runReaderT(context);
}
}
import com.jnape.palatable.lambda.adt.Either;
import com.jnape.palatable.lambda.monad.MonadRec;
public interface Transactor<M extends MonadRec<?, M>, Context, Cancel> {
<A> MonadRec<Either<Cancel, A>, M> runTransactional(Transactional<M, Cancel, Context, A> transactional);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment