Just Another Object-Relational Mapping

Build Status Sonarcloud Status SonarCloud Coverage SonarCloud Bugs SonarCloud Vulnerabilities

JAORM is a lightweight modular compile-time based Java ORM.

JAORM use Java Annotation Processor JSR 269 for Entity Mapping Generation instead of Runtime Reflection API-based mappers which have high performance cost.

JAORM is divided in modules that are used from main module using Java SPI

Modules

Core Module

<dependency>
    <groupId>io.github.ulisse1996</groupId>
    <artifactId>jaorm</artifactId>
    <version>${jaorm.version}</version>
</dependency>

DSL Module

<dependency>
    <groupId>io.github.ulisse1996</groupId>
    <artifactId>jaorm-dsl</artifactId>
    <version>${jaorm.version}</version>
</dependency>

Cache Module

<dependency>
    <groupId>io.github.ulisse1996</groupId>
    <artifactId>jaorm-cache</artifactId>
    <version>${jaorm.version}</version>
</dependency>

Transaction Module

<dependency>
    <groupId>io.github.ulisse1996</groupId>
    <artifactId>jaorm-transaction</artifactId>
    <version>${jaorm.version}</version>
</dependency>

Lombok Support Module

<dependency>
    <groupId>io.github.ulisse1996</groupId>
    <artifactId>jaorm-lombok</artifactId>
    <version>${jaorm.version}</version>
</dependency>

Sql Vendor Specific (DSL Plugin)

Oracle

<dependency>
    <groupId>io.github.ulisse1996</groupId>
    <artifactId>jaorm-sql-specific-oracle</artifactId>
    <version>${jaorm.version}</version>
</dependency>

SQL Server

<dependency>
    <groupId>io.github.ulisse1996</groupId>
    <artifactId>jaorm-sql-specific-oracle</artifactId>
    <version>${jaorm.version}</version>
</dependency>

PostgreSQL

<dependency>
    <groupId>io.github.ulisse1996</groupId>
    <artifactId>jaorm-sql-specific-oracle</artifactId>
    <version>${jaorm.version}</version>
</dependency>

1 Core

Core Module contains Entity Mapper and Query annotated services.

User must define and register a custom implementation of DatasourceProvider that return an instance of javax.sql.Datasource for execute SQL Statements.

import io.github.ulisse1996.jaorm.entity.sql.DataSourceProvider;

import javax.enterprise.inject.spi.CDI;
import javax.enterprise.util.AnnotationLiteral;
import javax.sql.DataSource;

public class DataSourceProviderImpl extends DataSourceProvider {

    @Override
    public DataSource getDataSource() {
        return CDI.current().select(DataSource.class)
                .get();
    }
}

1.1 Entity Mapper

An Entity is a POJO representing data that can be persisted to the database

Each Entity could contain:

  • 1..1 @Table annotation that represent database table name

  • 0..1 @Cacheable annotation that define an Entity as cacheable

  • 1..N @Column annotated fields that represent column in database table

  • 1..N @Id annotated fields that represent a primary/unique key in database table

  • 0..N @TableGenerated annotated fields that represent an autogenerated primary/unique key with a backed table

  • 0..N @CustomGenerated annotated fields that represent an autogenerated primary/unique key with custom logic

  • 0..N @Converter annotated fields that represent a logical conversione between a sql type and a custom/not supported type (like enum o user custom defined type)

  • 0..N @Relationship annotated fields that represent a logical relationship between two database tables

  • 0..N @Cascade annotated fields that are affected by operation done on parent Entity

import io.github.ulisse1996.jaorm.annotation.*;
import io.github.ulisse1996.jaorm.entity.converter.BooleanIntConverter;

import java.util.List;

@Table(name = "TABLE_NAME")
@Cacheable
public class Entity {

    @Id(autoGenerated = true)
    @Column(name = "COLUMN_1")
    private String column1;

    @Column(name = "COLUMN_2")
    private int column2;

    @Column(name = "READ_CONFIRM")
    @Converter(BooleanIntConverter.class)
    private boolean aBoolean;

    @Relationship(columns =
      @Relationship.RelationshipColumn(sourceColumn = "COLUMN_1", targetColumn = "COLUMN_REF_1")
    )
    @Cascade(CascadeType.ALL)
    private List<RefEntity> refEntities;

    // Getter and Setter omitted for brevity
}
For each @Column annotated fields , user must also define a getter and a setter for that field
A standard Constructor with no args must also be provided

Entities also supports Inheritance for @Columns , @Relationship or Local/Global Events

@Data
public abstract class AbstractEntity implements PrePersist<RuntimeException> {

    @Column(name = "CREATE_DATE")
    protected Date createDate;

    @Column(name = "CREATE_USER")
    protected String creationUser;

