View on GitHub

JLibs

Common Utilities for Java

J2EE DAO Pattern made easier

Dependencies

<dependency>
    <groupId>in.jlibs</groupId>
    <artifactId>jlibs-jdbc</artifactId>
    <version>2.2.1</version>
</dependency> 

<dependency>
    <groupId>in.jlibs</groupId>
    <artifactId>jlibs-jdbc-apt</artifactId>
    <version>2.2.1</version>
    <optional>true</optional>
</dependency> 

jlibs-jdbc-apt contains annotation processor and is required only at compile time

Annotating POJO

Create a POJO and map it to database using annotations

package sample;

import jlibs.jdbc.annotations.*;

@Table
public class Employee{
    @Column(primary=true)
    public int id;
    
    @Column public String firstName;
    @Column public String lastName;
    @Column public int age;
    @Column public int experience;
    @Column public int salary;
}

@Table Annotation

@Table
public class Employee{

@Table annotation says that, the class Employee is mapped to employees table in database.

The name of table is derived from class name by joining words with underscore and converting to lowercase and plural.

You can also explicitly specify the table name as below:

@Table(name="employees")
public class Employee{

@Column Annotation

@Column public String firstName;

here the property firstName is mapped to first_name in employees table.

We call firstName as column-property (i.e, property that is mapped to table column).

The name of the column is derived from property name by joining words with underscore and converting to lowercase

You can also explicitly specify the column name as below:

@Column(name="first_name")
public String firstName;

The primary key can be specified by specifying primary=true as below:

@Column(primary=true)
public int id;

If primary key is combination of multiple columns, specify primary=true on all those column properties;

If the primary key is auto-incremented value in database, then specify auto=true as below:

@Column(primary=true, auto=true)
public int id;

@Column annotation can also be applied on getXXX or setXXX method rather than on field as below:

private String firstName;
@Column(name="first_name")
public String getFirstName(){
    return firstName;
}
public void setFirstName(String firstName){
    this.firstName = firstName;
}

If you are using database specific sql type then set nativeType=true:

import org.postgresql.util.PGmoney;

@Column(nativeType=true)
public PGmoney salary;

A class with @Table annotation must have atleast one member with @Column annotation.
Otherwize you will get compilation error.

Invalid Column-Property Type

Only the java types which have sql type mapping can be used for column-property. For example:

@Column Properties defautls;

gives compilation error java.util.Properties has no mapping SQL Type.

Later will will know, how to use custom java types.

Duplicate Column-Property Type

If two column-properties are mapped to same column as below:

@Column(name="first_name")
public String firstName;

@Column(name="first_name")
public String lastName;

gives compilation error. This can happen when you used copy/paste and forgot to change column name

Validating POJOs with Database

Sometimes you might misspell table or column names in POJO definition.
To avoid such mistakes, during compilation time you can ask to validate POJO definitions against database.

for this you specify database in package-info.java as follows:

@Database(driver=com.mysql.jdbc.Driver.class , url="jdbc:mysql://localhost/test")
package foo.model;

import jlibs.jdbc.annotations.Database;

@Database annotation gives all the information required to make database connection

If your database requires authentication, then use:

@Database(driver=com.mysql.jdbc.Driver.class , url="jdbc:mysql://localhost/test", user="santhosh", password="stillThinking")

here @Database annotation is defined on package foo.model. So this database is used to validate all POJOs in package foo.model and its sub-packages.

the following are validated in POJO definition at compile time:

when new columns found in database which are not defined in POJO, it simply produces compilation warning.
If you want to fail compilation in such scenarios, specify failOnMissingColumns attribute as follows:

@Database(driver=com.mysql.jdbc.Driver.class , url="jdbc:mysql://localhost/test", failOnMissingColumns=true)

DataAccessObject (DAO)

Let us create DAO:

package sample;

import jlibs.jdbc.*;
import org.apache.commons.dbcp.BasicDataSource;

public class DB{
    public static final BasicDataSource DATA_SOURCE;
    public static final DAO<Employee> EMPLOYEES;
    
