Use declarative interfaces for Java database queries.
Quick Start
Gradle Configuration
Set up dependencies for the annotation processor and runtime. Add a JSON file with configuration for the annotation processor, and tell the annotation processor the path. TODO: milestone2 - gradle plugin to help with this.
src/main/resources/kiwi-config.json
{
"dataSources": {
"default": {
"named": "default",
"url": "jdbc:postgresql://localhost:5432/example?user=example",
"database": "example",
"username": "example"
}
},
"dependencyInjectionStyle": "JAKARTA",
"debug": false
}
To use with Spring, set "dependencyInjectionStyle": "SPRING"
.
def kiwiversion = "0.2"
dependencies {
annotationProcessor("org.ethelred.kiwiproc:processor:$kiwiversion")
implementation("org.ethelred.kiwiproc:runtime:$kiwiversion")
implementation("jakarta.inject:jakarta.inject-api:2.0.1")
}
tasks.withType(JavaCompile).configureEach {
options.compilerArgs.add("-Aorg.ethelred.kiwiproc.configuration=src/main/resources/kiwi-config.json")
}
val kiwiversion = "0.2"
dependencies {
annotationProcessor("org.ethelred.kiwiproc:processor:$kiwiversion")
implementation("org.ethelred.kiwiproc:runtime:$kiwiversion")
implementation("jakarta.inject:jakarta.inject-api:2.0.1")
}
tasks.withType<JavaCompile>().configureEach {
options.compilerArgs.add("-Aorg.ethelred.kiwiproc.configuration=src/main/resources/kiwi-config.json")
}
Define a DAO interface
@DAO (1)
public interface CountryCityDao {
@SqlQuery("""
SELECT id, name, code
FROM country
WHERE code = :code
""") (2)
@Nullable
Country findCountryByCode(String code);
@SqlUpdate("""
INSERT INTO city(name, country_id)
VALUES (:name, :country_id)
""")
boolean addCity(String name, int countryId);
}
1 | Declare an interface as being a DAO. |
2 | Define a query. The SQL statement goes inline with the code. Parameters are inserted with ':'. |
Inject
Use your favourite dependency injection framework to inject an instance of your DAO.
Framework Support
Kiwiproc uses only the Jakarta annotations @Singleton
and @Named
, so should work with any Dependency Injection framework that supports those.
It expects a DataSource
to be injected, with a name matching that specified on the @DAO
annotation.
For Spring, the @Repository
and @Qualifier
annotations are used instead.
-
Micronaut test cases are in the "test-micronaut" subproject.
-
Spring test cases are in the "test-spring" subproject.
Database Support
Kiwiproc is currently only tested with Postgresql. Support for SQL Arrays is known to be specific to Postgresql. Other databases using standard JDBC may work, as long as you don’t use Arrays.
Types and Validation
Kiwiproc performs validations during build, based on the method signature and database metadata.
- Type Hierarchy
-
Do the types in the method signature fit Kiwiproc conventions.
- Exhaustiveness
-
Are all input parameters matched. Are all result columns matched.
- Compatibility
-
Does Kiwiproc have supported conversions between the Java and SQL types.
Types
A semi-formal attempt to describe how Kiwiproc uses types. These are generalized types, not Java or SQL types.
Notation | Description |
---|---|
V |
void |
I |
int or a compatible type |
P |
A primitive or boxed primitive |
O |
Object type. Specifically, one of: String, BigInteger, BigDecimal, LocalDate, LocalTime, OffsetTime, LocalDateTime, OffsetDateTime |
S |
P | O |
?S |
@Nullable (specifically org.jspecify.annotations.Nullable), or Optional<S>. A boxed primitive is also treated as nullable when it is not a type parameter of a container. Optional is only accepted as the return type of a query method. |
SC<S> |
A Collection or array, only in a context where it is mapped to or from a SQL array. |
RC |
S | ?S | SC |
RCS |
RC | RCS RC |
R(RCS) |
A Java record. |
CV |
S | SC | R |
C<CV> |
A Collection, array or Iterable. |
MK |
R | S |
MV |
CV | C |
M<MK, MV> |
A Map. |
Unresolved Types
Used internally, not by end users. Documented here for developers working on Kiwiproc itself.
Notation | Description |
---|---|
UC<S> |
SC<S> | C<S> |
UM |
Map, where the key and/or value are not Record, and we need to know the column names. |
Nullability
In the current version of the library, nullability handling is neither as strict or as consistent as I would like it to be. |
Java values are treated as non-null, except as described here.
-
Elements of SC, C, M must not be nullable. The current implementation allows null values, but skips over adding them to the collection.
-
A boxed primitive is treated as nullable, except where it is an element of SC, C, M.
-
An object type annotated with
org.jspecify.annotations.Nullable
. This does not include boxed primitives. -
The element of an Optional<>, OptionalInt, OptionalDouble, OptionalLong.
JDBC drivers may return 'unknown' for nullability. In practice, the Postgres driver always returns 'unknown' for parameters. (As do a couple of other drivers I checked for comparison.) The way that Kiwiproc deals with unknown nullability could do with more design work.
Exhaustiveness
-
For each Java method parameter, there must be a corresponding parameter in the SQL statement. When the method parameter is a record, at least one of its components must correspond to a parameter in the SQL statement.
-
For each parameter in the SQL statement, there must be a corresponding method parameter, or component of a record that is a method parameter.
-
Every column in the SQL result must be used in the return type of the method.
-
Every value or component in the return type of the method must correspond to a column in the SQL result.
Compatibility
Kiwiproc has a set of type conversions, for any supported types where it makes sense. "Compatibility" means that there is a type conversion between the matching Java and SQL elements.
Source | Target | Warning |
---|---|---|
BigDecimal |
BigInteger |
possible lossy conversion from BigDecimal to BigInteger |
BigDecimal |
byte |
possible lossy conversion from BigDecimal to byte |
BigDecimal |
double |
possible lossy conversion from BigDecimal to double |
BigDecimal |
float |
possible lossy conversion from BigDecimal to float |
BigDecimal |
int |
possible lossy conversion from BigDecimal to int |
BigDecimal |
long |
possible lossy conversion from BigDecimal to long |
BigDecimal |
short |
possible lossy conversion from BigDecimal to short |
BigInteger |
BigDecimal |
|
BigInteger |
boolean |
|
BigInteger |
byte |
possible lossy conversion from BigInteger to byte |
BigInteger |
double |
possible lossy conversion from BigInteger to double |
BigInteger |
float |
possible lossy conversion from BigInteger to float |
BigInteger |
int |
possible lossy conversion from BigInteger to int |
BigInteger |
long |
possible lossy conversion from BigInteger to long |
BigInteger |
short |
possible lossy conversion from BigInteger to short |
LocalDate |
LocalDateTime |
|
LocalDate |
OffsetDateTime |
|
LocalDate |
long |
uses system default ZoneId |
LocalDateTime |
LocalDate |
|
LocalDateTime |
LocalTime |
|
LocalDateTime |
long |
uses system default ZoneId |
OffsetDateTime |
LocalDate |
|
OffsetDateTime |
LocalDateTime |
|
OffsetDateTime |
OffsetTime |
|
OffsetDateTime |
long |
|
OffsetTime |
LocalTime |
|
String |
BigDecimal |
possible NumberFormatException parsing String to BigDecimal |
String |
BigInteger |
possible NumberFormatException parsing String to BigInteger |
String |
Byte |
possible NumberFormatException parsing String to byte |
String |
Character |
possible NumberFormatException parsing String to char |
String |
Double |
possible NumberFormatException parsing String to double |
String |
Float |
possible NumberFormatException parsing String to float |
String |
Integer |
possible NumberFormatException parsing String to int |
String |
LocalDate |
possible DateTimeParseException parsing String to LocalDate |
String |
LocalDateTime |
possible DateTimeParseException parsing String to LocalDateTime |
String |
LocalTime |
possible DateTimeParseException parsing String to LocalTime |
String |
Long |
possible NumberFormatException parsing String to long |
String |
OffsetDateTime |
possible DateTimeParseException parsing String to OffsetDateTime |
String |
OffsetTime |
possible DateTimeParseException parsing String to OffsetTime |
String |
Short |
possible NumberFormatException parsing String to short |
String |
boolean |
|
String |
byte |
possible NumberFormatException parsing String to byte |
String |
char |
possible NumberFormatException parsing String to char |
String |
double |
possible NumberFormatException parsing String to double |
String |
float |
possible NumberFormatException parsing String to float |
String |
int |
possible NumberFormatException parsing String to int |
String |
long |
possible NumberFormatException parsing String to long |
String |
short |
possible NumberFormatException parsing String to short |
boolean |
BigInteger |
|
boolean |
byte |
|
boolean |
char |
|
boolean |
int |
|
boolean |
long |
|
boolean |
short |
|
byte |
BigDecimal |
|
byte |
BigInteger |
|
byte |
boolean |
|
byte |
byte |
|
byte |
char |
possible lossy conversion from byte to char |
byte |
double |
|
byte |
float |
|
byte |
int |
|
byte |
long |
|
byte |
short |
|
char |
boolean |
|
char |
byte |
possible lossy conversion from char to byte |
char |
char |
|
char |
double |
|
char |
float |
|
char |
int |
|
char |
long |
|
char |
short |
possible lossy conversion from char to short |
double |
BigDecimal |
|
double |
BigInteger |
|
double |
byte |
possible lossy conversion from double to byte |
double |
char |
possible lossy conversion from double to char |
double |
float |
possible lossy conversion from double to float |
double |
int |
possible lossy conversion from double to int |
double |
long |
possible lossy conversion from double to long |
double |
short |
possible lossy conversion from double to short |
float |
BigDecimal |
|
float |
BigInteger |
|
float |
byte |
possible lossy conversion from float to byte |
float |
char |
possible lossy conversion from float to char |
float |
double |
|
float |
float |
|
float |
int |
possible lossy conversion from float to int |
float |
long |
possible lossy conversion from float to long |
float |
short |
possible lossy conversion from float to short |
int |
BigDecimal |
|
int |
BigInteger |
|
int |
boolean |
|
int |
byte |
possible lossy conversion from int to byte |
int |
char |
possible lossy conversion from int to char |
int |
double |
|
int |
float |
|
int |
int |
|
int |
long |
|
int |
short |
possible lossy conversion from int to short |
long |
BigDecimal |
|
long |
BigInteger |
|
long |
LocalDate |
uses system default ZoneId |
long |
LocalDateTime |
uses system default ZoneId |
long |
LocalTime |
uses system default ZoneId |
long |
OffsetDateTime |
uses system default ZoneId |
long |
OffsetTime |
uses system default ZoneId |
long |
boolean |
|
long |
byte |
possible lossy conversion from long to byte |
long |
char |
possible lossy conversion from long to char |
long |
double |
|
long |
float |
|
long |
int |
possible lossy conversion from long to int |
long |
long |
|
long |
short |
possible lossy conversion from long to short |
short |
BigDecimal |
|
short |
BigInteger |
|
short |
boolean |
|
short |
byte |
possible lossy conversion from short to byte |
short |
char |
possible lossy conversion from short to char |
short |
double |
|
short |
float |
|
short |
int |
|
short |
long |
|
short |
short |
(Any type can be converted to String.)