    @Override
    public void prePersist() {
        this.createDate = new Date();
        this.creationUser = "1";
    }
}

@EqualsAndHashCode(callSuper = true)
@Table(name = "COMPOUND_ENTITY")
@Data
public class CompoundEntity extends AbstractEntity {

    @Id
    @Column(name = "COMPOUND_ID")
    private int compoundId;
}

1.1.1 Id (Primary/Unique Key)

In contrast to JPA or others ORM , Jaorm does not use compound id class

User can declare one or more @Id annotation in a single entity that are used to define how Entity must be selected, updated and deleted from database

Jaorm use each @Id for create specifics where conditions

An @Id annotation can also be marked as auto-generated.

Jaorm retrieve and set auto-generated keys during persist events
1.1.1.1 Auto Generated Id/Column

User can also mark @Id or @Column for custom auto-generation using @TableGenerated or @CustomGenerated

1.1.1.2 TableGenerated

TableGenerated will use a specific table for select keyColumn that match matchKey. If a custom converter is provided, Jaorm will convert matchKey to the selected Type.

@Table(name = "ENTITY")
public class Entity {

    @Id(autoGenerated = true)
    @Column(name = "MY_ID")
    @TableGenerated(tableName = "TABLE", keyColumn = "MY_KEY", valueColumn = "NUMBER",
    matchKey = "MY_MATCH", matchConverter = ParameterConverter.NONE)
    private int myId;

    @Column(name = "MY_COL")
    @CustomGenerated(CustomGenerator.class)
    private String col;

    // Getter And Setter Omitted for brevity
}
Jaorm will lock and update atomically valueColumn during auto-generation process. User must provide a custom implementation for lock select-update using jaorm specific sql syntax (see Vendor Specific)
1.1.1.3 Custom Generated

CustomGenerated will generate a custom value using a custom implementation of CustomGenerator provided by the user.

Provided value must be compatible with field type.

1.1.1.4 Default Initializers

Jaorm will generate default values for annotated field with DefaultNumeric, DefaultString and DefaultTemporal during persist event

@Table(name = "ENTITY_WITH_DEFAULTS")
public class EntityWithDefaults {

    @Id
    @Column(name = "E_ID")
    private BigInteger id;

    @Column(name = "E_STR")
    @DefaultString("STRING")
    private String str;

    @Column(name = "E_NUM")
    @DefaultNumeric(0.390)
    private BigDecimal dec;

    @Column(name = "E_DATE")
    @DefaultTemporal
    private Date date;

    @Column(name = "E_DATE_FORMAT")
    @DefaultTemporal(format = "dd-MM-yyyy'T'HH:mm:ss", value = "20-10-2022T00:00:00")
    private Date dateWithFormat;

    public Date getDateWithFormat() {
        return dateWithFormat;
    }

    public void setDateWithFormat(Date dateWithFormat) {
        this.dateWithFormat = dateWithFormat;
    }

    public BigInteger getId() {
        return id;
    }

    public void setId(BigInteger id) {
        this.id = id;
    }

    public String getStr() {
        return str;
    }

    public void setStr(String str) {
        this.str = str;
    }

    public BigDecimal getDec() {
        return dec;
    }

    public void setDec(BigDecimal dec) {
        this.dec = dec;
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }
}

1.1.2 Column

User can declare one or more @Column annotated fields that are used by Jaorm for retrieve all selected columns from a table.

Each field that is not annotated with @Column are treated as ignored property by Jaorm
import io.github.ulisse1996.jaorm.annotation.*;
import io.github.ulisse1996.jaorm.entity.converter.BooleanIntConverter;

import java.util.List;

@Table(name = "TABLE_NAME")
@Cacheable
public class Entity {

    @Id(autoGenerated = true)
    private String column1;

    @Column(name = "COLUMN_2")
    private int column2;

    @Column(name = "READ_CONFIRM")
    private boolean aBoolean;

    // Getter and Setter omitted for brevity
}

1.1.3 Converter

User can define a custom Converter between a custom defined type (or a simple different type from the one defined on table column) and table column referenced by @Column annotation using an implementation of ValueConverter<T,R> where T is the table column type and R is the custom defined type

Jaorm contains two custom implementation for Boolean type :

  • BooleanStringConverter, that convert a matching "Y" string to true , otherwise to false

  • BooleanIntConverter, that convert 1 to true, otherwise to false

Each of them contains standard implementation for ValueConverter with a public static instance (aka Singleton) named INSTANCE.

public class BooleanIntConverter implements ValueConverter<Integer, Boolean> {

    public static final BooleanIntConverter INSTANCE = new BooleanIntConverter();

    private BooleanIntConverter() {}