    static{
        DATA_SOURCE = new BasicDataSource();
        DATA_SOURCE.setUrl("jdbc:mysql://localhost/test");
        JDBC jdbc = new JDBC(DATA_SOURCE);
        EMPLOYEES = DAO.create(Employee.class, jdbc);
    }
}
JDBC jdbc = new JDBC(DATA_SOURCE);

JDBC is the class which executes the sql statements against the specified DATA_SOURCE.
By default JDBC finds the quoteString from DatabaseMetaData and uses it to quote identifiers
like table name, column name etc. To explicitly set quoteString:

JDBC jdbc = new JDBC(DATA_SOURCE, "\"");

If the second argument is null, then identifiers are not quoted.

To print sql queries being executed, set system property jlibs.jdbc.debug to true.
they are logged to System.out. This will be useful for debugging.

We created constants to all DAO’s in class DB:

EMPLOYEES = DAO.create(Employee.class, jdbc);

The above line creates DAO for Employee pojo on given javax.sql.DataSource.
jlibs.jdbc.DAO is an abstract base class for all DAO’s.
It has common methods like insert(...), delete(...) etc that are expected in all DAO’s.

When you compile Employee class, _EmployeeDAO.java is generated and compiled.
This class extends jlibs.jdbc.DAO<Employee> and implements the abstract methods.

Inserting record

import static sample.DB.*;

Employee emp = new Employee();
emp.id = 1;
emp.firstName = "santhosh";
emp.lastName = "kumar";
emp.age = 28;

EMPLOYEES.insert(emp);

let us say id of employee is defined as auto-increment as below:

@Column(primary=true, auto=true)
public int id;

then after inserting employee, emp.id gives the actual id assigned by database:

Employee emp = new Employee();
emp.firstName = "santhosh";
emp.lastName = "kumar";
emp.age = 28;

EMPLOYEES.insert(emp);
System.out.println("ID of new employee: "+emp.id);

Updating record

emp.age = 27;
EMPLOYEES.update(emp);

Listing records

List<Employee> employees = EMPLOYEES.all();
Employee emp = EMPLOYEES.first(); // get first employee from table
</code></pre>
Deleting records:
<pre><code>if(!EMPLOYEES.delete(emp))
    System.out.println("employee with id '"+emp.id+"' doesn't exist"); 

Deleting all records

int count = EMPLOYEES.delete();
System.out.println(count+" employees deleted"); 

Upsert record

Upsert means: update if exists, otherwise insert

import static sample.DB.*;

Employee emp = new Employee();
emp.id = 1;
emp.firstName = "Santhosh";
emp.lastName = "Kumar";
emp.age = 26;

EMPLOYEES.upsert(emp);

The above code updates the values of employee with id 1.
If employee with id 1 doesn’t exist, it inserts new record.

Custom Queries

Except upsert(...) all above methods in DAO support custom queries.

To find first 10 employees whose age is less than given age:

int age = ...;
List<Employee> employees = employeeDAO.top(10, "where age<?", age);

if first argument to top(...) is zero, then it fetches all rows.

All DAO methods throw jlibs.jdbc.DAOException.
This is runtime exception wrapping SQLException.

int id = (Integer)EMPLOYEES.insert("(first_name, last_name) values(?, ?)", "james", "gosling");
int numberOfEmployeesUpdated = EMPLOYEES.update("set age=? where last_name=?", 25, "kumar");
int numberOfEmployeesDeleted = EMPLOYEES.delete("where age < ?", 20);
Employee emp = EMPLOYEES.first("where id=?", 5);
List<Employees> youngEmployees = EMPLOYEES.all("where age < ?", 21);

custom queries like above are good, there are are no compile time validations.
let us say, if less aruments are specified than expected by the query, this mistake is not caught at compile time.

Let us see how to find such mistakes at compile time.

@Select Annotation

Create a separate DAO for Employee, and add abstract methods for each custom query you want:

package sample;

import jlibs.jdbc.*;
import javax.sql.*;

public abstract class EmployeeDAO extends DAO<Employee>{
    public EmployeeDAO(DataSource dataSource, TableMetaData table){
        super(dataSource, table);
    }
    
