Stasis
is an object-relational-mapping (ORM) framework aimed at simplicity of use by application
developer with minimized impact to surrounding application code.
If you wonder what is persistence framework good for, here is short list of functions it can handle:
First of all, it can CREATE A DATABASE TABLE just by providing some of your Java class (in fact it should be Java Bean style class with properties provided through getter-setters)
Next, when table is created, you can simply STORE your instance (well, its property values) into this table by one method call, which returns you its fresh unique identifier
When there are some instances in the table, you can LOAD some of them into newly constructed instances of your class (therefore your class must have default public constructor)
You can load instances by IDENTIFIER or by FILTER EXPRESSION - for example if you want to load all invoices with total price higher than $1000
Additionaly, you can load instances of not only single class (entity), but also of their CARTESIAN PRODUCT (eg. collect invoices together with their items per one database query)
You can provide to Stasis your own PROJECT-SPECIFIC RULES:
Rules for generating table names from names of classes
Rules for generating attribute names from property names
Specify identifier attribute name and data type (you can use String/VARCHAR ID's if you are THAT kind of person ;o)
Specify which attributes are indices
All abovementioned rules can be specified GLOBALLY for whole project, and/or LOCALLY in certain entity class.
You can also provide your own DATABASE CONFIGURATION describing capabilities of your favourite database (some of the most popular DB's configs are included)
You can handle persistent EVENTS directly in instances of your classes. Among these events belongs moments between saving particular instance, moment after saving, after loading or before removing instance from database
Finally, you can register your own global handler(s) of ALL EVENTS, including creation of instances in database, removal instance in database and change of particular attributes of any instances in database (providing you both original and new values).
Stasis also contains support module for many-to-many (M:N) relations, utilizing common operations of adding, removing and querying simple M:N relations.
Copyright (C) 2007 - 2008 Tomáš Darmovzal
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
You can download sources of Stasis persistence framework from
SourceForge.Net
Wikipedia defines term Stasis as:
Stasis
(pronounced /ˈsteɪsɪs/), or hypersleep, is a science fiction concept akin to suspended animation.
Stasis is a key term in the story of british series
Red Dwarf
.
Okay, let's start using Stasis. First of all you need to initialize instance of
stasis.Stasis
class. You must provide the constructor enough information to allow Stasis to create and open JDBC connection
to relational database. Four parameters are needed:
name of JDBC driver class
JDBC URL of database
user name for connecting to database
his password
For starters you can use simple
SQLITE
database connection (you need to provide SQLITE JDBC driver JAR file into CLASSPATH):
Stasis stasis = new Stasis("org.sqlite.JDBC", "jdbc:sqlite:example01.sqlite", null, null);
If you want to store this parameters into external "java properties" file, you can use another
variant of constructor:
Stasis stasis = new Stasis(properties);
If you prefer connecting your
PostgreSQL
database, you can use something like this:
Stasis stasis = new Stasis(
"org.postgresql.Driver",
"jdbc:postgresql://example.com:5432/example_database?charSet=UTF-8",
"username",
"password"
);
Now we have initialized instance of Stasis, so it's right time to start using it. Let's begin with creating our own entity class called, say,
Employee
:
package example01;
import stasis.*;
public class Employee extends StasisObject {
private String name, surname;
public Employee(){}
public Employee(String name, String surname){
this.name = name;
this.surname = surname;
}
public String getName(){ return this.name; }
public void setName(String name){ this.name = name; }
public String getSurname(){ return this.surname; }
public void setSurname(String surname){ this.surname = surname; }
}
Please notice, that this class is derived from class
stasis.StasisObject
which is recommended, but
NOT NECESSARY
way of creating entity classes. By subclassing of StasisObject your class obtains getter and setter of standard identifier
attribute called simply "id" of type "int" (that is int getId() and void setId(int)) and funky toString() method implementation.
If your class already has an unique identifier and/or you don't want to mess your subclass hierarchy with StasisObject class, you are free to use existing unique identifier, you will just need to tell Stasis framework which property it is (you can do it globally by providing your Project subclass, or locally in your class with public static String stasisBeanIdAttributeName() method).
Now there comes a big moment - creating your first database table:
stasis.reset(Employee.class);
Above command makes two steps: first it tries to drop entity table (any error in this step is silently ignored for it might be caused by non-existence of table in DB), and second it creates empty table. If you want to be more low-level, you can call stasis.drop(Class) or stasis.create(Class) yourself. Now let's have a look what happend in DB:
drop table EXAMPLE_0_1__EMPLOYEE
create table EXAMPLE_0_1__EMPLOYEE (
ID numeric(10),
NAME varchar(255),
SURNAME varchar(255)
)
All right, so we have a table now in DB (with kinda weird name, but anyway), and we are prepared to insert there our instance:
Object johnId = stasis.save(new Employee("John", "Whistler"));
causes this in DB:
select max(ID) as FRESH_ID from EXAMPLE_0_1__EMPLOYEE
insert into EXAMPLE_0_1__EMPLOYEE (ID, NAME, SURNAME) values (1, 'John', 'Whistler')
First query finds out fresh identifier (this way using MAX(ID) is default for it will work in most RDBM, ways of generating fresh IDs can be changed either globally in Project or locally in entity - you can use DB sequences if you like). The second then inserts property values of instance into the table.
Now you can try to load previously saved instance of Employee from database, using obtained fresh ID:
Employee john = (Employee) stasis.load(Employee.class, johnId);
System.out.println(john);
This code wil produce following output:
example01.Employee(id=1, name=John, surname=Whistler)
which is standard format of toString() method output inherited from StasisObject class.
If you are not happy with querying instances just by remembered ID, you can create arbitrary expression filters using
FilterBuilder
:
FilterBuilder fb = new FilterBuilder(stasis, Employee.class);
List doubleUEmployees = stasis.load(Employee.class, fb.like("surname", "W%"));
Finally, if we decide to remove Whistler guy from database, we will run this piece of code:
stasis.remove(john);
Now table is empty again.
Following example illustrates how to force Stasis accept chosen table name, column names and identifier name and type.
package example02;
import stasis.*;
public class Employee extends StasisObject {
private String name, surname;
public Employee(){}
public Employee(String name, String surname){
this.name = name;
this.surname = surname;
}
public String getName(){ return this.name; }
public void setName(String name){ this.name = name; }
public String getSurname(){ return this.surname; }
public void setSurname(String surname){ this.surname = surname; }
// This tells Stasis to save this class into DB table called "TBL_EMPLOYEE"
public static String stasisDbTableName(){
return "TBL_EMPLOYEE";
}
// This tells Stasis, that unique identifier name for this entity is
// property "name" (getName(), setName()) instead of default "id"
public static String stasisBeanIdAttributeName(){
return "name";
}
// This tells Stasis, that unique identifier java type is java.lang.String
// instead of default "Integer"
public static Class stasisBeanIdAttributeType(){
return String.class;
}
// This tells Stasis how should be named DB table columns corresponding to
// particular bean attributes
public static String stasisDbAttributeName(String name){
if("surname".equals(name)) return "SuRnAmE";
if("name".equals(name)) return "__FIRST_NAME__";
return null;
}
// This tells Stasis how should be generated unique identifiers for new
// instances of this class
public static String stasisFreshIdValue(){
return String.valueOf(System.currentTimeMillis());
}
}
Other static methods used by Stasis are:
stasisGetReference - tells Stasis if attribute is reference to identifier of another entity, and should be therefore foreign-key
stasisIsIndex - tells Stasis whether certain attribute should be indexed in DB
If you want to handle persistence events in your bean, you can implement some of these interfaces, and Stasis will inform you when such events occures:
public interface Actions extends SaveAction, LoadAction, RemoveAction {}
public interface LoadAction {
public void stasisAfterLoad(Stasis stasis) throws StasisException;
}
public interface SaveAction {
public void stasisBeforeSave(Stasis stasis) throws StasisException;
public void stasisAfterSave(Stasis stasis) throws StasisException;
}
public interface RemoveAction {
public void stasisBeforeRemove(Stasis stasis) throws StasisException;
public void stasisAfterRemove(Stasis stasis) throws StasisException;
}
If you want to use cascading persistence (eg. when Invoice is saved, it will also want to save all of its InvoiceItem-s), you will find useful
this events - they are raised on entities, which are "owned" by superior entity (such superior entity must call cascade save/load/remove methods with
parameter "owner" set to itself). In this methods can owned instances for example bind themselves to their owner.
public interface OwnedActions extends OwnedLoadAction, OwnedSaveAction, OwnedRemoveAction {}
public interface OwnedSaveAction {
public void stasisSavedByOwner(Stasis stasis, Object owner);
}
public interface OwnedLoadAction {
public void stasisLoadedByOwner(Stasis stasis, Object owner);
}
public interface OwnedRemoveAction {
public void stasisRemovedByOwner(Stasis stasis, Object owner);
}
When you wish to implement centralized log of all changes made by Stasis, you can register your own implementation of interface ChangeListener:
stasis.addChangeListener(new ChangeListener(){
// Called when instance is INSERTed into database for every attribute
public void instanceCreated(Class clazz, Object id, String name, Object value){
...
}
// Called when instance is UPDATEd in database for every actually CHANGED attribute
public void instanceChanged(Class clazz, Object id, String name, Object orgValue, Object newValue){
...
}
// Called when instance is DELETEd from database
public void instanceRemoved(Class clazz, Object id){
...
}
}
Example of how to set your own project-specific profile to Stasis:
stasis.setProject(new Project(){
public String getCommonDbTableName(Class clazz){
// return convertNameJavaToDb(clazz.getName());
...
}
public String getCommonDbAttributeName(Class clazz, String beanAttName){
// return convertNameJavaToDb(beanAttName);
...
}
public String getCommonBeanIdAttributeName(Class clazz){
// return "id";
...
}
public Class getCommonBeanIdAttributeType(Class clazz){
// return Integer.TYPE;
...
}
public Object getCommonBeanUnknownIdAttributeValue(Class clazz){
// return null;
...
}
public Object getCommonFreshIdValue(Class clazz) throws StasisException {
...
}
});
You will also probably need to control Java-to-DB attribute types mapping, change
default types mapped for built-in java types (String, Integer, Date) or just
connect to some unsupported SQL database. All this you can manage by providing
your own implementation of
DbConfig
:
stasis.setDbConfig(new MyDbConfig());
public class MyDbConfig extends DefaultDbConfig {
public MyDbConfig(){
// First provide mapping hints for JAVA-DB type conversion:
// String attributes will NOT be converted before passing to JDBC, DB type
// will be "varchar(255)"
this.addMapping(String.class, "varchar(255)");
// Boolean attributes will be saved as "numeric(1)" type and must be therefore
// converted to Integer before passing to JDBC driver
this.addMapping(Boolean.class, Integer.class, "numeric(1)", new Conversion(){
public Object javaToDb(Object javaObject){
return new Integer(Boolean.TRUE.equals(javaObject) ? 1 : 0);
}
public Object dbToJava(Object dbObject){
return new Boolean(((Number) dbObject).intValue() != 0);
}
});
// java.util.Date attributes will be converted to java.sql.Timestamp for JDBC driver
this.addMapping(java.util.Date.class, java.sql.Timestamp.class, "timestamp", new Conversion(){
public Object javaToDb(Object javaObject){
return new java.sql.Timestamp(((java.util.Date) javaObject).getTime());
}
public Object dbToJava(Object dbObject){
return new java.util.Date(((java.sql.Timestamp) dbObject).getTime());
}
});
// Now you can set some capability-flags of your RDBM
this.addCapability(CAP_PRIMARY_KEY); // can use PRIMARY KEY for IDs
this.addCapability(CAP_FOREIGN_KEY); // can use FOREIGN KEYs for referencing attributes
this.addCapability(CAP_ILIKE); // can use ILIKE operator (case insensitive LIKE)
this.addCapability(CAP_SEQUENCE); // can use sequences for ID generation
this.addCapability(CAP_INDEX); // can create DB indices for attributes
// Add RDBM-specific keywords
this.addKeyword("user");
this.addKeyword("right");
}
// Escaping of keywords is in your RDMB made using quotes
public String escape(String keyword){
return '"' + keyword + '"';
}
}