Persist is a Java-based ORM/DAO tool. It provides only the minimal amount of functionalities necessary to map
objects or maps from database queries and to statement parameters.
Persist works around a java.sql.Connection
object. This means that it does not care about customer query
languages (it uses plain SQL with placeholders, as PreparedStatement
objects use), connection pool handling,
transaction handling (for the most part), and so on. This also means it is very flexible, and can be integrated with any
code that depends on JDBC (including code that already use another ORM/DAO tool).
Persist does not require explicit mappings from POJOs to database tables. As long as there is some sort of naming
conventions that relate database names with POJO names, Persist will require virtually no mappings. It can, however, be
instructed to map Java classes and fields to database tables and columns using annotations.
Persist requires no singletons, no ThreadLocal’s, no global objects and no configuration files. It has
no external dependencies (although it will use Log4j, if available). It has a very small, robust and straightforward
codebase, which can be easily debugged in case you face an unforeseen problem. It is actively tested (with a high code
coverage) for MySQL, PostgreSQL, H2/HSQLDB, Derby, Oracle, DB2 and MS SQL Server. It imposes a very small overhead to
comparable plain JDBC operations, making it very attractive for high performance and/or low footprint applications.
Persist is distributed under a BSD license.
A Persist engine only requires a java.sql.Connection
to be created:
Persist persist = new Persist(connection);
Persist supports several different mapping strategies:
By default, if no annotations specify a given class should not be mapped to a table, Persist will try to find a
table that matches that class and create a mapping between fields and columns.
// inserts a new customer (the class _Customer_ is mapped to the table _customer_ automatically) persist.insert(customer); // reads a customer by its primary key Customer c = persist.readByPrimaryKey(Customer.class, 42); // retrieves customers using a custom query (note the usage of varargs) List<Customer> list = persist.readList(Customer.class, "select * from customer where id > ?", 10); // fetch all customers and assign the ResultSet to an Iterator ResultSetIterator<Customer> allCustomersIterator = persist.readIterator(Customer.class, "select * from customer"); // release database resources allCustomersIterator.close();
If a class is annotated with @NoTable
, Persist will not try to map it to a table, and the class will
only be able to hold data produced by queries.
@NoTable class QueryData { private int count; private String concatName; public long getCount() { return count; } public void setCount(long count) { this.count = count; } public String getConcatName() { return concatName; } public void setConcatName(String concatName) { this.concatName = concatName; } } QueryData qd1 = persist.read(QueryData.class, "select 1 as count, 'hello' as concat_name from dual"); QueryData qd2 = persist.read(QueryData.class, "select 2 as counts, null as concatnames from dual");
Map
instances can be used to hold data from queries. Persist will convert values returned from the query to Java
types. Keys in the table are the names of the columns returned in lower case.
// fetch a customer using a custom query and return the result as a map Map<String,Object> customerMap = persist.readMap("select * from customer where id=?", 10); // fetch all customers and result the results as Map instances in a List List<Map<String,Object>> customerMapList = persist.readMapList("select * from customer"); // fetch all customers and assign the ResultSet to an Iterator which maps rows to Map instances ResultSetIterator<Map<String, Object>> allCustomersIterator = persist.readMapIterator("select * from customer"); // release database resources allCustomersIterator.close();
If a query returns a single column, Persist can map data directly into primitive types (either single values or lists):
// return customer name as String String name = persist.read(String.class, "select name from customer where id=?", 55); // fetch all customer id's as a list of integers List<Integer> ids = persist.readList(int.class, "select id from customer");
Arbitrary queries that return no data can be easily executed.
// execute arbitrary SQL with parameters persist.executeUpdate("delete from customer where id in (?,?)", 10, 20);
For the POJO mapping strategies, persist can map names either using explicit annotations or through a name
guesser which translates class and field names to table and column names. A name guesser is a class that implements an
interface with a single method, and provide a generic, uniform way of translating names from the database schema to the
Java name conventions.
That’s almost everthing you need to know before using Persist! To get a better understanding on its internals,
please consult the following sections.
Persist only requires a java.sql.Connection
object to be created:
Persist persist = new Persist(connection);
Internally, Persist will maintain a cache for all mapped objects it interacts with. Caches exist on a classloader
basis. If an application has to deal with connections with different databases, different mapping caches must be used.
To specify different caches, use a cache name in the constructor:
// Create a persist instance for MySQL using the default cache Persist persistMysql = new Persist(connectionMysql); // Create a persist instance for Oracle using the "oracle" cache name Persist persistOracle = new Persist("oracle", connectionOracle);
If Persist can find Log4J in the classpath, it will use it. The following channels are used, all in debug
mode:
persist.engine
persist.parameters
persist.results
persist.profiling
There are a few annotations that control the mapping behavior from classes to database tables:
@Table
can be associated with a class and specify the name of the table that class is mapped to@Column
can be associated with a setter or getter of a field and specify the name of the column to be
associated with that field, and/or specify if the field is auto-incremented by the database upon insertion.@NoTable
can be associated with a class to specify the class should not be mapped to a table in the
database. Classes annotated with@NoTable
can only be used to hold data from queries.@NoColumn
can be associated with a setter or getter of a field and specify it should not be mapped to
a column in the database (by default, Persist attempts to map all fields of a given class to columns in the table
associated with their class).
By default, if Persist is given a class, it will try to map it to a database table. To do so, it relies either on
explicit annotations (such as @Table
and @Column
) or name guessers which are configurable and
determine a global translation mechanism between class and field names to table and column names.
Consider the following table definition and its associated bean
create table customer ( id int auto_increment, name varchar(255), primary key (id) )
class Customer { private int id; private String name; @Column(autoIncrement=true) public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
class Customer
and its fields don’t specify annotations to explicitly define which tables and columns
should be used. Therefore, when Persist tries to map this class, it will use a name guesser. Name guessers are
responsible for programatically converting class and field names to table and column names, using whichever convention
is in place.
Since no name guesser was specified, the DefaultNameGuesser
will be used. The DefaultNameGuesser
converts class and field names in the form CompoundName
to this list of guessed names: [compound_name,
compound_names, compoundname, compoundnames]
While performing the automatic mapping for the Customer
class above, Persist would try to find any of
those tables in the database: [customer, customers]
. Since the table customer
exists, it picks it for
the mapping and start mapping fields from Customer
to columns in customer
using the same approach.
During the process of mapping columns to fields, Persist stores information about which columns are primary keys
and which ones are auto-incremented upon insertion (this must be specified using @Column(autoIncrement=true)
since there’s no deterministic way of doing this automatically).
This mapping process only happens once (per class per classloader). Persist stores mappings for each class it
interacts with in an internal cache
After a mapping is created, Persist can perform CRUD operations directly on instances of the Customer
POJO. To illustrate the whole process, consider the following code
// create a new customer instance Customer customer = new Customer(); customer.setName("a new customer"); // fetch a customer using its primary key Customer c = persist.readByPrimaryKey(Customer.class, 10);
The moment persist has contact with Customer
, it will build a mapping automatically and cache it. With
the mapping, it will know that the Customer
class is mapped to the customer
table, and that id
is the primary key in that table. With that information, Persist can issue a select
SQL statement querying for
all columns in customer
having the specified primary key (id
).
Other CRUD operations can be used directly as well
// insert persist.insert(customer); // update persist.update(customer); // delete persist.delete(customer);
Important note: Persist can only perform readByPrimaryKey
, update
and delete
operations for classes mapped to tables that have primary keys. insert
and all the read
operations can work on any POJO mapped on any table.
Persist supports several different ways of reading data from mapped tables:
// fetch a single customer using a custom query Customer customer = persist.read(Customer.class, "select * from customer where id = 10"); // fetch all customers List<Customer> allCustomersList = persist.readList(Customer.class); // fetch a set of customers using a custom query List<Customer> customersList = persist.readList(Customer.class, "select * from customer where id < ?", 100); // fetch all customers using a custom query and assign the ResultSet to an Iterator which maps rows to Customer instances Iterator allCustomersIterator = persist.readIterator(Customer.class, "select * from customer where id in (?,?)", 10, 20);
Persist will respect the Java types of the fields on a given POJO as much as it can while retrieving data from
ResultSet’s. Furthermore, Persist can perform type conversions to/from query parameters and ResultSet columns.
The following tables depict the ResultSet.get and PreparedStatement.set methods used for each Java type:
Java type | ResultSet.get method | PreparedStatement.set method |
---|---|---|
Boolean/boolean | getBoolean | setBoolean |
Byte/byte | getByte | setByte |
Short/short | getShort | setShort |
Integer/int | getInt | setInt |
Long/long | getLong | setLong |
Float/float | getFloat | setFloat |
Double/double | getDouble | setDouble |
Character/char | getString | setString |
Character[]/char[] | getString | setString |
Byte[]/byte[] | getBytes | setBytes |
String | getString | setString |
Enum | getString | setString p |
java.math.BigDecimal | getBigDecimal | setBigDecimal |
java.io.Reader | getCharacterStream | setCharacterStream |
java.io.InputStream | getBinaryStream | setBinaryStream |
java.util.Date | getTimestamp | setTimestamp |
java.sql.Date | getDate | setDate |
java.sql.Time | getTime | setTime |
java.sql.Timestamp | getTimestamp | setTimestamp |
java.sql.Clob | getClob | setClob |
java.sql.Blob | getBlob | setBlob |
POJOs that are annotated with NoTable
can only be used to hold data from queries. Mapping for classes
annotated with NoTable
is performed using these rules:
- Class names won’t affect the mapping
- If a field contains a
@Column(name="...")
annotation, then only the specified column name will be
used for that field - Otherwise, all column names returned by the name guesser will be associated to the field
- If more than one field have conflicting table names (either from
@Column
annotations or from guessed
names), Persist will throw an exception while trying to use the class
To illustrate how this works, consider the following class:
class QueryData { private int count; private String concatName; public long getCount() { return count; } public void setCount(long count) { this.count = count; } public String getConcatName() { return concatName; } public void setConcatName(String concatName) { this.concatName = concatName; } }
Persist would create the following mapping for this class:
Column names | Field name |
---|---|
count, counts | count |
concat_name, concat_names, concatname, concatnames | concatName |
Some examples of how this would work using dummy queries:
QueryData qd1 = persist.read(QueryData.class, "select 1 as count, 'hello' as concat_name from dual"); QueryData qd2 = persist.read(QueryData.class, "select 2 as counts, null as concatnames from dual");
Type conversions are performed using the same conversion table as POJOs mapped to tables use.
Query results can be mapped directly to java.util.Map
instances, using the readMap-prefixed methods.
Keys in the map are the names of the columns in lower case, and values are fetched from the ResultSet.
// fetch a customer using a custom query and return the result as a map Map<String,Object> customerMap = persist.readMap("select * from customer where id=?", 10); // fetch all customers and result the results as Map instances in a List List<Map<String,Object>> customerMapList = persist.readMapList("select * from customer"); // fetch all customers and assign the ResultSet to an Iterator which maps rows to Map instances Iterator allCustomersIterator = persist.readMapIterator("select * from customer");
Values are retrieved from the ResultSet according with their SQL types (as defined in java.sql.Types), so that
for each SQL type Persist will request the value according with a pre-defined Java type, as specified in the following
table:
SQL type | ResultSet.get method |
---|---|
ARRAY | getArray |
BIGINT | getLong |
BIT | getBoolean |
BLOB | getBytes |
BOOLEAN | getBoolean |
CHAR | getString |
CLOB | getString |
DATALINK | getBinaryStream |
DATE | getDate |
DECIMAL | getBigDecimal |
DOUBLE | getDouble |
FLOAT | getFloat |
INTEGER | getInt |
JAVA_OBJECT | getObject |
LONGVARBINARY | getBytes |
LONGVARCHAR | getString |
NULL | getNull |
NCHAR | getString |
NUMERIC | getBigDecimal |
OTHER | getObject |
REAL | getDouble |
REF | getRef |
SMALLINT | getInt |
TIME | getTime |
TIMESTAMP | getTimestamp |
TINYINT | getInt |
VARBINARY | getBytes |
VARCHAR | getString |
100 (Oracle specific) | getFloat |
101 (Oracle specific) | getDouble |
Persist can map query results having a single column directly to primitive Java types (such as int, Double,
String, etc.), either as single values or lists.
// return customer name as String String name = persist.read(String.class, "select name from customer where id=?", 55); // fetch all customer id's as a list of integers List<Integer> ids = persist.readList(int.class, "select id from customer");
Type conversions are performed using the same conversion table as POJOs mapped to tables use.
In order to run the tests for all the supported databases you’ll need the following databases installed:
- MySQL
- PostgreSQL
- H2/HSQLDB
- Derby
- Oracle
- DB2
- MS SQL Server
One easy way to test against all databases supported in Linux (all the ones listed above except MS SQL Server) is to build a VM (using VMWare, VirtualBox, Parallels, KVM, Xen, etc.).
In order to build a Ubuntu VM with the databases ready for testing:
- Install Ubuntu Server using your preferred virtualization server having the user ‘ubuntu’ with sudo access
- Upgrade the VM packages:
apt-get dist-upgrade
- Install openssh in the VM:
apt-get install openssh-server
- Copy your ssh public key to the VM’s
~ubuntu/.ssh/authorized_keys
- In the VM, run
sudo visudo
and add the following line to the end of the file:ubuntu ALL=(ALL) NOPASSWD: ALL
- Install Python and Fabric (
easy_install fabric
) - Run the command:
cd build && fab install
- Configure your /etc/hosts file to map 127.0.0.1 to
dbvm
To access the databases in the VM, use:
Database | Admin command line | Persist db command line |
---|---|---|
MySQL | mysql -u root | mysql -u persist persist |
PostgreSQL | sudo -u postgres psql | psql -d persist -U persist |
Oracle | sqlplus SYS/root as SYSDBA | sqlplus persist/persist |
Derby | /usr/local/derby/bin/ij then: connect ‘jdbc:derby://127.0.0.1/persist;create=true;user=persist;password=persist’; | |
H2 | java -cp /usr/local/h2/bin/h2-1.2.147.jar org.h2.tools.Shell -url jdbc:h2:tcp://127.0.0.1/persist -user persist -password persist |
Copyright 2011, persist committers. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied by the copyright holders.