    @Select
    public abstract Employee findByID(int id);
}

NOTE:

the abstract method findById is annotated with @Select.
this says that this method does select query on database.

The method signature should follow some rules.
When these rules are violated, you will get compilation error.

Return Type:

If you want to select single employee use return type as Employee.
If you want to select list of employees use return type as List<Employee>.

Parameters:

The parameter names should match the column-proprty name in your POJO,
and their type should match with the type of that column-property in POJO.

For example, here the method findByID(...) takes one parameter int id.
Its name and type matches with the column-property Employee.id in POJO.

Method Name:

There is no restriction on method name.
i.e you can name the method of your choice.

Exceptions:

The abstract query methods can only throw jlibs.jdbc.DAOException which is runtime exception.

We have seen that, when you compile Employee.java, the _EmployeeDAO.java generated extends DAO<Employee>.
Now we have to say that _EmployeeDAO.java generated should extend EmployeeDAO that we have written.
This is done by specifing extend attribute of @Table annotation:

@Table(name="employees", extend=EmployeeDAO.class)
public class Employee{

Now the generated _EmployeeDAO class extends our EmployeeDAO class and implements the abstract query method findByID(...)

If you open generated _EmployeeDAO.java, you can see the following implementation generated:

@Override
public jlibs.jdbc.Employee findByID(long id){
    return first("where id=?", id);
}

To find employees by last name and age, you define abstract query method as below:

@Select
public abstract List<Employee> findByLastNameAndAge(String lastName, int age);

and the generated implementation will be:

@Override
public java.util.List<jlibs.jdbc.Employee> findByLastNameAndAge(java.lang.String lastName, int age){
    return all("where last_name=? and age=?", lastName, age);
}

If you want to find all employees with complex conditions:

@Select(sql="where #{age} between ${fromAge} and ${toAge} or #{lastName}=${lastName}")
public List<Employee> findByAgeOrLastName(int fromAge, int toAge, String lastName);

here we are specifying the sql query in @Select annotation.

#{column-property} in query will be replaced by its columnName
i,e #{age} is replaced by age and #{lastName} is replaced by last_name

${parameter-name} in query will be replaced by its parameter value
i,e ${fromAge} is replaced by the value of fromAge passed to the method.

the generated implementation for the above method will be:

@Override
public List<Employee> findByAgeOrLastName(int fromAge, int toAge, String lastName){
    return all("where age between ? and ? or last_name=?", fromAge, toAge, lastName);
}

Let us say we did a mistake as below:

@Select(sql="where #{age} between ${fromAge} and ${toAge} or #{lastName}=${lastName}")
public List<Employee> findByAgeOrLastName(int fromAge, String toAge, String lastName);

here the second argument toAge is specified as String. But it is supposed to be int.
But when you compile above method, it generates the following implementation without any compile time error:

@Override
public List<Employee> findByAgeOrLastName(int fromAge, String toAge, String lastName){
    return all("where age between ? and ? or last_name=?", fromAge, toAge, lastName);
}

to add compile time validations to verify that the types are matching with their column types:

@Select(sql="where #{age} between ${fromAge} and ${(age)toAge} or #{lastName}=${lastName}")
public List<Employee> findByAgeOrLastName(int fromAge, String toAge, int lastName);

Now on compiling the above method you get following compile time error:

toAge must be of type int/java.lang.Integer

Notice that, here rather than simply using ${toAge} in query, we specified ${(age)toAge}
i.e, ${(column-property)parameter-name} will verify that the type of parameter matches with
the type of column property specified, during compile time.

@Insert Annotation

@Insert
public abstract void insert(int id, String firstName);

The parameter names should match the column-proprty name in your POJO,
and their type should match with the type of that column-property in POJO.

the generated implementation will be:

@Override
public void insert(int id, java.lang.String firstName){
    insert("(id, first_name) values(?, ?)", id, firstName);
}

The method can return either void or the POJO. for example:

@Insert
public Employee insert(long id, String firstName, String lastName);

the generated implemenation will be:

@Override
public jlibs.jdbc.Employee insert(long id, java.lang.String firstName, java.lang.String lastName){
    insert("(id, first_name, last_name) values(?, ?, ?)", id, firstName, lastName);
    return first("where id=?", id);
}

@Delete Annotation

@Delete
public abstract int delete(String firstName, String lastName);

The parameter names should match the column-proprty name in your POJO,
and their type should match with the type of that column-property in POJO.

The return value will be the number of employees deleted;
The method can return void, if you are not interested in that number.

the generated implementation will be:

@Override
public int delete(java.lang.String firstName, java.lang.String lastName){
    return delete("where first_name=? and last_name=?", firstName, lastName);
}

If you want to use complex conditions:

@Delete(sql="where #{age} between ${(age)fromAge} and ${(age)toAge} or #{lastName}=${(lastName)lastN}")
public abstract int delete(int fromAge, int toAge, String lastN);

the generated implementation will be:

@Override
public int delete(int fromAge, int toAge, java.lang.String lastN){
    return delete("where age between ? and ? or last_name=?", fromAge, toAge, lastN);
}

@Update Annotation

@Update
public abstract int update(int age, String where_firstName, String where_lastName);

The parameter names should match the column-proprty name in your POJO,
and their type should match with the type of that column-property in POJO.
the parameters to be used in WHERE condition should be prefixed with where_.

The return value will be the number of employees updated;
The method can return void, if you are not interested in that number.

the generated implementation will be:

@Override
public int update(int age, java.lang.String where_firstName, java.lang.String where_lastName){
    return update("set age=? where first_name=? and last_name=?", id, age, where_firstName, where_lastName);
}

If you want to use complex conditions:

@Update(sql="set #{lastName}=${(lastName)lastN} where #{age} between ${(age)fromAge} and ${(age)toAge}")
public abstract int updateLastName(int fromAge, int toAge, String lastN);

the generated implementation will be:

@Override
public int updateLastName(int fromAge, int toAge, java.lang.String lastN){
    return update("set last_name=? where age between ? and ?", lastN, fromAge, toAge);
}

@Upsert Annotation

This is similar to @Update except that the method should return void.

If the record is found in database it is updated, otherwise new record is inserted.

WHERE Condition

various sql operators can be specified by using prefixes for method parameters.

for example to find all employees whose age is less than specified age:

@Select
public abstract List<Employee> findYoungEmployees(int lt_age);

here the prefix is lt_ and column property is age.
the prefix lt_ says to use the < sql operator on age column

to delete all employees between specified ages:

@Delete
public abstract int deleteEmployeesByAge(int from_age, int to_age);

to update experience of employees between specified ages:

@Update
public abstract int updateEmployeesByAge(int from_age, int to_age, int experience);

The supported operators and the prefixes to use are:

| Prefix     | Operator        |
|------------|----------------:|
| where_     | = ?             |
| eq_        | = ?             |
| ne_        | <> =            |
| lt_        | < ?             |
| le_        | <= ?            |
| gt_        | > ?             |
| ge_        | >= ?            |
| like_      | LIKE ?          |
| nlike_     | NOT LIKE ?      |
| from_, to_ | BETWEEN ? AND ? |

This technique of using prefixes, will avoid writing custom queries to large extent.

Selecting Single Column

Some times you might want value of a specific column rather than values of all columns.

to find first name of employee of given id:

@Select(column="firstName")
public abstract String getFirstName(int id);

Notice that the value of column attribute is firstName (not first_name).
i.e value of column attribute is name of the column property to be selected.

if there is no employee with the specified id, the above method returns null.
if the first name of employee with specified id is NULL in database, the above method returns null

in order to differentiate above to cases:

@Select(column="firstName", assertMinimumCount=1)
public abstract String getFirstName(int id);

the attribute assertMinimumCount ensures that there are atleast specified number of records.
Now the above method throws jlibs.jdbc.IncorrectResultSizeException, if there is no employee with given id.
jlibs.jdbc.IncorrectResultSizeException is subclass of jlibs.jdbc.DAOException

the assertMinimumCount can also be used when selecting entire records:

@Select(assertMinimumCount=5)
public abstract List<Employee> findByAge(int from_age, int to_age);

if you want to find first names of employees:

@Select(column="firstName")
public abstract List<String> findFirstNames(int from_age, int to_age);

Selecting Expression

to find number of employees with age greater than specified:

@Select(expression="count(*)")
public abstract int countOlderEmployees(int gt_age);

to find sum of salaries of all employees:

@Select(expression="sum(#{salary})")
public abstract int totalSalary();

the method can also return List:

@Select(expression="#{age}-#{experience}")
public abstract List<Integer> calculateEquation1();

Dynamic SQL

@Select(ignoreNullConditions=true)
public abstract List<Employee> search(String firstName, String lastName, Integer age, Integer experience);

ignoreNullConditions=true says that don’t include the conditions whose values are null.
i.e

EMPLOYEES.search(null, null, 28, null); // search by age
EMPLOYEES.search(null, null, 35, 5); // search by age and experience

the generated implementation will be as below:

@Override
public List<Employee> search(String firstName, String lastName, Integer age, Integer experience){
    java.util.List<String> __conditions = new java.util.ArrayList<String>(4);
    java.util.List<Object> __params = new java.util.ArrayList<Object>(4);
    if(firstName!=null){
        __conditions.add("first_name=?");
        __params.add(firstName);
    }
    if(lastName!=null){
        __conditions.add("last_name=?");
        __params.add(lastName);
    }
    if(age!=null){
        __conditions.add("age=?");
        __params.add(age);
    }
    if(experience!=null){
        __conditions.add("experience=?");
        __params.add(experience);
    }
    String __query = null;
    if(__conditions.size()>0)
        __query = " WHERE " + jlibs.core.lang.StringUtil.join(__conditions.iterator(), " AND ");
    return all(__query, __params.toArray());
}

ignoreNullConditions attribute is supported on @Select, @Delete and @Update

Custom Java Types

let us say employees table in database has column named grade of type number.
You don’t want to use int for grade property in Employee class, rather you want
to use following enum.

public enum Grade{
    JUNIOR, SENIOR, LEAD, MANAGER
}

because Grade is not one of the java types supported by JDBC,
you need to create a mapping as below:

import jlibs.jdbc.JDBCTypeMapper;

public class GradeTypeMapper implements JDBCTypeMapper<Grade, Integer>{
    @Override
    public Grade nativeToUser(Integer nativeValue){
        if(nativeValue==null)
            return null;
        else
            return Grade.values()[nativeValue];
    }

