simple-jpa 0.5

simple-jpa is a Griffon plugin for developing JPA and Swing based desktop application.

Download .zip View on GitHub

Table of Contents

1. Introduction

simple-jpa is a Griffon plugin for developing JPA and Swing based desktop application. The main goal of simple-jpa is to allow developer to concentrate on business logic. simple-jpa provides much functionality that is needed when working with JPA, therefore, frees developer from writing high-ceremony code.

simple-jpa is very useful for rapidly developing Swing-based database oriented desktop application. It can also be used for prototyping.

The following is a list of some of simple-jpa’s features:

Scaffolding – simple-jpa can generate an MVCGroup based on a domain class. This will speed up development.

Dynamic finders – simple-jpa injects dynamic finders to controllers (or services). With dynamic finders, developer can perform a query on JPA entities (or domain objects) quickly and easily. simple-jpa also supports the execution of JPA named query, JPQL and native SQL.

Transaction management – Unlike web-based applications, desktop applications do not require Java Transaction API (JTA). simple-jpa automatically provides and manages transaction for each method in controllers (can be configured by using annotation). By default, simple-jpa will share EntityManager across transaction in a way that is suitable for desktop application.

Bean Validation API (JSR-303) support – In the case of failed validation, simple-jpa will automatically present error messages in Swing-based view. Developer can also configure error notification and its behavior.

Common database application features – simple-jpa adds the following to all domain classes: an id (auto generated primary key), fields that store created time and last modified time (will be filled automatically), and a soft delete flag (soft delete is marking the object as inactive without deleting it from database).

Swing nodes for database application - simple-jpa provides template renderer for effortlessly represent domain object in JTable, JList or JComboBox. It also provides new nodes that can be used in Griffon’s view such as tagChooser, numberTextField, maskTextField, and dateTimePicker.

Integration testing – simple-jpa is using dbUnit in integration testing to fill database with predefined data from a Microsoft Excel file (or csv file). This way, every test cases will be executed with the same table data.

2. What's News In simple-jpa 0.5

simple-jpa 0.5 has a new SwingBuilder's nodes for using GlazedLists for JTable which is simpler and easier to use than the previous one. simple-jpa 0.5 also has improved dynamic finders. More information about simple-jpa developments can be found at http://thesolidsnake.wordpress.com/tag/simple-jpa.

Warning: These changes make simple-jpa 0.5 not compatible with previous releases. It is not recommended for project which uses previous release to upgrade to simple-jpa 0.5.

simple-jpa 0.5.2 fixes bugs that causes create-simple-jpa command not working as expected. In this release, create-simple-jpa will download required JDBC driver from Maven central repository based on -jdbc value if it is not available. This will be disabled if -skip-database argument is present.

simple-jpa 0.5.2 also introduces support for Apache Derby embedded database in create-simple-jpa command. For example, the following command will setup an embedded Apache Derby database:

griffon create-simple-jpa -user=myuser -password=mypassword 
    -rootPassword=password -database=C:/database 
    -jdbc=derby-embedded

The command above will use Derby database stored in C:/database. If it doesn't exist, a new database will be created in the same location. The owner of this new database is user root and her password is password (specified in -rootPassword). User myuser (password is mypassword) will also be created. The project will connect to the specified Derby database as user myuser.

find[DomainClass]ByDsl() now accepts nested path by separating them by double underscores (__). For example:

def result = findOrderByDsl {
    orderItem__category__name eq('CAT1')
}

The code above produces the same result as:

def result = executeQuery("FROM Order o 
    WHERE o.orderItem.category.name = 'CAT1'")

3. Getting Started

Work in progress...

4. Scripts

simple-jpa scripts can be called just like any Griffon's commands:

griffon [command-name] [argument1] [argument2] ...

To display more information for a command, call it with -info argument:

griffon [command-name] -info

4.1. create-simple-jpa

This command is usually the first command that will be invoked before working with Java Persistence API (JPA). It will create persistence.xml and orm.xml in current project. It will also create some resource files that are commonly required when working with JPA.

The syntax for this command is:

create-simple-jpa -user=[databaseUser] -password=[databasePassword]
    -database=[databaseName] -rootPassword=[databaseRootPassword]
    -provider=[JPAProvider] -jdbc=[databaseType]

or

create-simple-jpa -user=[databaseUser] -password=[databasePassword]
    -database=[databaseName] -provider=[JPAProvider]
    -jdbc=[databaseType] -skipDatabase

user is the name of database user. JPA will establish connection to database by using the specified user name. If user name doesn't exists, it will be created automatically.

password is the password used when establishing connection to the database.

database is the database name or schema name. If this database doesn't exists, it will be created automatically. The specified user will also be granted privilleges to use this database.

rootPassword is the password for database root/administrator. To create user and database and grants privilleges, this command requires password for root/administrator user. This value will never be stored in project files.

provider is the name of JPA provider that will be used. The default value for this parameter is 'hibernate'. The following is list of available values:

  • hibernate - Use Hibernate JPA

databaseType is the name of JDBC driver that will be used. The default value for this parameter is 'mysql'. The following is list of Available values:

  • mysql - Use MySQL JDBC
  • derby-embedded - Use Apache Derby embedded database JDBC

skipDatabase is used to tell this command to not create user and database automatically. This command will only write to persistence.xml and assume required database schema and user is available.

Examples:

The following command will generate persistence.xml with a connection to MySQL database (user: steven, password: 12345, database schema: sample), uses Hibernate JPA, and creates user steven and sample schema if they are not exists:

griffon create-simple-jpa -user=steven -password=12345 -database=sample
    -rootPassword=secret

The following command will do the same as the previous one:

griffon create-simple-jpa -user=steven -password=12345 -database=sample
    -provider=hibernate -databaseType=mysql -rootPassword=secret

The following command will generate persistence.xml with a connection to MySQL database (user: scott, password: tiger, database schema: ha), uses Hibernate JPA, and will not check if required user and schema are available:

griffon create-simple-jpa -user=scott -password=tiger -database=ha
    -skip-database

4.2. create-domain-class

This command will create a new empty domain class and register it in persistence context file. Before creating domain class, the project must has persistence.xml file in metainf directory. To generate required files for working with JPA, use create-simple-jpa command.

Domain class will be generated in the package specified by griffon.simplejpa.model.package value. The default package is domain.

To change the default template used for generating domain clasess, execute install-templates command and edit SimplaJpaDomainClass.groovy.

The syntax for this command is:

create-domain-class [domainClassName]

or

create-domain-class [domainClassName] [domainClassName] ...

or

create-domain-class [domainClassName],[domainClassName], ...

domainClassName is the name of domain class that will be generated.

Examples:

griffon create-domain-class Student
griffon create-domain-class Teacher Student
griffon create-domain-class Teacher,Student

4.3. generate-all

This command will create a new MVCGroup based on a domain class. The generated MVCGroup (consists of a view, a model and a controller) has the ability to perform CRUD operations on a domain class. This command can also generate a startup MVCGroup that act as container for the others.

Domain classes should be located in the package specified by griffon.simplejpa.model.package in Config.groovy. The default value for package is domain.

When the value of griffon.simplejpa.finders.alwaysExcludeSoftDeleted is true, the generated controller will call softDelete() instead of remove().

To change the default template used by this command, execute install-templates command and edit the generated template files.

The syntax for this command is:

generate-all * [-generatedPackage] [-forceOverwrite] [-setStartup]
    [-skipExcel] [-startupGroup=value]

or

griffon generate-all [domainClassName] [-generatedPackage]
    [-forceOverwrite] [-setStartup] [-skipExcel] 
    [-startupGroup=value]

or

generate-all [domainClassName] [domainClassName] ...
    [-generatedPackage] [-forceOverwrite] [-setStartup] [-skipExcel]
    [-startupGroup=value]

domainClassName is the name of domain class the will be manipulated by the generated MVCGroup. Each domain class will have their own MVCGroup generated. If this value is *, then all domain classes will be processed.

generatedPackage (optional) is the target package. By default, the value for this parameter is project.

forceOverwrite (optional) is used to tell this command to replace existing files without any notifications.

setStartup (optional) is used to tell this command to set the generated MVCGroup as startup (the MVCGroup will be launched when program starts). If this argument is present when generating more than one MVCGroup, then only the last MVCGroup will be set as startup group.

skipExcel (optional) is used to tell this command to not create Microsoft Excel file for integration testing (DbUnit).

startupGroup (optional) is used to tell this command to generate a MVCGroup that serves as startup group. The generated MVCGroup will not based on any domain class, instead it will act as a container for the other domain classes' based MVCGroups.

Examples:

The following command will generate MVCGroup for all domain classes:

griffon generate-all *

The following command will generate MVCGroup for all domain classes, overwriting existing files, and set the last MVCGroup as startup:

griffon generate-all * -forceOverwrite -setStartup

The following command will generate MVCGroup for domain class Student, Teacher, and Classroom:

griffon generate-all Student Teacher Classroom

The following command will generate MVCGroup for domain class Student and generate a container MVCGroup which name is MainGroup:

griffon generate-all Student -startupGroup=MainGroup

The following command will generate a container MVCGroup which name is MainGroup:

griffon generate-all -startupGroup=MainGroup

4.4. install-templates

This command will add templates used by simple-jpa to current project in /src/templates/artifacts. This command is useful for changing templates that is used by simple-jpa generator. Developer can edit the templates and the next invocation of simple-jpa generator will based on them.

The syntax for this command is:

install-templates

Examples:

griffon install-templates

4.5. simple-jpa-console

This command will launch Groovy Console loaded with Griffon and simple-jpa. Developer can use this command to test or execute code interactively.

For each loaded MVCGroup, there are three variables to refer to its model, view, and controller. For example, if MVCGroup name is student, developer can refer to its model, view, or controller by using the following variables: studentModel, studentController and studentView. Developer can also use app to refer to GriffonApplication. To display list of available variables, select Script, Inspect Variables.

When console is started, it only loads startup MVCGroup. To load the another MVCGroup, select simple-jpa, MVCGroups and check the desired MVCGroup.

The syntax for this command is:

simple-jpa-console

Examples:

griffon simple-jpa-console

5. Configuration

simple-jpa can be configured by adding configuration lines in griffon-app/conf/Config.groovy. Adding JPA provider properties can also be done directly by editing griffon-app/conf/metainf/persistence.xml or by using system properties.

5.1. griffon.simplejpa.domain.package

simple-jpa doesn't use any metadata to manage domain classes. The only way for simple-jpa to find or to write domain classes is by inspecting the content of domain package. All domain classes should be located in this domain package. By default, the name of domain package is domain.

Developer can change the name of domain package by using griffon.simplejpa.domain.package configuration line. For example, the following configuration change the name of domain package to com.example.domain:

griffon.simplejpa.domain.package='com.example.domain'
Warning: Changing this configuration value will not move existing domain classes to the new domain package. This value merely used as an indicator to find domain classes. Current version also doesn't support subpackages. Dynamic finders only work with domain classes that are located exactly in domain package (excluding its subpackages).

5.2. griffon.simplejpa.entityManager.checkThreadSafeLoading

Griffon has many features related to threading. For example, controller will automatically executed in its own thread to prevent them running in event dispatch thread. The problem is EntityManager (JPA) is not thread-safe. The default pattern for simple-jpa doesn't use data transfer objects but shares domain objects across application. While this reduces complexity, it may causes inconsistency.

simple-jpa can check for an entity to determine if a domain object is manipulated from unexpected thread. While this feature is part of AuditingEntityListener, it is not recommended to enable this feature in production. To enable this feature, add the following line to Config.groovy:

griffon.simplejpa.entityManager.checkThreadSafeLoading = true

Issues related to threading can be avoided by setting griffon.simplejpa.entityManager.lifespan to TRANSACTION. Since simple-jpa 0.5, TRANSACTION is the default value.

5.3. griffon.simplejpa.entityManager.defaultFlushMode

Use this configuration key to change the default flushMode for all EntityManager in application. The possible values for flushMode is COMMIT and AUTO. The default value for flushMode is depends on JPA provider.

For example, the following configuration line will set flushMode for all EntityManager to DefaultFlushModeType.COMMIT:

griffon.simplejpa.entityManager.defaultFlushMode = 'COMMIT'

Default flushMode can be overriden in certain queries by using flushMode query configuration.

5.4. griffon.simplejpa.entityManager.lifespan

This configuration determines when will simple-jpa creates and destroys EntityManager. The possible values is MANUAL and TRANSACTION.

Before simple-jpa 0.5, the default value for this configuration key is MANUAL. In this mode, simple-jpa will always reuse EntityManager as far as possible. simple-jpa will maintain session per MVCGroup pattern: EntityManager will be created when an MVCGroup is created and the same EntityManager will be closed when its associated MVCGroup is destroyed. The problem with this pattern is it may introduces inconsistencies because EntityManager is not thread-safe. It is recommended to set griffon.simplejpa.entityManager.checkThreadSafeLoading to true in this mode.

Since simple-jpa 0.5, the default value for this configuration key is TRANSACTION. In this mode, simple-jpa will maintain session per transaction pattern. A new EntityManager will be created in the beginning of transaction and will be destroyed in the end of transcation.

For example, the following configuration will use session per MVCGroup pattern:

griffon.simplejpa.entityManager.lifespan = 'MANUAL'

5.5. griffon.simplejpa.entityManager.properties

In most cases, JPA properties can be added directly to griffon-app/conf/metainf/persistence.xml. The following is content of a typical persistence.xml file for desktop application:

<?xml version="1.0" encoding="UTF-8"?><persistence xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/persistence" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0">
<persistence-unit name="default" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<class>domain.Student</class> <class>domain.Classroom</class>
<properties>
<property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost/mydatabase"/>
<property name="javax.persistence.jdbc.user" value="scott"/>
<property name="javax.persistence.jdbc.password" value="tiger"/>
<property name="hibernate.connection.autocommit" value="false"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect"/>
<property name="hibernate.hbm2ddl.auto" value="create-drop"/>
<property name="jadira.usertype.autoRegisterUserTypes" value="true"/>
</properties>
</persistence-unit>
</persistence>

Developer can override properties in persistence.xml by adding configuration line with griffon.simplejpa.entityManager.properties key. For example, the following configuration will result in the same JPA properties as above:

griffon {
    simplejpa {
        entityManager {
            properties  {
                javax.persistence.jdbc.driver = 'com.mysql.jdbc.Driver'
                javax.persistence.jdbc.url = 'jdbc:mysql://localhost/mydatabase'
                javax.persistence.jdbc.user = 'scott'
                javax.persistence.jdbc.password = 'tiger'
                hibernate.connection.autocommit = 'false'
                hibernate.dialect = 'org.hibernate.dialect.MySQL5Dialect'
                jadira.usertype.autoRegisterUserTypes=  'true'
            }
        }
    }
}

5.6. griffon.simplejpa.entityManager.propertiesFile

Another way to override JPA provider properties is by writing the properties in a file called simplejpa.properties. Content of the file is in Groovy's config format just like the one specified in Config.groovy. simple-jpa will search for this file in current working directory. If the file exists, its content will override properties in persistence.xml and Config.groovy. This is a convenient way to modify JPA properties without changing source code. It is common to put database connection properties in simplejpa.properties so that changing database connection will not require touching application's source code.

The following is sample content of simplejpa.properties:

javax {
    persistence {
        jdbc {
            driver = 'com.mysql.jdbc.Driver'
            url = 'jdbc:mysql://localhost/mydatabase'
            user = 'scott'
            password = 'tiger'
        }
    }
}
hibernate {
    connection {
	autocommit = 'false'
    }
    dialect = 'org.hibernate.dialect.MySQL5Dialect'
}               
jadira.usertype.autoRegisterUserTypes=  'true'

Developer can change location and name of the simplejpa.properties by adding griffon.simplejpa.entityManager.propertiesFile configuration line. For example, in the following configuration, simple-jpa will search for a file called connection.db in drive C root directory:

griffon.simplejpa.entityManager.propertiesFile = 'C:/connection.db'

5.7. griffon.simplejpa.finders.alwaysExcludeSoftDeleted

If the value for this configuration key is true, simple-jpa finders will not return soft deleted objects. A soft deleted object is an object whose deleted attribute is not equals to 'N'. The deleted attribute is available on all domain classes that have @DomainClass annotation. To soft delete an domain object, use softDelete() method.

Note that developer can still retrieve soft deleted objects by using executeQuery(), executeNativeQuery(), or passing true to notSoftDeleted query configuration.

By default, the value for this configuration key is false. To enable it, add the following line to Config.groovy:

griffon.simplejpa.finders.alwaysExcludeSoftDeleted = true

5.8. griffon.simplejpa.finders.injectInto

By default, simple-jpa will inject JPA related methods such as finders to all Griffon's controllers. This causes all controllers to act as public repositories with the abilities to retrieve arbitary domain objects. While this pattern reduces complexity, some people may want to appoint service layer as repository layer (instead of controllers as repositories). This can be achieved by adding the following configuration line:

griffon.simplejpa.finders.injectInto = ['service']

The following examples will cause simple-jpa to inject JPA related methods to all controllers and all models:

griffon.simplejpa.finders.injectInto = ['controller', 'model']

5.9. griffon.simplejpa.finders.prefix

To avoid conflict with existing methods in controller, simple-jpa can add prefix to its dynamic methods. For example, the following configuration line will add jpa prefix to simple-jpa dynamic methods:

griffon.simplejpa.finders.prefix = 'jpa' 
// This will add 'jpa' prefix to dynamic methods name, for example:
// findAllStudent() becomes jpaFindAllStudent()
The following dynamic methods will never have prefix:
  • beginTransaction()
  • commitTransaction()
  • rollbackTransaction()
  • return_failed()
  • createEntityManager()
  • destroyEntityManager()
  • getEntityManager()

5.10. griffon.simplejpa.validation.convertEmptyStringToNull

In some cases, validation may be failed because the empty JTextField value in model is an empty String and not a null value. To create a consistent behaviour, simple-jpa can translate all empty String into a null value before performing validation. This feature is disabled by default. To enable it, add the following configuration line to Config.groovy:

griffon.simplejpa.validation.convertEmptyStringToNull = true

6. Annotations

These annotations are using Groovy AST transformation feature to change the content of a class dynamically on compile.

6.1. @ConditionSupport

Adding @ConditionSupport annotation to a Swing TableCellRenderer will make the renderer ready to use with condition() node.

Example:

@ConditionSupport
class MyCustomRenderer extends DefaultTableCellRenderer {
   ...
}

6.2. @DomainClass

This annotation is supposed to be used on domain class, for example:

@DomainClass
class Student {

}

@DomainClass annotation will add the following attribute to the annotated class:

@Id @GeneratedValue(strategy=GenerationType.TABLE)
Long id

String deleted = 'N'

Date createdDate

Date modifiedDate

@DomainClass will not add the attributes above if the annotated class is a subclass of another class which is annotated by @DomainClass or @Entity.

To skip adding id attribute, set excludeId to true. To skip adding deleted attribute, set excludeDeletedFlag to true. To skip adding createdDate and modifiedDate attribute, set excludeAuditing to true. For example, the following annotation will only add id to the annotated class:

@DomainClass(excludeDeletedFlag=true, excludeAuditing=true)
class Student {

}

By default, the id generation strategy is GenerationType.TABLE. To change the default generation strategy, use idGenerationStrategy member. For example, the following annotation will add an id attribute whose value is generated by using database identity column (auto increment field):

@DomainClass(idGenerationStrategy=GenerationType.IDENTITY)
class Student {

}
Warning: This annotation only add attributes to annotated class. Using this annotation on a class doesn't mean that simple-jpa will register it in persistence.xml. Finders will only find domain classes located in griffon.simplejpa.domain.package. Even when a class is annotated with @DomainClass, if it is not located in domain package, finders will not recognize it. This behaviour may change in the future.

6.3. @Transaction

@Transaction annotation can be used to annotate a class, a closure field, or a method. It will cause the method or closure to be wrapped in database transaction. Using @Transaction on a class will make all execution of methods or closure fields in the class to be wrapped in database transaction. If there are both @Transaction annotation in a method/closure field and its class, only annotation in method will be used.

@Transaction annotation value can be one of Policy.NORMAL, Policy.SKIP_PROPAGATION, and Policy.SKIP. The default value is Policy.NORMAL.

A Policy.NORMAL transaction will join the previous transaction if it is called from another transaction. This will enable transaction propagation. For example, the following code will not save any objects to database because processA(), processB(), and processC() is part of one transaction and if any of them fails, all operations will rollback:

@Transaction
class AController {

    def processC() {
    	throw new RuntimeException("Suddenly there is an error here!")
    }

    def processB() {
    	ADomainClass b = new ADomainClass()
        persist(b)
        processC()
    }

    def processA() {
    	ADomainClass a = new ADomainClass()
        persist(a)
        processB()
    }
    
}

A Policy.SKIP_PROPAGATION transaction will always start a new transaction. It will not join previous transaction. For example, the following code will result in a being persisted in database but b will be ignored:

@Transaction
class AController {

    @Transaction(Transaction.Policy.SKIP_PROPAGATION)
    def processB() {
    	ADomainClass b = new ADomainClass()
        persist(b)
        processC()
    }

    def processA() {
    	ADomainClass a = new ADomainClass()
        persist(a)
        processB()
    }
    
}
Warning: Policy.SKIP_PROPAGATION will behave as described in the example above if griffon.simplejpa.entityManager.lifespan is set to MANUAL.

A Policy.SKIP transaction mean there is no transaction at all (@Transaction will do nothing). simple-jpa will still wrap each dynamic methods invocation inside a standalone transaction. It is recommended to use this attribute to method or closure fields that don't access database, as shown in the sample below:

@Transaction
class AController {

    @Transaction(Transaction.Policy.SKIP)
    def notRecommended() {
        ADomainClass a = new ADomainClass()
        persist(a)
        throw new RuntimeException("Suddenly fail here!")
        ADomainClass b = new ADomainClass()
        persist(b)
    }
    
    @Transaction(Transaction.Policy.SKIP)
    def recommended() {
        // Use it on method which doesn't require database access
        model.result = model.number1 + model.number2
    }
}

In the example above, notRecommended() will persist a in the database but not b because an error was encountered immediately after persisting a. This is not recommended because it will leave database in inconsistent state.

@Transaction annotation has an member called newSession. If newSession is true, the previous EntityManager will be destroyed before starting a new transaction.

Warning: Use newSession to indicate the start of a new session that use session per MVCGroup pattern. This value will have no effect if griffon.simplejpa.entityManager.lifespan is set to MANUAL.

7. Injected Methods

simple-jpa will inject JPA related methods to all controllers (or other artifacts configured in griffon.simplejpa.finders.injectInto). These methods are always public.

7.1. beginTransaction()

It is not recommended to call this method directly. This is a low level method that will be called by @Transaction.

Use this method to start a new transaction. If resume parameter is true, it will join the previous transaction if previous transaction is exists. If newSession parameter is true, this method will destroy previous EntityManager.

Example of low level operations using simple-jpa:

beginTransaction()
def em = getEntityManager()
... // perform works with em
em.close()
commitTransaction()

7.2. commitTransaction()

It is not recommended to call this method directly. This is a low level method that will be called by @Transaction.

Use this method to commit a transaction. If griffon.simplejpa.entityManager.lifespan is set to TRANSACTION, this method will also close current EntityManager.

Example of low level operations using simple-jpa:

beginTransaction()
def em = getEntityManager()
... // perform works with em
em.close()
commitTransaction()

7.3. createEntityManager()

It is not recommended to call this method directly. This is a low level method that will be called by @Transaction.

Use this method to create a new EntityManager for current thread.

7.4. destroyEntityManager()

It is not recommended to call this method directly. This is a low level method that will be called by @Transaction.

Use this method to close all open EntityManager.

7.5. executeNamedQuery()

Use this method to execute JPA named query. This method will return a List that contains the result from named query execution. The syntax of this method is:

def executeNamedQuery(String namedQuery, Map args, Map config = [:])

namedQuery is the name of JPA named query that will be executed. args is a Map that stores query parameter values. config is an optional query configuration.

This is an example of JPA Named Query declaration:

@NamedQuery(name='Product.CalculateTotal', query='''
    SELECT SUM(i.qty) FROM items i WHERE 
    i.product = :product AND (i.date <= :endDate)
'''
class Product {

}

To call the named query declared above, use the following code:

Product p = findProductByCode('P001')
def total = executeNamedQuery('Product.CalculateTotal', 
    [product: p, endDate: LocalDate.now()])[0]

7.6. executeNativeQuery()

Use this method to execute native SQL. This is a low level query method (from JPA point of view) so it is better to use executeQuery() if possible. This method will return a List that contains the result from SQL execution. The syntax of this method is:

def executeNativeQuery (String sql, Map config = [:])

sql is SQL string that will be executed. config is an optional query configuration.

Example:

def mysqlDbUser = executeNativeQuery('SELECT user()')[0]

7.7. executeQuery()

Use this method to execute JP QL. This method will return a List that contains the result from JP QL execution. The syntax of this method is:

def executeQuery (String jpql, Map config = [:])

jpql is JP QL string that will be executed. config is an optional query configuration.

Example:

def allStudents = executeQuery('FROM Student')

7.8. getEntityManager()

It is not recommended to call this method directly. It is better to use simple-jpa methods rather than calling EntityManager methods directly.

This method will return EntityManager for current thread.

Example:

createEntityManager()
def em = getEntityManager()
// do something with em
destroyEntityManager()

7.9. merge()

This is a shortcut for calling merge() method of current EntityManager. Use this method to put a detached entity into current EntityManager.

Example:

merge(student)

7.10. persist()

This is a shortcut for calling persist() method of current EntityManager. Use this method to save a new entity into database.

Example:

Student s = new Student()

// insert operation
persist(s)

// update operation
s.name = 'new'

7.11. remove()

This method will call remove() method of current EntityManager. Before executing remove(), simple-jpa will try to merge the entity if it is detached. Use this method to delete an entity from database.

Example:

findAllStudentByName('%Steve%').each {
  remove(it)
}

7.12. rollbackTransaction()

It is not recommended to call this method directly. This is a low level method that will be called by @Transaction.

Use this method to rollback a transaction and clear the EntityManager.

Example of low level operations using simple-jpa:

beginTransaction()
def em = getEntityManager()
... // perform works with em
rollbackTransaction()
em.close()

7.13. softDelete()

Use this method to set deleted attribute to 'Y'. In simple-jpa, a soft deleted entity is an entity whose deleted attribute is not equals to 'N'. To automatically add deleted attribute to an entity, use @DomainClass annotation.

Example:

findAllStudentByName('%steve%').each {
    softDelete(it)
}

7.14. validate()

Use this method to validate an entity. The syntax for this method is:

def validate(entity, group = Default)

entity is the entity that will be validated (by using Java Validation API). group is an optional interface that marks one or more validation annotation as a group. By default, all declared validation annotations is part of Default group.

This method will store validation result in model (in the same MVCGroup). All models in application that uses simple-jpa will have the following injected attributes and methods:

ObservableMap errors = new ObservableMap(new ConcurrentHashMap())

boolean hasError() // return true if errors is not empty

Validation will not be performed if model.hasError() is true. It is important to clear the previous errors before reattempting validation.

If entity is not valid, this method will return false. Otherwise, it will return true. If this method return false, it will store failed attributes and their error messages in model.errors.

This is an example of domain class with Java Validation API annotations:

@DomainClass @Entity @Canonical
class Student {
   @Size(min=2, max=50)
   String name
   
   @Min(0l) @Max(100l)
   Integer score
}

This is an example validation for the instance of domain class above:

Student s = new Student('a', 101)
model.errors.clear()
boolean result = validate(s)

println "Result is $result"
println "Messages: ${model.errors}"

// Output:
// Result is false
// Messages: [score:must be less than or equal to 100, name:size must be between 2 and 50]

To change the error messages returned by Java Validation API, edit the following file: /griffon-app/i18n/ValidationMessages.properties.

8. Finders

Finders are dynamic methods that are used to retrieve entities from database. By following their naming pattern, developer can easily execute query for any domain classes.

8.1. Query Configuration

All finders except find[DomainClass]By[Attributes]() have query configuration as their parameter. Query configuration is stored in a Map. The possible keys for query configuration are:

  • excludeSubclass
  • flushMode
  • notSoftDeleted
  • orderBy
  • orderDirection
  • page
  • pageSize

If excludeSubclass value is '*', it will exclude all subclasses of the domain class. It can also accepts a String that contains comma separated class name to be excluded.

For example, assuming Employee has two subclasses: Teacher and Staff, the following finder will return all instances of Employee including all Teacher and all Staff.

findAllEmployee()

The following finder will return only instance of Employee but not Teacher or Staff:

findAllEmployee([excludeSubclass: '*'])

The following finder will return all instance of Employee and all instance of Teacher but excluding all Staff:

findAllEmployee([excludeSubclass: 'Staff'])

flushMode configuration key accepts FlushModeType.COMMIT or FlushModeType.AUTO. Use this key to override flush mode for specific queries.

If notSoftDeleted is true, then finder will not return soft deleted entities. An entity is considered as soft deleted if its deleted attribute is not equals to 'N'.

orderBy will causes finder to sort results based on certains attributes. To sort based on more than one attribute, use comma separated attribute name as shown in the following:

findAllEmployee([orderBy: 'name,salary'])

To set order direction, use orderDirection. This key accept 'asc' for ascending order and 'desc' for descending order. For example, the following finder will find all Employee sorted by name and salary in descending order:

findAllEmployee([orderBy: 'name,salary', orderDirection: 'desc,desc'])

To limit the result to certain page, set the value for page (starting from 1) and pageSize. If pageSize is not specified, simple-jpa will assume 1 page consists of 10 rows. For example, the following finder will limit the results to the first 3 entities:

findAllTeacher([orderBy: 'salary', page: 1, pageSize: 3])

8.2. findAll[DomainClass]()

This finder will return a List that contains all instances of a domain class. If no instances are found, it will return an empty List.

Example:

def allStudents = findAllStudent()
def allTeachers = findAllTeacher()

8.2. find[DomainClass]By[And|Or]()

This finder can be used to quickly find instances of domain class based on one or more attributes value using and or or logical operator.

For example, the following finder will return all female Student in class 'A3':

List result = findAllStudentByAnd([classRoom: 'A3', gender: 'F'])

The following finder will return a Student whose name is Steven and class is 'A3' (if it is not found, the finder will return null value):

Student s = findStudentByAnd([name: 'Steven', classRoom: 'A3'])

An alternative syntax for this finder is:

Student s = findByAnd(Student, [name: 'Steven', classRoom: 'A3'])

8.3. find[DomainClass]By[Attributes]()

This is the most flexible finder. It allows complex searching in one method call. For example, this finder will search all female Student in class 'A3':

List result1 = findAllStudentByClassRoomEqAndGenderEq('A3','F')
List result2 = findAllStudentByClassRoomAndGender('A3', 'F')

The default operator used for comparison is eq (equality) if operator is not specified. The following is list of supported operators:

  • greaterThanEqualTo or ge
  • lessThanEqualTo or le
  • greaterThan or gt
  • lessThan or lt
  • isNotMember
  • isNotEmpty
  • isNotNull
  • notEqual or ne
  • isMember
  • isEmpty
  • isNull
  • like
  • notLike
  • between
  • equal or eq

For example, the following finder will return all Student in class room 'A3' whose score is less than 70:

List result = findAllStudentByClassRoomAndScoreLt('A3', 70)

The following finder will return only one Student whose name contains 'jack' or null if not found:

Student s = findStudentByNameLike('%jack%')

8.4. find[DomainClass]ByDsl()

This finder will perform a query based on closure (Dsl). For example, the following finder will return all Student in class room 'A3' whose score is less than 70:

List result = findAllStudentByDsl {
    classRoom eq('A3')
    and()
    score lt(70)
}

Note that it is important to separate these conditions and logical operator by line.

The benefit of using Dsl closure is developer can build the query conditions on the fly. For example, it is typical to perform searching based on one or more user selection in view:

List result = findAllInvoiceByDsl {
    if (model.txtNumber) {
       number like("%${txtNumber}%")
    }
    if (model.selectedSupplier) {
       and()
       supplier eq(selectedSupplier)
    }
    if (model.paid) {
       and()
       paid eq(true)
    }
}

An alternative syntax for this finder is:

List result = findByDsl(Student) {
    classRoom eq('A3')
    and()
    score lt(70)
}

Since version 0.5.2, this method can accept nested path by separating them using double underscores (__). For example, to find Orders based on their OrderItem's Category, use the following code:

List result = findOrderByDsl {
    orderItem__category__name eq('CAT1')
}

9. Events

simple-jpa will raise the following custom Griffon events:

  • simpleJpaBeforeCreateEntityManager is raised before EntityManager is created.
  • simpleJpaCreateEntityManager is raised when a new EntityManager is created.
  • simpleJpaDestoryEntityManager is raised when EntityManager is destroyed.
  • simpleJpaBeforeAutoCreateTransaction is raised before simple-jpa automatically create new transaction.
  • simpleJpaNewTransaction is raised when a new transaction is created.
  • simpleJpaCommitTransaction is raised when transaction is committed.
  • simpleJpaRollbackTransaction is raised when transaction is rollbacked.

10. SwingBuilder Nodes

In most cases, database oriented application will need to display data in the form of table. Groovy already has an easy to use SwingBuilder. simple-jpa extends it by adding the following nodes to make working with JTable and GlazedLists become painless:

  • glazedTable()
  • glazedColumn()
  • templateRenderer()
  • defaultHeaderRenderer()
  • customConditionalRenderer()
  • condition

This is an example of Swing table backed by GlazedLists:

glazedTable(list: model.studentList, sortingStrategy: SINGLE_COLUMN) {
   glazedColumn(name: 'Name', property: 'name')
   glazedColumn(name: 'Class Room', property: 'classRoom')
   glazedColumn(name: 'Score', property: 'score')
}

glazedTable() will accept any EventList such as BasicEventList in list. If sortingStrategy is specified, glazedTable() will automatically convert it into SortedList.

glazedTable() must have one or more glazedColumn() to represent a table column. For example, the following glazedColumn() will display column value from domain class property:

glazedColumn(name: 'Name', property: 'name')

Value for a column can also be obtained from a closure, such as:

// 'it' refers to the object that will be displayed
glazedColumn(name: 'Total', expression: {it.total()}) 

The following glazedColumn() will set the column's minWidth, preferredWidth and maxWidth to the same value:

glazedColumn(name: 'Total', expression: {it.total()}, width: 30) 

To set data type for a column, use columnClass as shown in the following code:

glazedColumn(name: 'Total', expression: {it.total()}, 
   columnClass: Integer) 

glazedColumn() can have their own renderer, for example:

glazedColumn(name: 'Date', property: 'birthDate') {
   templateRenderer("\${it.toString('dd-MM-yyyy')}")
}

or

glazedColumn(name: 'Date', property: 'birthDate') {
   templateRenderer(templateExpression: {it.toString('dd-MM-yyyy')})
}

templateRenderer() will return an implementation of TableCellRenderer that will format column value based on a template String or a closure expression.

templateRenderer() and any renderers annotated by @ConditionSupport will accept condition() node that will evaluate value and change the renderer properties when table is viewed. For example, the following templateRenderer() will display font in red color if its date value is after today:

templateRenderer(templateExpression: {it.toString('dd-MM-yyyy')}) {
   condition(if_: {it && LocalDate.now().isAfter(it)}, 
      then_property_: 'foreground', is_: Color.RED, 
      else_is_: Color.BLACK)
   condition(if_: {isSelected}, then_property_: 'foreground',
      is_: Color.WHITE)
}

Inside the closure value for if_, then_ or else_ of condition() node, developer can use the following variables:

  • table - refers to the current table.
  • value - refers to the unformatted value.
  • isSelected - true if this is a selected cell.
  • hasFocus - true if this cell has focus.
  • row - refers to row number.
  • column - refers to column number.
  • component - refers to the renderer itself.

The following code show how to use custom renderer that has @ConditionSupport:

templateRenderer(...) {
    customConditionalRenderer(new MyCustomRenderer()) {
        condition(...)
        condition(...)
        condition(...)
    }
}

11. Integration Testing

Work in progress...