    @Override
    public Boolean fromSql(Integer val) {
        return val != null && val == 1;
    }

    @Override
    public Integer toSql(Boolean val) {
        return val != null && val ? 1 : 0;
    }
}

User can follow standard implementation (defining a public INSTANCE field) or defining a public constructor

1.1.4 Relationships

User can define one of One To One, One To Many, Many-To-Many relationships between two entities using @Relationship annotation on a field.

Each annotated fields are treated as a linked entity that will be retrieved using a lazy strategy calling getter method on parent Entity

import io.github.ulisse1996.jaorm.annotation.*;
import io.github.ulisse1996.jaorm.entity.Result;
import io.github.ulisse1996.jaorm.entity.ParameterConverter;
import io.github.ulisse1996.jaorm.entity.converter.BooleanIntConverter;

import java.util.List;

@Table(name = "TABLE_NAME")
@Cacheable
public class Entity {

    @Id(autoGenerated = true)
    @Column(name = "COLUMN_1")
    private String column1;

    @Column(name = "COLUMN_2")
    private int column2;

    @Column(name = "READ_CONFIRM")
    @Converter(BooleanIntConverter.class)
    private boolean aBoolean;

    @Relationship(columns =
      @Relationship.RelationshipColumn(sourceColumn = "COLUMN_1", targetColumn = "COLUMN_REF_1")
    )
    @Cascade(CascadeType.ALL)
    private List<RefEntity> refEntities;

    @Relationship(columns =
      @Relationship.RelationshipColumn(defaultValue="1",
      converter=ParameterConverter.BIG_DECIMAL, targetColumn = "COLUMN_REF_1")
    )
    private Result<OptEntity> refEntities;

    @Relationship(columns =
      @Relationship.RelationshipColumn(defaultValue="aString", targetColumn = "COLUMN_REF_1")
    )
    private AnotherEntity refEntities;

    // Getter and Setter omitted for brevity
}

Each @Relationship must contain 1..N @RelationshipColumn

Each @RelationshipColumn could contain :

  • a default value , for relationship with a constant value

  • a converter from ones defined in ParameterConverter that convert default value in a different sql type

  • a source column , that match a table column defined in current entity

  • a target column , that match a table column defined in relationship Entity

The following table define how user must use @RelationshipColumn

Default Value Converter Source Column Target Column

Default Value

Optional

Not Required

Required

Converter

Required

Not Required

Required

Source Column

Not Required

Not Required

Required

Supported type for @Relationship annotated fields are :

  • io.github.ulisse1996.jaorm.entity.Result<T> , for an optional One to One relationship that match Optional<T> specifications

  • java.util.List<T> , for One To Many or Many-To-Many relationship

  • a User defined Entity, for One to One relationship

If Jaorm could not find a linked Entity and the linked Entity is not defined as a List or a Result , Jaorm will throw a Runtime SQL Exception

1.1.5 Entity Events

User can observe Local and/or Global Entity Events during Persist, Update or Remove events performed by Jaorm

1.1.5.1 Local Entity Events

User can observe Local Entity events implementing one or more of the followings interface on Entity class:

  • PrePersist<X>

  • PostPersist<X>

  • PreUpdate<X>

  • PostUpdate<X>

  • PreRemove<X>

  • PreDelete<X>

@Table(name = "USER")
public class User implements PostPersist<IllegalArgumentException>, PreUpdate<IllegalArgumentException> {

    @Column(name = "USER_ID")
    @Id(autoGenerated = true)
    private BigDecimal userId;

    @Column(name = "USERNAME")
    private String username;

    @Column(name = "EMAIL")
    private String email;

    @Column(name = "NAME")
    private String name;

    @Column(name = "LAST_NAME")
    private String lastName;

Each interface use a custom exception parameter for user checked exception.

If an Exception is thrown, Jaorm will throw a custom EventException

1.1.5.2 Global Entity Events

User can handle Global Events (Persist, Delete or Update) registering a custom GlobalEventListener

Jaorm will handle only Entity annotated with GlobalListener

import io.github.ulisse1996.jaorm.annotation.Column;
import io.github.ulisse1996.jaorm.annotation.GlobalListener;
import io.github.ulisse1996.jaorm.annotation.Id;
import io.github.ulisse1996.jaorm.annotation.Table;

@GlobalListener
@Table(name = "CITY")
public class GlobalEntityCity {

    @Id
    @Column(name = "CITY_ID")
    private int cityId;

    @Column(name = "CITY_NAME")
    private String name;

    public int getCityId() {
        return cityId;
    }