    @Override
    public Integer userToNative(Grade userValue){
        if(userValue==null)
            return null;
        else
            return userValue.ordinal();
    }
}

The above class maps Grade with Integer.
Grade is called User Type.
Integer is called Native Type.

Now in Employee class you need to specify grade property to use above mapping:

@Table
public class Employee{
    ...
    
    @Column
    @TypeMapper(mapper=GradeTypeMapper.class, mapsTo=Integer.class)
    public Grade grade;
}

if you are using custom queries, you must specify column-property for parameter which is non-native type:

@Select(expression="count(*)", sql="WHERE #{grade}=${(grade)grade} AND #{age}<${age}")
public abstract int countByGradeAndAge(Grade grade, int age);

Notice that we used ${(grade)grade} in query rather than ${grade}.
otherwise, you will get following compilation error:

the column property must be specified for parameter grade in query.

Sorting by Column

to sort employees by experience:

@Select(orderBy=@OrderBy(column="experience", order=Order.DESCENDING))
public abstract List<Employee> youngEmployees(int le_age);

the order attribute is optional and defaults to Order.ASCENDING.

to sort by multiple columns:

@Select(orderBy={
    @OrderBy(column="experience", order=Order.DESCENDING),
    @OrderBy(column="age", order=Order.DESCENDING)
})
public abstract List<Employee> youngEmployees(int le_age);

Paging

Suppose you are searching employees in databse, the number of employees maching your criteria might be large.
You don’t want list all employees, but you want to page though the search results:

@Select(ignoreNullConditions=true, pageBy=@OrderBy(column="id", order=Order.DESCENDING))
public abstract Paging<Employee> pageById(String firstName, String lastName, Integer age, int experience);

pageBy attribute specifies that you want to page through results.
when pageBy attribute specified the method return type should be jlibs.jdbc.Paging.

In order to page through the results, you need to sort the results by set of columns whose combination of
values is unique. In above method, we are paging by column property id and id is unique for each employee.

Now you can do paging as follows:

Paging<Employee> paging = EMPLOYEES.pageById(null, null, 35, 5);
Page<Employee> page = paging.createPage(10); // page size is 10

int totalRowCount = page.getTotalRowCount();
int totalPageCount = getTotalPageCount();

to Navigate the page:

List<Employee> firstPage = page.navigate(Page.Action.FIRST);
List<Employee> secondPage = page.navigate(Page.Action.NEXT);
List<Employee> lastPage = page.navigate(Page.Action.LAST);
List<Employee> lastButOnePage = page.navigate(Page.Action.PREVIOUS);

Page.Action is an enum.
to find whether you can navigation in given direction:

boolean enableNextButton = page.canNavigate(Page.Action.NEXT);

Note that, if the total number of rows is zero, you can’t navigate in any direction.
page.navigate(...) throws IllegalArgumentException if the specified navigation action is not possible.

page.getIndex() returns the index of current page. The indexing starts from zero.
page.getIndex() returns -1 if the page is created but not navigated yet.

Note that you can’t jump to a given page.

Paging in Servlet/JSP

You can place the page object in session and use it. But if user is paging simultaniously
by different crieterias, you will end up multiple page objects in session and becomes impossible
to know which page object from session to use.

In order to overcome this situation, you can pass the information required to create current page from request to request using POST.

the information required to construct page at given index are:

page.getIndex();
page.getTotalRowCount();
page.getFirstRow(); // returns Employee
page.getLastRow(); // returns Employee

all above methods have their corresponding set methods.
For first and last rows, it is enough to fill only properties of the Employee object which are used for paging.
i.e Employee.id for above example.

Transactions

To run multiple statements in a transaction:

import static sample.DB.*;

Object returnValue = TransactionManager.run(DATA_SOURCE, new Transaction<Object>(){
    @Override
    public Object run(Connection con) throws SQLException{
        // invoke some DAO/DB actions
        // return something
    }
});

If Transaction.run(...) throws exception, then the current transaction is rolled back.

What happens when transactions are nested as below:

public void method1(){
    Object returnValue = TransactionManager.run(DATA_SOURCE, new Transaction<Object>(){
        @Override
        public Object run(Connection con) throws SQLException{
            // invoke some DAO/DB actions
            method2();
            // return something
        }
    });
}

public void method2(){
    Object returnValue = TransactionManager.run(DATA_SOURCE, new Transaction<Object>(){
        @Override
        public Object run(Connection con) throws SQLException{
            // invoke some DAO/DB actions
            // return something
        }
    });
}

here both both method1() and method1() are using transactions. Notice that method1() is calling method2().
so when method1 is invoked, method2() will run in the same transaction that is started by method1().

Pros of this library