Introduction
Why Pyranid?
Pyranid makes working with JDBC pleasant and puts your relational database first. Writing SQL "just works". Closure-based transactions are simple to reason about. Modern Java language features are supported.
No new query languages to learn. No leaky object-relational abstractions. No kitchen-sink frameworks that come along for the ride. No magic.
Pyranid is commercially-friendly Open Source Software, proudly powering production systems since 2015.
Design Goals
- Small codebase
- Customizable
- Threadsafe
- Zero dependencies
- DI-friendly
Pyranid is built to be small and easy to understand.
Its API design aims for a minimal footprint with a high strength-to-weight ratio.
Do Zero-Dependency Libraries Interest You?
Similarly-flavored commercially-friendly OSS libraries are available.
- Soklet is a DI-friendly HTTP 1.1 server that supports Virtual Threads
- Lokalized enables natural-sounding translations (i18n) via an expression language
Installation
Pyranid is a single JAR, available on Maven Central.
Maven
Java 16+
<dependency>
<groupId>com.pyranid</groupId>
<artifactId>pyranid</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>com.pyranid</groupId>
<artifactId>pyranid</artifactId>
<version>2.0.1</version>
</dependency>
Java 8+ (legacy; only critical fixes will be applied)
<dependency>
<groupId>com.pyranid</groupId>
<artifactId>pyranid</artifactId>
<version>1.0.17</version>
</dependency>
<dependency>
<groupId>com.pyranid</groupId>
<artifactId>pyranid</artifactId>
<version>1.0.17</version>
</dependency>
Gradle
repositories {
mavenCentral()
}
dependencies {
implementation 'com.pyranid:pyranid:2.0.1'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'com.pyranid:pyranid:2.0.1'
}
Direct Download
If you don't use Maven, you can drop pyranid-2.0.1.jar directly into your project. No other dependencies are required.
Example Usage
First, obtain a javax.sql.DataSource
and use it to back your Database
.
// Use any javax.sql.DataSource you like
DataSource dataSource = new HikariDataSource(new HikariConfig() {{
setJdbcUrl("jdbc:postgresql://localhost:5432/my-database");
setUsername("example");
setPassword("secret");
setConnectionInitSql("SET TIME ZONE 'UTC'");
}});
// Initialize with default configuration.
// These instances are threadsafe and intended to be shared across your app
Database database = Database.forDataSource(dataSource).build();
// Use any javax.sql.DataSource you like
DataSource dataSource = new HikariDataSource(new HikariConfig() {{
setJdbcUrl("jdbc:postgresql://localhost:5432/my-database");
setUsername("example");
setPassword("secret");
setConnectionInitSql("SET TIME ZONE 'UTC'");
}});
// Initialize with default configuration.
// These instances are threadsafe and intended to be shared across your app
Database database = Database.forDataSource(dataSource).build();
Then, do some work:
enum DepartmentId {
ACCOUNTING,
HR
}
record Employee (
UUID employeeId,
DepartmentId departmentId,
String name,
BigDecimal salary,
ZoneId timeZone,
Locale locale,
Instant createdAt
) {}
void awardAnnualRaises(DepartmentId departmentId) {
payrollSystem.startLengthyWarmupProcess();
// Ensure this set of operations commits or rolls back atomically.
// A rollback occurs if an exception bubbles out
database.transaction(() -> {
// Pull a list of all employees in the department
List<Employee> employees = database.queryForList("""
SELECT *
FROM employee
WHERE department_id=?
""", Employee.class, departmentId);
// Get a reference to the transaction that's scoped to this closure
Transaction transaction = database.currentTransaction().get();
// Calculate and apply a raise for everyone
for(Employee employee : employees) {
BigDecimal newSalary = payrollSystem.salaryWithAnnualRaise(employee));
// Make a savepoint we can roll back to if something goes wrong
Savepoint savepoint = transaction.createSavepoint();
try {
database.execute("""
UPDATE employee
SET salary=?
WHERE employee_id=?
""", newSalary, employee.employeeId());
} catch(DatabaseException e) {
// Detect a constraint violation and gracefully continue on
if("salary_too_big".equals(e.getConstraint().orElse(null)) {
// Put transaction back in good state
// (prior to constraint violation)
transaction.rollback(savepoint);
out.printf("Salary %s is too big for employee %s\n",
newSalary, employee.employeeId());
} else {
// There must have been some other problem, bubble out
throw e;
}
}
}
// Schedule some work to be done after this transaction ends
transaction.addPostTransactionOperation((transactionResult) -> {
if(transactionResult == TransactionResult.COMMITTED) {
// Successful commit?
// Email everyone with the good news
for(Employee employee : employees)
sendCongratulationsEmail(employee);
} else if(transactionResult == TransactionResult.ROLLED_BACK) {
// Exception bubbled out?
// Do some additional cleanup
payrollSystem.cancelLengthyWarmupProcess();
}
});
});
}
enum DepartmentId {
ACCOUNTING,
HR
}
record Employee (
UUID employeeId,
DepartmentId departmentId,
String name,
BigDecimal salary,
ZoneId timeZone,
Locale locale,
Instant createdAt
) {}
void awardAnnualRaises(DepartmentId departmentId) {
payrollSystem.startLengthyWarmupProcess();
// Ensure this set of operations commits or rolls back atomically.
// A rollback occurs if an exception bubbles out
database.transaction(() -> {
// Pull a list of all employees in the department
List<Employee> employees = database.queryForList("""
SELECT *
FROM employee
WHERE department_id=?
""", Employee.class, departmentId);
// Get a reference to the transaction that's scoped to this closure
Transaction transaction = database.currentTransaction().get();
// Calculate and apply a raise for everyone
for(Employee employee : employees) {
BigDecimal newSalary = payrollSystem.salaryWithAnnualRaise(employee));
// Make a savepoint we can roll back to if something goes wrong
Savepoint savepoint = transaction.createSavepoint();
try {
database.execute("""
UPDATE employee
SET salary=?
WHERE employee_id=?
""", newSalary, employee.employeeId());
} catch(DatabaseException e) {
// Detect a constraint violation and gracefully continue on
if("salary_too_big".equals(e.getConstraint().orElse(null)) {
// Put transaction back in good state
// (prior to constraint violation)
transaction.rollback(savepoint);
out.printf("Salary %s is too big for employee %s\n",
newSalary, employee.employeeId());
} else {
// There must have been some other problem, bubble out
throw e;
}
}
}
// Schedule some work to be done after this transaction ends
transaction.addPostTransactionOperation((transactionResult) -> {
if(transactionResult == TransactionResult.COMMITTED) {
// Successful commit?
// Email everyone with the good news
for(Employee employee : employees)
sendCongratulationsEmail(employee);
} else if(transactionResult == TransactionResult.ROLLED_BACK) {
// Exception bubbled out?
// Do some additional cleanup
payrollSystem.cancelLengthyWarmupProcess();
}
});
});
}
Want to see what else you can do? Start with Configuration.