    public void setCityId(int cityId) {
        this.cityId = cityId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

1.1.6 Cascade

Jaorm also supports Cascade operation with @Cascade annotation on relationship fields

@Table(name = "USER")
public class User {

    @Id
    @Column(name = "COL1")
    private String id;

    @Column(name = "COL2")
    private String col2;

    @Cascade(CascadeType.ALL)
    @Relationship(columns = @Relationship.RelationshipColumn(defaultValue = "DEPARTMENT_ID",
            targetColumn = "DEPARTMENT_ID"))
    private Department department;

    // getter and setters
}

Supported operations :

  • ALL , on Insert , Update or Delete , linked entities are affected by the operation on the entity

  • PERSIST , on Insert , linked entities are affected by the insert on the entity

  • REMOVE , on Delete , linked entities are affected by the remove on the entity

  • UPDATE , on Update , linked entities are affected by the update on the entity

If nested links are created , only annotated fields with @Cascade and with matched CascadeType are affected by the operation

1.1.7 Equality And Distinct

In Jaorm , equality between two Entity can be tricky.

Jaorm use Delegation pattern for Entity Mapping and Lazy fetching of linked entities , so a fetched Entity retrieved with Query, Dao or DSL is an instance of selected Entity but doesn’t have the same class at runtime

public class Example {

  @Table(name = "USER")
  public static class User {

    @Id
    @Column(name = "COL1")
    private String id;

    @Column(name = "COL2")
    private String col2;

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
      User user = (User) o;
      return Objects.equals(id, id) && Objects.equals(col2, user.col2);
    }

    @Override
    public int hashCode() {
      return Objects.hash(id, col2);
    }

    // getter and setters
  }

  public void should_return_true_for_same_entity() {
      UserDAO dao = QueriesService.getInstance().getQuery(UserDAO.class);

      User user = new User();
      user.setId("id");
      user.setName("name");

      User found = dao.read(user);

      // Return true, delegate instance underline check equality with two User instance
      boolean foundEquals = found.equals(user);

      // Return false , because found.getClass() != User.class
      boolean userEquals = user.equals(found);
  }
}

For Equals check between two Entities , EntityComparator must be used if equals implementation use getClass() checks

public class Test {

  public static void main(String[] args) {
    User user1 = new User();
    User user2 = new User();
    EntityComprator.getInstance(User.class)
            .equals(user1, user2);
    EntityComprator.getInstance(User.class)
            .equals(Collections.singletonList(user1), Collections.singletonList(user2));
  }
}

For support with distinct() in Java 8 Stream API , user can also use EntityComparator.distinct(Function<? super T,?> function)

public class Test {

    void method() {
        List<User> users = QueriesService.getQuery(UserDao.class)
                .readAll();
        List<User> usersDistinctId =
                users.stream()
                      .filter(EntityComparator.distinct(User::getUserId))
                      .collect(Collectors.toList);
    }
}

1.2 Projections

Jaorm also supports Projections, a subset of an Entity that only store data without supporting persist/update/delete events.

User can use a Projection for store a set of selected columns from a custom query

For defining a new Projection, just annotate a POJO with Projection and Column/Converter for each field that JAORM needs to read/convert.

@Projection
public class MyProjection {

    @Column(name = "ID_COL")
    private BigDecimal id;

    @Column(name = "SUB_NAME")
    private String subName;

    @Column(name = "VALID")
    @Converter(BooleanIntConverter.class)
    private boolean valid;

    private Date other;

    public boolean isValid() {
        return valid;
    }

    public void setValid(boolean valid) {
        this.valid = valid;
    }

    public Date getOther() {
        return other;
    }

    public void setOther(Date other) {
        this.other = other;
    }

    public BigDecimal getId() {
        return id;
    }

    public void setId(BigDecimal id) {
        this.id = id;
    }

    public String getSubName() {
        return subName;
    }

    public void setSubName(String subName) {
        this.subName = subName;
    }
}

1.3 Query Processor

For each interface that contains a method with @Query annotation or is annotated with @Dao annotation, an implementation is generated.

Implemented Method execute sql value in the annotation and return an object for a non-void method. If returned Object is an Entity , Core module create the mapped entity else the first column is returned.

Custom runtime mapper are also supported with the use of TableRow return type.

Query supports different arguments matchers likes :

  • Standard Wildcards

  • A named parameter (es :name)

  • Ordered Wildcards (es: ?1,?2)

  • At Names (es: @name, @name2)

  • No Args (@Query.noArgs must be set to true for this special case)

Query uses parameter name or annotated parameter with Param for retrieve the current value during compile-time

public interface UserDao extends BaseDao<User> {

    @Query(sql = "SELECT * FROM USER WHERE USER_ID = ? AND USERNAME = ?")
    User getUser(String userId, String username);

    @Query(sql = "SELECT * FROM USER WHERE USER_ID = ? AND USERNAME = ?")
    Optional<User> getUserOpt(String userId, String username);

