An open API service indexing awesome lists of open source software.

https://github.com/kayr/ezy-query

Convert Your Sql Query To A Queryable Java API/Code.. think of A Queryable View In Your Code Using Java
https://github.com/kayr/ezy-query

code-generation codegenerator gradle gradle-plugin java sql sql-query

Last synced: 10 months ago
JSON representation

Convert Your Sql Query To A Queryable Java API/Code.. think of A Queryable View In Your Code Using Java

Awesome Lists containing this project

README

          

# EzyQuery: Bring back the power of raw SQL in Java.

EzyQuery brings the best of both worlds: SQL's expressiveness and Java's compile-time safety, making database access
both powerful and reliable. It is a Java library that bridges the gap between raw SQL and compile-time type-safe code.

It is lightweight with zero runtime dependencies.

## Download

[![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.github.kayr/ezy-query-gradle-plugin/badge.svg?style=plastic)](https://maven-badges.herokuapp.com/maven-central/io.github.kayr/ezy-query-gradle-plugin)

Add the following to your `build.gradle` file.

```groovy
buildscript {
repositories {
mavenCentral()
}
}

plugins {
id 'io.github.kayr.gradle.ezyquery' version '0.0.24'
}
```

## Features

1. Flexible Query fluent API e.g `where(Q.CUSTOMER_NAME.eq("John").and(Q.CUSTOMER_EMAIL.isNotNull()))`.
2. Query Expressions e.g `.where(Cnd.expr("customerName = 'John' and customerEmail is not null"))`.
Ideally if you are building a Rest-API then clients get a powerful filtering API by passing the expressions as a
parameter. The query is parsed and converted to a parameterized sql query.
3. Named parameters. You can add named parameters to static parts of your SQL query and these will recognize.
4. Support for CTEs.
5. If your sql is too complex or uses unsupported features you can still use it to generate APIs for your named params.
6. All generated sql queries are parameterized to avoid sql injection.
7. Automatic mapping of sql result to java pojo.
8. The same query is used to count and list data. Which makes building pagination easy and prevents the need to write
two queries.
9. Sort by any field in the query. e.g `orderBy(GET_CUSTOMERS.CUSTOMER_NAME.asc())`.
10. You can sort using a string expression. e.g `customerName asc, customerEmail desc`.
11. Gradle plugin to generate the java code from your sql files.
12. Maven plugin

## How it works

Converts your SQL query to A queryable Java API/Code... think of A Queryable View In Your Code Using Java
This will work for most sql queries in the
format `WITH ... ... SELECT ... FROM ... WHERE ... JOIN ... ORDER BY ... LIMIT ... OFFSET ...`

1. Write your sql query in a file ending with `.ez.sql`. in the directory `src/main/ezyquery`.
For better organisation consider adding the files in the similar package structure and your sources code.

```sql
-- file: customer-queries.sql
-- ## dynamic:get all customers
SELECT c.id as customerId_long,
lower(c.customerName) as customerName_string,
c.email as customerEmail_string,
c.phone as customerPhone,
c.score as customerScore
FROM customers c;

-- ##dynamic:get orders
SELECT o.id as customerId_long,
c.cutomerName as customerName_string,
c.email as customerEmail,
o.item as item,
o.price as price_double,
o.quantity as quantity
FROM orders o
inner join customers c on c.id = o.customerId and c.membership = :membership
WHERE c.membership = :membership;

-- ## static: update customer score
update customers c
set c.score = :score
where email = :email;
```

The above query has two types of queries dynamic and static.
- **Dynamic queries**: These are pre-processed by ezy-query to allow for advanced filtering, sorting, and
pagination. If you prefix your query name with dynamic e.g `-- ## dynamic:get all customers` then ezy query will
pre-process to allow for advance feature. Only select statements can be dynamic queries.
- **Static queries**: These only get basic preprocessing to extract the named parameters and mostly left untouched.
These are useful for other types of queries for example insert, delete, update statements or any other types of
sql. EzyQuery will not try to validate the sql syntax like the case for dynamic queries. These are creating by
prefixing the name with *static:* e.g. `## static: update customer status`
2. Run `./gradlew ezyBuild` to convert your SQL query file to a java class. This will generate a java class for you.
The class name will be generated based on your file name. The class `CustomerQueries` will contain two query classes
that can be accessed via the static methods e.g. `CustomerQueries.getAllCustomers()`. Below is a snippet of what will
be generated.
```java
//This is a snippet of the generated class
public class CustomerQueries {
public static GetAllCustomers getAllCustomers() {...}
public static GetOrders getOrders() {...}
//this is generated by the get all get all customers query
public static class GetAllCustomers implements EzyQueryWithResult {
...
public static class Result implements DynamicFieldSetter {
...
}
}
//this is generated by the get orders query
public static class GetOrders implements EzyQueryWithResult {
...
public static class Result implements DynamicFieldSetter {
...
}
}
}
```
3. Setup up EzySql instance that will act as an entry point to using EzyQuery. Below is an example with Hikari
```java
var config = new HikariConfig();
//...
EzySql ezySql = EzySql.withDataSource(new HikariDataSource(config))
```
4. You can now use the generated class to query your database with a flexible easy to use api.
The example below that fetches/filters/sorts customers from the db whose name is `John` and email is not null;

```groovy
var Q = CustomerQueries.getAllCustomers()
var result = ezySql.from(Q)
.where(Q.CUSTOMER_NAME.eq("john").and(Q.CUSTOMER_EMAIL.isNotNull()))
.orderBy(Q.CUSTOMER_NAME.asc(), Q.CUSTOMER_EMAIL.desc())
.offset(0)
.limit(10)
.listAndCount()

assert result.getCount() > 0
assert !result.getList().isEmpty()
assert result.getList().get(0).getCustomerName().equals("john")
```

## Usage

- [Other features](#other-features)
- [Working with spring boot](#working-with-spring-boot)
- [Filtering](#filtering)
- [Sorting and pagination](#sorting-and-pagination)
- [Named Parameters](#named-parameters)
- [Executing other types of queries(INSERT/UPDATE/DELETE etc)](#executing-other-types-of-queriesinsertupdatedelete-etc)
- [Specifying a custom result mapper](#specifying-a-custom-result-mapper)
- [Adding a default where clause to a generate query](#adding-a-default-where-clause-to-a-generate-query)
- [Adding data types to the generated pojo.](#adding-data-types-to-the-generated-pojo)
- [Overriding default type mappings e.g for newer JDBC drivers.](#overriding-default-type-mappings-eg-for-newer-jdbc-drivers)
- [Optionally select fields to be returned.](#optionally-select-fields-to-be-returned)
- [Using on older versions of Gradle.](#using-on-older-versions-of-gradle)

## Other features

### Working with spring boot

If you are using spring boot, you can do this by creating a bean of type `EzySql` in your spring configuration. Then
inject the bean
into your code using the `@Autowired` annotation.

```java

@Bean
public EzySql ezyQuery(DataSource dataSource) {
return EzySql.withDataSource(dataSource);
}
```

### Filtering

You have multiple options to filter provide filters to your query:

1. **Through the fluent API**

```groovy
var Q = CustomerQueries.getAllCustomers()
var sql = ezySql.from(Q)
.where(Q.CUSTOMER_NAME.eq("John").and(Q.CUSTOMER_EMAIL.isNotNull()))
```

The above will generate the below sql query. Notice how it replaces the customer name with the full function call
`lower(c.customerName)`. Expressions can be as complex as you need them to be.

```sql
SELECT
c.id as "customerId",
lower(c.customerName) as "customerName",
c.email as "customerEmail",
c.phone as "customerPhone",
c.score as "customerScore"
FROM customers c
WHERE (lower(c.customerName) = ? AND c.email IS NOT NULL)
LIMIT 50 OFFSET 0
```

2. **Filtering with the condition API:** This one is similar to building an AST with and offers alot more predictability
of the resulting filter at the cost of readability(lispy style).

```groovy
var Q = CustomerQueries.getAllCustomers()
var sql = ezySql.from(Q)
.where(Cnd.and(Cnd.eq(Q.CUSTOMER_NAME, "John"), Cnd.isNotNull(Q.CUSTOMER_EMAIL)))
```

3. **Filtering with the ezy-query expression:** This allows you pass filter strings directly to ezy-query. This can be
useful where you want to allow client code to dynamically filter the data from the frontend.

```groovy
var Q = CustomerQueries.getAllCustomers()
var sql = ezySql.from(Q)
.where(Cnd.expr("customerName = 'John' and customerEmail is not null"))
```

**NOTE:** To use this feature you will need to add JSQLParser to your classpath. If you are using gradle you can add
it like this
```groovy
dependency {
implementation 'com.github.jsqlparser:jsqlparser:4.8'
}
```
or maven
```xml


com.github.jsqlparser
jsqlparser
4.8

```
4. **Filtering with maps:** This is where you can use the java maps to build filters for a query. This is useful for
example when you need to pass query params from an http call directly to your query:
e.g `http://example.com?customerName.eg=John&customerEmail.isNotNull`

```groovy
var Q = CustomerQueries.getAllCustomers()
var sql = ezySql.from(Q)
.where(Cnd.fromMap(
Map.of("customerName.eq", "John",
"customerEmail.isnotnull", Optional.empty())))//use optional empty or null to show that we have no value here
```

Most http server libraries use multivalue maps so there is alternate method for creating a criteria from a
multivalue map.

```groovy
var Q = CustomerQueries.getAllCustomers()

def filterMap = new HashMap>()
filterMap.put("customerName.eq", List.of("John"))
filterMap.put("customerEmail.isnotnull", Collections.emptyList())//empty list to show we have no values here

var sql = ezySql.from(Q)
.where(Cnd.fromMvMap(filterMap))
```

*Supported map operators for a map include:"
`eq,neq,like,notlike,gt,gte,lt,lte,in,notin,between,notbetween,isnull,isnotnull`
5. **Filtering with native sql:** Sometimes there will be operations not supported by ezy-query for this case you can
always fall back to using native sql. Be careful with this method,
***always paremetirize your variables to avoid sql injection***

```groovy
var Q = CustomerQueries.getAllCustomers()
var sql = ezySql.from(Q)
.where(Cnd.sql("c.customerName = ? AND c.email IS NOT NULL", "John"))
```

### Sorting and pagination

Sort using fields

```groovy
var Q = CustomerQueries.getAllCustomers()
var sql = ezySql.from(Q)
.orderBy(Q.CUSTOMER_NAME.asc(), Q.CUSTOMER_EMAIL.desc())
.limit(10)
.offset(20)
```

Sort using strings expression

```groovy
var Q = CustomerQueries.getAllCustomers()
var sql = ezySql.from(Q)
.orderBy("customerName asc, customerEmail desc")
.limit(10, 20) //another alternative for pagination
```

Sort using Sort Object

```groovy
var Q = CustomerQueries.getAllCustomers()
var sql = ezySql.from(Q)
.orderBy(Sort.by("customerName", Sort.DIR.ASC))
```

### Named Parameters

You can add named parameters to static parts of your sql query and pass them at runtime. This is useful when some
parts of the query are not necessarily dynamic e.g if you have an sql query that has derived tables that need named
params.

Name parameters are supported in the where clause, join conditions and order by clauses.

Given the following sql query.

```sql
-- file: get-customers.sql
SELECT o.id as customerId,
c.name as customerName,
c.email as customerEmail,
o.item as item,
o.price as price,
o.quantity as quantity
FROM orders o
inner join customers c on c.id = o.customerId and c.membership = :membership
WHERE c.membership = :membership
```

You can pass the named parameter `:membership` at runtime as follows.

```groovy
var Q = CustomerQueries.getOrders()
var P = CustomerQueries.GetOrders.PARAMS
var sql = ezySql.from(Q)
.where(Q.PRICE.gt(100).and(Q.QUANTITY.lt(10)))
.setParam(P.MEMBERSHIP, "GOLD")
```

This will generate the following sql query along with the params.

```sql
SELECT o.id as "customerId",
c.cutomerName as "customerName",
c.email as "customerEmail",
o.item as "item",
o.price as "price",
o.quantity as "quantity"
FROM orders o
INNER JOIN customers c ON c.id = o.customerId AND c.membership = ?
WHERE (c.membership = ?)
AND (o.price > ? AND o.quantity < ?)
LIMIT 50 OFFSET 0
-- params=[GOLD, GOLD, 100, 10]}
```

You can see that the `GOLD` param has been added to the list of params.

### Executing other types of queries(INSERT/UPDATE/DELETE etc)

Ezyquery was mainly built to provide dynamically ways to filter your data at runtime using a relatively safe and easy to
use API. However, you can also use it to execute any arbitrary sql query like inserts, deletes or updates. Parameters are
extracted and made available as method calls in your query.

**Example**:

When you have the query below.


```sql
-- ## static: update customer
update customers c
set c.score = :score
where email = :email;
```

Below is how you would execute it using the ezy query sql utility classes.


```groovy
//set up the datasource
DataSource ds = new HikariDataSource(config)
//create the Zql instance which is a convenient api around jdbc
var zql = new Zql(ConnectionProvider.of(ds)
//var zql = ezySql.getZql() // or you can get it from EzySql instance

var Q = CustomerQueries.updateCustomer()

var updateCount = zql.update(
Q.email("john@example.com") //these functions are generated from the query param in the sql query
.score(10)
.getQuery())
assert updateCount > 0
```

### Specifying a custom result mapper

You can specify a custom mapper to control how you want the results to be returned or mapped from the database. E.g
Instead of returning a list of pojos you can return a list of maps. Here is an example.

We already have a built-in mapper that converts the result to a map. You can use it as follows.

```groovy
var Q = CustomerQueries.getAllCustomers()
List result = ezySql.from(Q)
.mapTo(Mappers.toMap())
.list()
```

For illustration purposes we will create a custom mapper that converts the result to a map. See code below.

```groovy
var Q = CustomerQueries.getAllCustomers()
List result = ezySql.from(Q)
.mapTo((rowIndex, columns, resultSet) -> {
Map map = new HashMap<>();
for (ColumnInfo column : columns) {
map.put(column.getLabel(), resultSet.getObject(column.getLabel()));
}
return map;
})
.list();
```

### Adding a default where clause to a generate query

Just add a default where clause to your base sql query, then all your queries will have this where clause.
An example use-case is where you always want to fetch active customers

```sql
-- file: get-customer.sql
SELECT c.id as customerId,
c.name as customerName,
c.email as customerEmail,
c.score as customerScore
FROM customers c
WHERE c.status = 'active'
```

The above will add the where clause `c.status = 'active'` to all queries generated from the above query.

### Adding data types to the generated pojo.

The generated result pojo by default will have all fields as `Object`.
You can add a data type to the generated pojo by adding a suffix to the field name aliases like below.

```sql
-- file: get-customer.sql
SELECT c.id as customerId_int,
c.name as customerName_string,
c.score as customerScore_double, ....
```

With the above sql,the generated pojo will have the following fields.

```java
... // code ommited for brevity
private Integer customerId;
private String customerName;
private Double customerScore;
...
```

The supported data types are:

- `int`
- `long`
- `double`
- `float`
- `string`
- `boolean`
- `date`
- `time`
- `decimal`
- `bigint`
- `byte`
- `object`

If these types are not enough for you, you can add your own custom types by specifying custom type mappings in
the `ezy-query.properties` file.

In the root of the ezy-query source directory, create a file called `ezy-query.properties` and add the following.

```properties
# file: ezy-query.properties
#add your custom type mappings here
#the format is type.=
#e.g
type.customtype=java.time.LocalDate
type.vector=java.util.Vector
```

Then in your sql file you can use the custom type as follows.

```sql
-- file: get-customer.sql
SELECT c.id as customerId_customtype, -- specify the custom type
c.name as customerName_string,
c.score as customerTags_vector, -- specify the custom vector type
....
```

The generated pojo will have the following fields.

```java
//.... code ommited for brevity
private LocalDate customerId;
private String customerName;
private Vector customerTags;
...
```

### Overriding default type mappings e.g for newer JDBC drivers.

Some JDBC drivers may return types that are not supported by default. e.g newer mysql drivers
return `java.time.LocalDate` or `java.util.LocalTime` for `date` and `time` types respectively. You can override the
default mappings by specifying your own custom mappings.

```properties
# file: ezy-query.properties
type.date=java.time.LocalDate
type.time=java.time.LocalTime
```

### Optionally selecting fields to be returned.

```groovy
var Q = CustomerQueries.getAllCustomers()
var result = ezySql.from(Q)
.select(Q.CUSTOMER_NAME, Q.CUSTOMER_EMAIL)
.list()

assert result.size() > 0
assert result.get(0).customerName != null
assert result.get(0).customerId == null//we did not select this.. so it will be null
```

### Using on older versions of Gradle.

In the future, we will support older versions. For older versions
add the script below as a workaround.
The script adds the necessary tasks to your `build.gradle` file.

```groovy

buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "io.github.kayr:ezy-query-codegen:" //see the latest above
}
}

task("ezyBuild") {

def input = file("src/main/ezyquery").toPath()
def output = file("build/generated/ezy/main").toPath()

doLast {
if (input.toFile().exists()) {
Files.createDirectories(output)
BatchQueryGen.generate(input, output)
}
}
}
task("ezyClean") {
doLast {
project.delete("build/generated/ezy/")
}
}

sourceSets {
main {
java {
srcDir "build/generated/ezy/main"
}

}
test {
java {
srcDir "build/generated/ezy/test"
}
}
}
```