Created
October 28, 2022 01:49
-
-
Save 7h3kk1d/ad7c6fae91ab20c38d4b1081aa8b568c to your computer and use it in GitHub Desktop.
Monadic SQL Transactions
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.sql.Date; | |
public record Dog(String name, Date birthdate) {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public record Person(String name) { | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
}))); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
}); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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