    @Query(sql = "SELECT * FROM USER WHERE USER_ID = ? AND USERNAME = ?")
    List<User> getAllUsers(String userId, String username);

    @Query(sql = "SELECT * FROM USER WHERE USER_ID = :userId
        AND USERNAME = :username AND NAME = :username")
    User getUserNamed(String userId, String username);

    @Query(sql = "SELECT * FROM USER WHERE USER_ID = :userId
        AND USERNAME = :username AND NAME = :username")
    User getUserNamed2(String userId, @Param(name = "USERNAME") String name);

    @Query(sql = "SELECT * FROM USER WHERE USER_ID = ?1 AND USERNAME = ?2 AND NAME = ?1")
    User getUserOrdered(String userId, String username);

    @Query(sql = "SELECT * FROM USER WHERE USER_ID = @userId
        AND USERNAME = @username AND NAME = @username")
    User getUserAtNamed(String userId, String username);

    @Query(sql = "SELECT * FROM USER WHERE USER_ID = @userId
        AND USERNAME = @username AND NAME = @username")
    TableRow getUserTableRow(String userId, String username);

    @Query(sql = "SELECT * FROM USER WHERE USER_ID = @userId
        AND USERNAME = @username AND NAME = @username")
    Optional<TableRow> getUserTableRowOpt(String userId, String username);

    @Query(sql = "SELECT * FROM USER WHERE USER_ID = @userId
        AND USERNAME = @username AND NAME = @username")
    Stream<TableRow> getUserTableRowStream(String userId, String username);

    @Query(sql = "UPDATE USER SET USER_ID = :userId where USERNAME = :username")
    void updateUser(String userId, String username);

    @Query(sql = "DELETE FROM USER", noArgs = true)
    void deleteAll();
}

Annotations on an interface with @Query methods or @Dao annotation are also propagated in the implementation

@Dao
@RequestScoped
public interface MyDao extends BaseDao<User> {}

@RequestScoped
public class MyDaoImpl implements MyDao {} // @RequestScoped is propagated

User can also use custom types that can be converted to sql types if the converter is already used on an Entity

1.2.1 Supported Return Type

Custom generated Queries supports return types like :

  • Void, for Update/Delete SQL

  • Optional<T>, for an optional entity/projection

  • Optional<TableRow>, for an optional mappable row

  • List<T>, for a collection of entities/projections

  • Stream<T>, for a stream of entities/projections

  • Stream<TableRow>, for a stream of mappable rows

  • TableRow, for a mappable row

  • T, for an entity or a projection or a supported accessible type

1.2.2 Fast Table Read

User can also use generated Tables entry point for type-safe read of an Entity.

Tables use a custom DSL for retrieve each Entity annotated with Table

public class Test {

  public static void main(String... args) {
    User user = Tables.USER_TABLE.userId(10).read();
  }
}

1.2.3 SQL in File

User can also use stored SQL located in resources.

Jaorm will read and replace SQL location with full text during compile phase before validation.
@Dao
public interface CustomUserDao {

    @Query(sql = "/mySql.sql")
    Optional<User> getUserOpt(int userId);
}

1.4 Logging

A default NoOp Logger is created for each service that use JaormLogger.

User can redirect and use logged information implementing and registering a custom JaormLoggerHandler

1.5 Pagination

User can retrieve paginated results using default method page in BaseDao<T>.

Page is a 0-index based page that use a lady-fetching strategy for retrieve data using a custom set of sorting orders.

public class Test {

    public static final main(String... args) {
        UserDAO userDAO = QueriesService.getInstance()
                .getQuery(UserDAO.class);

        Page<User> userPage = userDAO.page(0, 10, Collections.emptyList());
    }
}

User can retrieve an optional previous or next page using getNext() and getPrevious() or simply check for hasNext() and hasPrevious()

Jaorm provide a default Page implementation for BaseDao<T> and for DSL module == 2 DSL

DSL Module is an abstraction over simple SQL Queries using a DSL.

It can be combined with Query annotated implementation or used in a stand-alone class.

2.1 Query Builder

QueryBuilder is the entry-point for create custom query and can be used for create complete queries with where, join, order and limit/offset clause

    QueryBuilder.select(MyEntity.class)
                    .join(MyEntityJoin.class, "A")
                        .on(COL_4).eq(COL_2)
                        .on(COL_3).eq(COL_1).orOn(COL_4).ne(COL_2)
                    .where(COL_1).eq(1)
                    .andWhere(COL_3, "A").ne(2)
                    .read();

User can define a custom QueryConfig or use standard implementation with select(Class<T>) method

QueryBuilder will produce :

  • A java.util.Optional<T>, for an optional result, using readOpt method

  • A java.util.List<T>, for a list of result, using readAll method

  • An Entity instance , for a single result, using read method

Input Class in select methods must be a valid Entity or Jaorm will throw a Runtime Exception

2.1.1 Entity Columns

Processor check if DSL is present in the classpath during processing annotation phase and create custom <EntityName>Columns class that contains constant defined columns used for type-safety building for where, join and order by clause

public final class UserColumns {
    public static final SqlColumn<User, Integer> USER_ID = SqlColumn.instance(User.class, "USER_ID", Integer.class);

    public static final SqlColumn<User, Integer> DEPARTMENT_ID = SqlColumn.instance(User.class, "DEPARTMENT_ID", Integer.class);

    public static final SqlColumn<User, String> USER_NAME = SqlColumn.instance(User.class, "USER_NAME", String.class);
}

2.1.2 Query Config

User can configure how DSL should manage different options like:

  • Case Sensitive Like

  • Where Clause Checks

2.1.2.1 Case Insensitive Like

If DSL is configured with case-insensitive configuration

    QueryConfig.builder().caseInsensitive().build()

will transform like conditions from

    SELECT MY_TABLE.COL1, MY_TABLE.COL2 FROM MY_TABLE WHERE (MY_TABLE.COL2 LIKE CONCAT('%',?,'%'))

to

    SELECT MY_TABLE.COL1, MY_TABLE.COL2 FROM MY_TABLE WHERE (UPPER(MY_TABLE.COL2) LIKE CONCAT('%',UPPER(?),'%'))
2.1.2.2 Where Clause Checks

Default DSL WhereChecker for Where Clause will check required not null arguments in where conditions

public class DefaultWhereChecker implements WhereChecker {

    @Override
    public boolean isValidWhere(SqlColumn<?, ?> column, Operation operation, Object value) {
        Objects.requireNonNull(value, "Value can't be null !");
        return true;
    }
}

User can define a custom WhereChecker for check if a Where Clause must be used in the final SQL statement like

public class DefaultChecks {

    private static final QueryConfig DEFAULT_CONFIG = QueryConfig.builder()
            .caseInsensitive()
            .withWhereChecker((sqlColumn, operation, o) -> {
                if (o instanceof String) {
                    return ObjectUtils.isNotEmpty((String) o);
                } else {
                    return Objects.nonNull(o);
                }
            }).build();
}

With previous defined configuration and with current Query Builder

public class TestDSL {

    public static void main(String[] args) {
        QueryBuilder.select(MyEntity.class, DEFAULT_CONFIG)
                    .where(COL_1).eq(null)
                    .where(COL_2).eq("Hello")
                    .read();
    }
}

QueryBuilder will produce SQL

    SELECT MY_TABLE.COL1, MY_TABLE.COL2 FROM MY_TABLE WHERE (MY_TABLE.COL2 = ?)

2.1.3 Where Clause

User can filter selected entity with a subset of where clauses using generated Columns classes for type-safety and different sql operations

public class TestDSL {

    public static void main(String[] args) {
        QueryBuilder.select(MyEntity.class)
                    .where(MyEntityColumn.COL_2).eq("Hello")
                    .read();
    }
}
2.1.3.1 El Expression

Compatibile el expression operations available for create where clause

Method SQL Result

eq(value)

WHERE (MY_TABLE.COL_2 = ?)

ne(value)

WHERE (MY_TABLE.COL_2 <> ?)

lt(value)

WHERE (MY_TABLE.COL_2 < ?)

gt(value)

WHERE (MY_TABLE.COL_2 > ?)

le(value)

WHERE (MY_TABLE.COL_2 ⇐ ?)

ge(value)

WHERE (MY_TABLE.COL_2 >= ?)

2.1.3.2 Standard Expression

Compatibile standard expression operations available for create where clause

Method SQL Result

equalsTo(value)

WHERE (MY_TABLE.COL_2 = ?)

notEqualsTo(value)

WHERE (MY_TABLE.COL_2 <> ?)

lessThan(value)

WHERE (MY_TABLE.COL_2 < ?)

greaterThan(value)

WHERE (MY_TABLE.COL_2 > ?)

lessOrEqualsTo(value)

WHERE (MY_TABLE.COL_2 ⇐ ?)

greaterOrEqualsTo(value)

WHERE (MY_TABLE.COL_2 >= ?)

2.1.3.3 Complex Operations

Where clauses could also be used with complex operations like

Method SQL

in(Iterable)

WHERE (MY_TABLE.COL_2 IN (?,?))

in(Sub query)

WHERE (MY_TABLE.COL_2 IN (SELECT OTHER_TABLE.COL1 FROM OTHER_TABLE))

notIn(Iterable)

WHERE (MY_TABLE.COL_2 NOT IN (?,?))

notIn(Sub query)

WHERE (MY_TABLE.COL_2 IN (SELECT OTHER_TABLE.COL1 FROM OTHER_TABLE))

isNull()

WHERE (MY_TABLE.COL_2 IS NULL)

isNotNull()

WHERE (MY_TABLE.COL_2 IS NOT NULL)

like(LikeType)

WHERE (MY_TABLE.COL_2 LIKE ?)

notLike(LikeType)

WHERE (MY_TABLE.COL_2 NOT LIKE ?)

DSL will automatically generate 1..n wildcards for each element in Iterable

2.1.3.4 Alias Support

User can also use alias for select where clause column

public class TestDSL {

    public static void main(String[] args) {
        QueryBuilder.select(MyEntity.class)
                    .where(MyEntityColumn.COL_2, "B").eq("Hello")
                    .read();
    }
}
2.1.3.5 Complex Clause Generation

With custom method and/or , User can also generate complex where clause like

public class TestDSL {

    public static void main(String[] args) {
        QueryBuilder.select(MyEntity.class)
                    .where(COL_1).eq(1).and(COL_2).ne("3")
                    .orWhere(COL_1).ne(1).and(COL_2).ne("3")
                    .read();
    }
}

that will generate

SELECT MY_TABLE.COL1, MY_TABLE.COL2 FROM MY_TABLE WHERE (MY_TABLE.COL1 = ? AND MY_TABLE.COL2 <> ?) OR (MY_TABLE.COL1 <> ? AND MY_TABLE.COL2 <> ?)

2.1.4 Join Clause

User can filter selected entity with a subset of join clauses using generated Columns classes for type-safety and different sql operations

public class TestDSL {

    public static void main(String[] args) {
         QueryBuilder.select(MyEntity.class)
                    .join(MyEntityJoin.class, "A").on(COL_3).eq(COL_1)
                    .where(COL_1).eq(null)
                    .andWhere(COL_2).eq("Hello")
                    .read();
    }
}
2.1.4.1 El Expression

Compatibile el expression operations available for create join clause

Method SQL Result

eq(Column)

JOIN MY_TABLE ON (MY_TABLE.COL_2 = OTHER_TABLE.COL_1)

ne(Column)

JOIN MY_TABLE ON (MY_TABLE.COL_2 <> OTHER_TABLE.COL_1)

lt(Column)

JOIN MY_TABLE ON (MY_TABLE.COL_2 < OTHER_TABLE.COL_1)

gt(Column)

JOIN MY_TABLE ON (MY_TABLE.COL_2 > OTHER_TABLE.COL_1)

le(Column)

JOIN MY_TABLE ON (MY_TABLE.COL_2 ⇐ OTHER_TABLE.COL_1)

ge(Column)

JOIN MY_TABLE ON (MY_TABLE.COL_2 >= OTHER_TABLE.COL_1)

2.1.4.2 Standard Expression

Compatibile standard expression operations available for create join clause

Method SQL Result

equalsTo(Column)

JOIN MY_TABLE ON (MY_TABLE.COL_2 = OTHER_TABLE.COL_1)

notEqualsTo(Column)

JOIN MY_TABLE ON (MY_TABLE.COL_2 <> OTHER_TABLE.COL_1)

lessThan(Column)

JOIN MY_TABLE ON (MY_TABLE.COL_2 < OTHER_TABLE.COL_1)

greaterThan(Column)

JOIN MY_TABLE ON (MY_TABLE.COL_2 > OTHER_TABLE.COL_1)

lessOrEqualsTo(Column)

JOIN MY_TABLE ON (MY_TABLE.COL_2 ⇐ OTHER_TABLE.COL_1)

greaterOrEqualsTo(Column)

JOIN MY_TABLE ON (MY_TABLE.COL_2 >= OTHER_TABLE.COL_1)

2.1.4.3 Complex Operations

Join clauses could also be used with complex operations like

Method SQL

like(LikeType, Column)

JOIN MY_TABLE ON (MY_TABLE.COL_2 LIKE OTHER_TABLE.COL_1)

notLike(LikeType, Column)

JOIN MY_TABLE ON (MY_TABLE.COL_2 NOT LIKE OTHER_TABLE.COL_1)

2.1.4.4 Alias Support

User can also use alias for multiple joins between different tables

public class TestDSL {

    public static void main(String[] args) {
         QueryBuilder.select(MyEntity.class)
                    .join(MyEntityJoin.class, "A").on(COL_3).eq(COL_1)
                    .join(MyOtherJoin.class, "B").on(COL_5).eq(COL_3, "A")
                    .where(COL_1).eq(null)
                    .andWhere(COL_2).eq("Hello")
                    .read();
    }
}
2.1.4.5 Complex Clause Generation

With custom method on/orOn , User can also generate complex join clause like

public class TestDSL {

    public static void main(String[] args) {
        QueryBuilder.select(MyEntity.class)
                    .join(MyEntityJoin.class, "A")
                        .on(COL_4).eq(COL_2)
                        .on(COL_3).eq(COL_1).orOn(COL_4).ne(COL_2)
                    .where(COL_1).eq(1)
                    .andWhere(COL_3, "A").ne(2)
                    .read()
    }
}

that will generate

SELECT MY_TABLE.COL1, MY_TABLE.COL2 FROM MY_TABLE JOIN MY_TABLE_JOIN AS A ON (A.COL4 = MY_TABLE.COL2) AND (A.COL3 = MY_TABLE.COL1) OR (A.COL4 <> MY_TABLE.COL2) WHERE (MY_TABLE.COL1 = ?) AND (A.COL3 <> ?)

2.1.5 Order Clause

User can also order selected Entity with order clause

Order clause like where and join also supports alias

public class TestDSL {

    public static void main(String[] args) {
        QueryBuilder.select(MyEntity.class)
            .orderBy(OrderType.DESC, COL_1)
            .read();

        QueryBuilder.select(MyEntity.class)
            .orderBy(OrderType.ASC, COL_1, "A")
            .read();
    }
}

2.1.6 Limit/Offset Clause

User can limit and skip selected rows with Offset or Limit clause

Jaorm will only accept value that are greater than 0 for offset and limit

public class TestDSL {

    public static void main(String[] args) {

        QueryBuilder.select(MyEntity.class)
            .limit(10)
            .read();

        QueryBuilder.select(MyEntity.class)
            .offset(10)
            .read();

        QueryBuilder.select(MyEntity.class)
            .offset(10)
            .limit(10)
            .read();
    }
}
2.1.6.1 Vendor Specific Clause

Jaorm will generate custom Offset and Limit clause with vendor-specific modules in classpath during runtime.

User must provide only one sql-specific vendor implementation for entire project

3 Vendor Specific

VendorSpecific use customized modules for modify unsupported SQL Syntax in DSL Module like

  • Like Support

  • Limit Offset Support

  • Read Lock For Update

4 Cache

Cache module implements a key based cache for @Cacheable annotated entities.

Each time a request for a select query is done with @Query implementation, an entity is stored with selected keys. In the followings requests , cached entities are returned if previous request was successful.

@Cacheable
@Table(name = "TABLE_NAME")
public class Entity {

    @Id
    @Column(name = "COLUMN_1")
    private String column1;

    @Column(name = "COLUMN_2")
    private int column2;
}

Default cache implementation is Caffeine but user can override it implementing custom JaormCache and JaormAllCache using an AbstractCacheConfiguration.

If custom implementation is used , user must create an SPI file for <b>CacheService</b> and cache module must be omitted from dependencies.

Default cache implementation require a startup configuration for each entity that are @Cacheable annotated.

public class Startup {

    public void doStartup() {
        CacheService cacheService = CacheService.getInstance();
        MyCacheConfiguration configuration = MyCacheConfiguration.INSTANCE;
        // or use default StandardConfiguration in cache module

        cacheService.setConfiguration(User.class, configuration);
    }
}

5 Transaction

Jaorm natively supports @org.springframework.transaction.annotation.Transactional and @javax.transaction.Transactional for JTA or Spring Managed Beans.

For SE applications, Transactional module can also be used.

Transactional.exec(Callable<V> callable, Class<X> exceptionClass) create a new Transaction and execute Callable<V> as first argument.

If an exception , matched with the second argument is thrown , the exception is propagated, current transaction execute a rollback and close current transaction

If an exception that doesn’t match with second argument , is thrown , an UnexpectedException is thrown , current transaction execute a rollback and close current transaction

If Callable<V> is correctly executed , exec return the returned value if any is provided, current transaction execute a commit and close current transaction

Transaction is propagated on the current Thread and on each child Thread

public class TransactionalTest {

  public static void main(String[] args) {
      Transactional.exec(() -> {
        UserDAO dao = QueriesService.getInstance().getQuery(UserDAO.class);

        User user = new User();
        user.setId("id");
        user.setName("name");

        dao.insert(user);

        // After this , commit is done on current transaction
      }, Exception.class);
  }
}

6 Lombok

Jaorm offers supports for Lombok annotations such @Getter , @Setter or @Data

Core Module only use custom generated getter and setter from Lombok if Jaorm Lombok is provided as dependency

@Data
@Table(name = "LOMB")
public class Lombok {

    @Id
    @Column(name = "LOMBOK_ID")
    private int lombokId;

    @Column(name = "LOMBOK_NAME")
    private String name;

}