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.
This version is not compatible with Griffon 2.0. You will need Griffon 1.5 and upgrade its Groovy version to at least Groovy 2.3. |
2. Getting Started
This section will help you write your first simple-jpa application. You should have installed JDK 7 and Griffon 1.5 if you haven’t done so. You can find more information about Griffon’s installation in http://griffon.codehaus.org/guide/latest/guide/gettingStarted.html.
simple-jpa binary is compiled using Groovy 2.3 while the latest Griffon shipped with Groovy 2.2. Unfortunately,
Groovy 2.3 binary is not backward compatible with previous version. If you run simple-jpa in Griffon 1.5, you
will encouter strange Exception
such as java.lang.NoClassDefFoundError: org/codehaus/groovy/runtime/typehandling/ShortTypeHandling
.
To fix this, you can either upgrade Griffon to use Groovy 2.3 or rebuild simple-jpa from source. See http://www.jroller.com/aalmiray/entry/running_griffon_with_an_alternate for
more information on upgrading Groovy used by Griffon.
The article explains how to edit griffon-cli-1.5.0.jar by using CLI. As an alternative, you can
also open griffon-cli-1.5.0.jar in 7zip, right click build.properties and select Edit.
|
You can execute the following command to determine Groovy version used by Griffon:
C:\> griffon -v
If Griffon has been setup properly, you are ready to create your first Griffon application:
C:\> griffon create-app myapp
Griffon creates and stores your new project in a folder named myapp
. You should move to this folder:
C:\> cd myapp
Install simple-jpa plugin by executing the following script:
C:\myapp> griffon install-plugin simple-jpa
The first thing to do when using simple-jpa is calling create-simple-jpa
to setup persistence layer in your application.
Don’t forget to make sure your database server is ready. If you don’t have a database server installed in your computer,
you can use Apache Derby as embedded database by executing the following script:
C:\myapp> griffon create-simple-jpa -user=steven -password=12345 -database=C:/mydb -jdbc=derby-embedded
You don’t need to install anything to use Apache Derby as embedded database because it is embedded in your application.
Your data will be stored in C:\mydb
. If you accidentally delete this folder, you need to recreate this folder by
appending create=true
to JDBC URL in griffon-app\conf\metainf\persistence.xml
:
<property name="javax.persistence.jdbc.url" value="jdbc:derby:C:/mydb;create=true" />
Lets create your first domain classes:
C:\myapp> griffon create-domain-class Invoice LineItem
You can their properties and methods by changing the content of Invoice.groovy
and LineItem.groovy
in C:\myapp\src\main\domain
:
package domain
import groovy.transform.*
import simplejpa.DomainClass
import javax.persistence.*
import org.hibernate.annotations.Type
import javax.validation.constraints.*
import org.hibernate.validator.constraints.*
import org.joda.time.*
@DomainClass @Entity @Canonical
class Invoice {
@NotBlank
String number
@NotNull @Type(type="org.jadira.usertype.dateandtime.joda.PersistentLocalDate")
LocalDate date
@ElementCollection(fetch=FetchType.EAGER) @NotEmpty
List<LineItem> items = []
void add(LineItem item) {
items << item
}
BigDecimal total() {
items.sum { it.total() }
}
}
package domain
import groovy.transform.*
import simplejpa.DomainClass
import javax.persistence.*
import org.hibernate.annotations.Type
import javax.validation.constraints.*
import org.hibernate.validator.constraints.*
import org.joda.time.*
@Embeddable @Canonical
class LineItem {
@NotBlank @Size(min=2, max=100)
String productName
@NotNull @Min(value=1l)
Integer qty
@NotNull @Min(value=0l)
BigDecimal price
BigDecimal total() {
qty * price
}
}
Now, you can instruct simple-jpa to generate basic presentation layer (scaffolding):
C:\myapp> griffon generate-all * -startupGroup=MainGroup
If you change your domain classes later, you can overwrite the generated code by calling: C:\myapp> griffon generate-all * -startupGroup=MainGroup -forceOverwrite |
Try to run your application:
C:\myapp> griffon run-app
You’ve just created Swing-based Java application that uses JPA. It is a working application! You can add new record, edit or delete existing record. You can search for existing records. You can also double-click on table row to modify line items.
If you close and re-run your application, tables will be recreated to reflect the latest change in domain classes. You can
disable this by remove the following line from griffon-app/conf/metainf/persistence.xml
:
<property name="javax.persistence.schema-generation.database.action" value="drop-and-create" />
If you think logger is too noisy, you change log4j
configuration in griffon-app/conf/Config.groovy
into:
log4j = {
appenders {
console name: 'stdout', layout: pattern(conversionPattern: '%d [%t] %-5p %c - %m%n')
}
root {
error 'stdout'
}
}
Now if you close and re-run your application, existing records are still there because they’re persisted in C:\mydb
by
Apache Derby.
Want to learn more about programming with simple-jpa? Close your application and execute the following script:
C:\myapp> griffon simple-jpa-console
This will launch Groovy Console where you can input Groovy code and execute them on the fly. Click on simple-jpa, MVC Groups in the menu bar and place a checkmark in invoice MVC group. Type the following code and press Ctrl + R to run it:
Try to delete existing code in Groovy console (without closing or relaunching a new console) and type a different one, such as:
invoiceController.findAllInvoice()
Press Ctrl + R to execute this script and the result will be displayed right away. By using simple-jpa-console
,
you easily test the result of finders or experiment on the right JP QL without relaunching your application.
3. Persistence
By default, simple-jpa uses Hibernate JPA as persistence layer for your application. You have to execute create-simple-jpa
to setup persistence layer in your application. This is the first script you will need to execute when you start a
new project.
3.1. Setup
In order to setup your persistence layer, you need to call create-simple-jpa
script. This is usually performed once
for every new project. create-simple-jpa
will create database user and schema for you if they don’t exists. Current version
of simple-jpa only supports MySQL Server and Apache Derby Embedded setup. If your database is not supported, you should
configure your database manually.
If you use MySQL Server, you can execute create-simple-jpa
like:
griffon create-simple-jpa -user=steven -password=12345 -database=sample -provider=hibernate -jdbc=mysql -rootPassword=secret
Because hibernate
is default value for -provider
and mysql
is default value for -jdbc
, you can ommit them:
griffon create-simple-jpa -user=steven -password=12345 -database=sample -rootPassword=secret
create-simple-jpa
will check if sample
database is exists or not. If sample database doesn’t exists, it will be created.
Because this operation requires root user and its password, you need to provide a value in -rootPassword
. This password
won’t be saved in your application. create-simple-jpa
also creates new database user called steven with default password
12345 if it doesn’t exists yet. This user will have full privilleges on sample database.
If you want to use Derby Embedded, you can execute create-simple-jpa
like:
griffon create-simple-jpa -user=steven -password=12345 -database=C:/Users/steven/mydb -jdbc=derby-embedded -rootPassword=secret
When using -jdbc=derby-embedded
, it is better to use absolute path for -database
value. Derby embedded database will
be created at the specified location. The draw back of using absolute path is you must make sure database is copied
to the proper location when you distribute your application.
Root password is not required when using Derby embedded. If you didn’t specify root password, create-simple-jpa
will
create root user with password equals to user password (in this sample, it is 12345).
In addition to setup your database, create-simple-jpa
also performs the following steps:
-
Create
griffon-app/conf/metainf/persistence.xml
that contains information required to connect to database, such as JDBC URL, database username and password. -
Create
griffon-app/conf/metainf/orm.xml
that registerAuditingEntityListener
. This is required if you want to use Auditing feature. -
Create
griffon-app/i18n/ValidationMessages.properties
. You can edit this file if you want to change Validation Message. -
Add dependencies to JPA provider and JDBC driver in
griffon-app/conf/BuildConfig.groovy
.
If you want create-simple-jpa
to perform the operations above without touching your database, use -skipDatabase
argument:
griffon create-simple-jpa -user=steven -password=12345 -database=sample -skipDatabase
You can instruct your JPA provider to recreate database objects (tables) based on current domain classes by executing
generate-schema
script:
griffon generate-schema -target=database -action=drop-and-create
Rather than directly executing in target database, generate-schema
can also store the generated SQL statements in a file:
griffon generate-schema -target=script -action=create -createTarget=mydatabase.sql
The command above will create mydatabase.sql
in current directory. This file contains SQL statements to create
tables required by your application.
3.2. JPA Configurations
By default, JPA provider will read persistence layer configurations stored in persistence.xml
. The following is a
sample persistence.xml
created by create-simple-jpa
script:
<?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.jpa.HibernatePersistenceProvider</provider>
<class>domain.Product</class>
<class>domain.LineItem</class>
<class>domain.Invoice</class>
<properties>
<property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost/exercises"/>
<property name="javax.persistence.jdbc.user" value="steven"/>
<property name="javax.persistence.jdbc.password" value="12345"/>
<property name="hibernate.connection.autocommit" value="false"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect"/>
<property name="hibernate.connection.provider_class" value="org.hibernate.c3p0.internal.C3P0ConnectionProvider"/>
<property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
<property name="jadira.usertype.autoRegisterUserTypes" value="true"/>
</properties>
</persistence-unit>
</persistence>
You can add new or edit existing JPA configurations inside <properties/>
. For example, setting
javax.persistence.schema-generation.database.action
to drop-and-create
makes your JPA provider to recreate
database tables everytime application is launched. You may want to disable this to make application startup faster. If
you disable auto schema generation, you can still use generate-schema
to create database tables manually.
In addition to persistence.xml
, simple-jpa also reads JPA configurations stored in Config.groovy
and external
properties file. If more than one property are found, simple-jpa uses the value based on the following order:
-
Configuration passed as system properties (must start with
javax.persistence
) have the first priority. -
Configuration stored in properties file have the first priority.
-
Configuration stored in
Config.groovy
. -
Configuration stored in
persistence.xml
.
The following lines show a sample configurations added to Config.groovy
:
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'
}
}
}
}
The advantage of storing configurations in Config.groovy
is you can have different JPA configurations per Griffon
environments. For example, the following configurations use different database for different environments:
griffon {
simplejpa {
entityManager {
properties {
environments {
development {
javax.persistence.jdbc.url = 'jdbc:mysql://localhost/exercises'
javax.persistence.jdbc.user = 'steven'
javax.persistence.jdbc.password = '12345'
}
test {
javax.persistence.jdbc.url = 'jdbc:mysql://localhost/test'
javax.persistence.jdbc.user = 'test'
javax.persistence.jdbc.password = 'secret'
javax.persistence.'schema-generation'.database.action = 'drop-and-create'
}
}
}
}
}
}
If you run the application by using command like run-app
, it will activate development
environment. In this case, JPA
uses database jdbc:mysql://localhost/exercises
. If you activate test
environment (for example by running test-app
),
JPA uses database jdbc:mysql://localhost/test
. It also recreate tables in that database.
To avoid storing sensitive information such as JDBC URL, username dan password in your source code, you
can take advantage of Griffon feature to include properties file from Config.groovy
. For example, if you store JPA
configuration to hibernate.properties
, you can add the following line to Config.groovy
to include your properties file:
griffon.config.locations = ['classpath:hibernate.properties']
You may want to configure your source code repository to make sure your properties file will never committed to public server.
simple-jpa can also be configured to read JPA properties from external properties file. You can define the location of this
properties file by adding the following line to Config.groovy
:
griffon.simplejpa.entityManager.propertiesFile = 'C:/example/db.properties'
A sample db.properties
will look like:
javax.persistence.jdbc.url = 'jdbc:mysql://localhost/test'
javax.persistence.jdbc.user = 'test'
javax.persistence.jdbc.password = 'secret'
Configurations stored in this properties file will override existing configurations in Config.groovy
and persistence.xml
,
but can be overriden by JVM system properties. If you didn’t change griffon.simplejpa.entityManager.propertiesFile
,
it defaults to simplejpa.properties
. This means you can always override JPA configuration for existing distribution by
adding a new file called simplejpa.properties
in the same location when you launched the application.
simple-jpa supports obfuscating configuration value. For example, if you want to store obfuscated version of 12345, you need to execute the following command to retrieve its obsfuscated version:
griffon obfuscate -generate='12345'
Then, you can use the obfuscated version for any value in any location (such as Config.groovy
, properties file or
system propeties). For example, you can add the following line to Config.groovy
:
griffon {
simplejpa {
entityManager {
properties {
javax.persistence.jdbc.password = 'obfuscated:YUqF9w6l5lpvNyH+1tnJBg=='
}
}
}
}
While obfuscation makes it harder for lay people to read your configuration file, it doesn’t actually increase
your security. Anyone can easily display the original string by executing griffon obfuscate -reverse . You should
never publish obfuscated value if you don’t want people to know the original value.
|
3.3. Domain Class
simple-jpa provides persistence methods to deal with persistent domain class (marked by @Entity
). To create such
entity, you can use create-domain-class
script, for example:
griffon create-domain-class Invoice LineItem
The command creates two new classes: domain.Invoice
and domain.LineItem
. It also add these classes to persistence.xml
.
You can change the default base packages for domain classes by setting griffon.simplejpa.domain.package
in Config.groovy
.
You can also specify subpackage when executing create-domain-class
, for example:
griffon create-domain-class sales.Invoice sales.LineItem inventory.Product
The command creates three new classes: domain.sales.Invoice
, domain.sales.LineItem
and domain.inventory.Product
.
Persistent domain classes in simple-jpa is a normal JPA entities. Just like when using JPA in Java, you can
decorate JPA entity with JPA annotations such as @Entity
, @OneToMany
, @ManyToMany
, @ManyToOne
and others.
See JPA documentation for more information about JPA annotations. The following show examples of persistent domain
classes in simple-jpa:
package domain
// import statements are not shown.
@DomainClass @Entity @Canonical
class Invoice {
@NotEmpty @Size(min=5, max=5)
String number
@NotNull @Type(type="org.jadira.usertype.dateandtime.joda.PersistentLocalDate")
LocalDate date
@ElementCollection @OrderColumn @NotEmpty
List<LineItem> items = []
void add(LineItem item) {
items << item
}
BigDecimal total() {
items.sum { it.total() }
}
}
package domain
// import statements are not shown.
@Embeddable @Canonical
class LineItem {
@NotNull @ManyToOne
Product product
@NotNull @Min(0l)
BigDecimal price
@NotNull @Min(1l)
BigDecimal qty
BigDecimal total() {
price * qty
}
}
package domain
// import statements are not shown.
@DomainClass @Entity @Canonical
class Product {
@NotEmpty @Size(min=2, max=50)
String name
@NotNull @Min(value=1l)
BigDecimal retailPrice
}
@DomainClass
is special annotation provided by simple-jpa. This annotation automatically adds the following property
to the annotated class:
@Id @GeneratedValue(strategy=GenerationType.TABLE) (1)
Long id (2)
String deleted = 'N' (3)
Date createdDate (4)
String createdBy (4)
Date modifiedDate (4)
String modifiedBy (4)
1 | To change generation strategy, add desired strategy to idGenerationStrategy attribute. |
2 | @DomainClass(excludeId=true) will not generate this property. |
3 | @DomainClass(excludeDeletedFlag=true) will not generate this property. |
4 | @DomainClass(excludeAuditing=true) will not generate these properties. |
You aren’t required to add @DomainClass
to every persistent domain classes, but some features such as Auditing will
not work without the properties generated by @DomainClass
. Of course, you can still code by hand the required properties in
every entities.
3.4. Persistence Methods
To make it possible for Griffon’s artifacts to manage domain classes, simple-jpa injects persistence methods to them.
By default, persistence methods are injected into controller, but you can change it by adding the following line to Config.groovy
:
griffon.simplejpa.finders.injectInto = [ 'service', 'repository' ]
The configuration above will inject persistence methods to services and repositories. Repository is a custom artifact type
provided by simple-jpa. You can create a new repository by using griffon create-repository
, for example:
griffon create-repository MyRepository
The script creates MyRepository.groovy
in griffon-app/repositories
.
To retrieve instance of repository, you can use code like:
def myRepository = SimpleJpaUtil.instance.repositoryManager.findRepository('MyRepository')
For a simple application, it is usually acceptable to inject persistence methods to controllers. If you want a clear
separation, you should inject persistence methods only to repositories. You must add @Transaction
annotation to
the injected artifacts to enable Transaction. Class generated by create-repository
already has @Transaction
,
but Griffon’s controllers do not have @Transaction
by default.
The following is list of persistence methods injected by simple-jpa:
-
persist(entity)
Use this method to save new entity. It is a shortcut for
entityManager.persist()
. -
merge(entity)
Use this method to add detached entity to current EntityManager. It is a shortcut for
entityManager.merge()
. -
remove(entity)
Delete an entity. It is a shortcut for
entityManager.remove()
. -
softDelete(entity)
Set
deleted
property of an entity into'Y'
. -
validate(object)
See Validation for more information.
-
Finders
See Finders for more information.
-
getEntityManager()
Returns an
EntityManager
for current session. This method can’t be renamed. -
Transaction methods
See Transaction Methods for more information.
-
All public methods of
EntityManager
.simple-jpa exposes all public methods of
EntityManager
to injected class. This means you can directly call methods such aslock()
,refresh()
, ordetach()
in injected class. For more information aboutEntityManager
, see JPA documentation. These methods can’t be renamed.
All persistence methods are public so you can still call them from different class. |
To avoid conflict with existing methods in injected class, simple-jpa can add prefix to persistence methods. For example,
you can add the following line to Config.groovy
:
griffon.simplejpa.finders.prefix = 'jpa'
Now, every persistence methods that can be renamed will have jpa prefix. For example, you have to execute jpaPersist()
rather than persist()
.
The easiest way to learn simple-jpa persistence methods is by using simple-jpa-console
script:
griffon simple-jpa-console
It will launch a Groovy console where you can write code snippet and see the result.
The main advantage of Groovy console is you can edit existing code and execute it directly by selecting Script, Run (Ctrl+R).
This is many times faster than relaunching application by using griffon run-app
. You will find Groovy console very useful
in testing your code snippet or understanding the result of persistence methods.
4. Scaffolding
Scaffolding feature from simple-jpa can be used to generate MVC groups based on existing domain classes. This is very
useful in the early phase of application development as it provides guide for further development.
Scaffolding can be invoked by using generate-all
command or by enabling it in
Config.groovy
.
For example, MVC groups for domain class Student
can be generated by using the following command:
griffon generate-all Student
simple-jpa will search for definition of Student
class based on information from griffon-app/conf/metainf/persistence.xml
.
If it is found, simple-jpa will perform the following actions:
-
Create a new
griffon-app/models/project/StudentModel.groovy
if it doesn’t exists. -
Create a new
griffon-app/views/project/StudentView.groovy
if it doesn’t exists. -
Create a new
griffon-app/controllers/project/StudentController.groovy
if it doesn’t exists. -
Search for
student
MVC group. If it doesn’t exists, create a new MVC group with the following content:Application.groovy// MVC Group for "student" 'student' { model = 'project.StudentModel' view = 'project.StudentView' controller = 'project.StudentController' }
-
Create a new
test/integration/project/StudentTest.groovy
if it doesn’t exists. -
Create a new
data.xls
if it doesn’t exists. Add a single empty sheet named asstudent
to the Microsoft Excel file. This Excel file will act as data source when invokingStudentTest.groovy
. See Testing for more information. -
Add messages to
griffon-app/i18n/messages.properties
if they don’t exists. -
Add a generic exception handler in
griffon-app/conf/Events.groovy
if it doesn’t exists.
By default, simple-jpa will not modify the content of existing files. To force simple-jpa to overwrite existing
files, add -forceOverwrite
parameter when invoking generate-all
. For example:
griffon generate-all -forceOverwrite Student
The default target package for generated MVC artifacts is project
. To use a different target package,
-generatedPackage
should be passed when invoking generate-all
. For example:
griffon generate-all -generatedPackage=com.jocki.exercises Student
The command will place generated MVC artifacts in com.jocki.exercises
package and register the following MVC group:
// MVC Group for "student"
'student' {
model = 'com.jocki.exercises.StudentModel'
view = 'com.jocki.exercises.StudentView'
controller = 'com.jocki.exercises.StudentController'
}
simple-jpa is smart enough to recognize package hierarchy in domain classes. It will try to preserve the
source package hierarchy in the resulting MVC artifacts. For example, if Teacher
is located in domain.staff
,
then the following command:
griffon generate-all -generatedPackage=com.jocki.exercises Teacher
will place the generated MVC artifacts in com.jocki.exercises.staff
and register the following MVC group:
// MVC Group for "teacher"
'teacher' {
model = 'com.jocki.exercises.staff.TeacherModel'
view = 'com.jocki.exercises.staff.TeacherView'
controller = 'com.jocki.exercises.staff.TeacherController'
}
In Griffon, startup group is the first MVC group to be instantiated and displayed. By default, generate-all
will
not modify the value of current startup group. This behaviour can be changed by using -setStartup
parameter. If
-setStartup
is passed to generate-all
, the command will set startup group to the generated MVC group. For
example, the following command will generate teacher
MVC group and set it as startup group:
griffon generate-all -setStartup Teacher
Since 0.8, simple-jpa generates view as a panel. Because the generated view doesn’t have top level container
such as |
simple-jpa can also create a dedicated startup group that serves as container for existing domain class
based MVC groups. To generate a dedicated startup group, -startupGroup
parameter should be used with
the startup group’s name as its value. For example, the following command will generate mainGroup
MVC group and set
it as startup group:
griffon generate-all -startupGroup=MainGroup
The generated startup group’s view has a toolbar to launch domain class based MVC groups. It uses MainTabbedPane
to display
selected MVC group.
You can add a new tab to main group from any MVC group by using code like:
MainGroupView mainView = app.getMvcGroupManager()['mainGroup'].view
mainView.mainTab.addMVCTab('mvcGroupName', [arg1: param1], "Tab Caption")
addMVCTab()
always create new MVC group and display it in a new tab. This means you can have multiple unrelated instances of the
same MVC group.
Domain class based MVC group generation and startup group generation can also be combined in a single command execution. For example:
griffon generate-all Teacher -startupGroup=MainGroup
New MVC groups may be generated in the future after startup group has been created. These new MVC groups will not
appear in current startup group’s toolbar. One of possible solutions is recreate the startup group by including
griffon generate-all -startupGroup=MainGroup -forceOverwrite |
generate-all
can accept multiple domain classes separated by space. For example:
griffon generate-all Student Teacher Classroom
As a shortcut, *
can be used as domain class name to generate all registered domain classes. For example:
griffon generate-all *
Since version 0.8, scaffolding in simple-jpa is performed by an instance of Generator
. The default generator
that shipped with simple-jpa is simplejpa.scaffolding.generator.basic.BasicGenerator
. To select another generator,
add -generator
parameter when invoking generate-all
. For example:
griffon generate-all -generator=my.custom.Generator *
Parameters that have been discussed so far can also be stored in Config.groovy
. The following table lists all possible
values that can be added to Config.groovy
:
Key | Expected Value | Description |
---|---|---|
|
|
If |
|
A string. |
The full class name that is instance of |
|
A string. |
The location of target package for MVC artifacts. Default value is |
|
A string. |
The name of startup group. If it is not defined, no startup group will be generated. |
|
|
If |
|
|
If |
|
|
If |
|
|
Represent formatting styles for auditing properties that are instances of |
|
A string that consists of domain class names or |
List of domain classes to generate. If |
For example, the configurations below:
griffon {
simplejpa {
scaffolding {
generatedPackage = 'com.jocki.exercises'
startupGroup = 'MainGroup'
forceOverwrite = true
}
}
}
is identical with executing the following command:
griffon generate-all -generatedPackage=com.jocki.exercises -startupGroup=MainGroup -forceOverwrite *
Another benefit of storing generate-all
parameters as configuration keys is automatic scaffolding. The scaffolding
process can be automated if griffon.simplejpa.scaffolding.auto
is set to true
. For example, this configuration will
automatically run scaffolding process:
griffon {
simplejpa {
scaffolding {
auto = true
startupGroup = 'MainGroup'
}
}
}
Automatic scaffolding will be performed whenever Griffon is compiling classes, such as when project is launched from
griffon run-app
command. Automatic scaffolding will not run in production when no class compilations being carried
out.
4.1. Basic Generator
Basic generator is the default generator used by simple-jpa. It supports the following attribute types in domain class:
Attribute Type | SwingBuilder node | Class |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Any Entity Object |
|
|
Basic generator doesn’t support native data types such as int , float , or double because they
are not nullable.
|
Basic generator will generate TODO comments in the generated code to provide information or warning to user. It is safe to delete these TODO comments. |
For example, the following domain class:
@DomainClass @Entity @Canonical
class Student {
String name
Integer age
@Type(type = "org.jadira.usertype.dateandtime.joda.PersistentLocalDate")
LocalDate birthDate
Boolean registered
@Enumerated
GRADE grade
@ManyToOne
Teacher teacher
@ManyToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
List<Classroom> classrooms = []
}
enum GRADE {
GRADE_1, GRADE_2, GRADE_3
}
will be generated as:
To create a new record, user should enter required values in the editing area and click the 'Save’ button.
To update existing record, user must first select a row in table, enter the updated values in the editing area, and click the 'Save’ button.
To remove record from database, user must click the 'Delete’ button that will appear if table’s row is selected.
For domain class that have one-to-one association, basic generator generates a dialog to create, edit or remove the related entity. This feature requires cascading to be activated for the attribute.
For example, the following attribute declaration:
@OneToOne(cascade=CascadeType.ALL, orphanRemoval=true)
Teacher teacher
is represented by JButton
that if clicked will open a dialog that allows user to modify the related
Teacher
.
If this button is clicked in create operation, the dialog can be used to create a new instance of Teacher
entity:
If this button is clicked in update operation, the dialog can be used to update or delete existing Teacher
entity:
The naming convention for one-to-one MVC group and its artifact’s name is the target entity name
with 'AsPair’ as suffix. For example, if target entity is Teacher
, basic generator generates
TeacherAsPairModel
, TeacherAsPairView
, and TeacherAsPairController
. They are not to be confused with the
standalone MVC group for Teacher
such as TeacherModel
, TeacherView
, and TeacherController
that may also exists
in the project (for example they are used for displaying list of Teacher
in their own screen and not as popup).
If the annotated attribute in the generated code doesn’t have cascade set to CascadeType.ALL ,
org.hibernate.TransientPropertyValueException will be raised when saving the entity. To fix this, either add a
proper cascading to object mapping or change the generated code to make it work without cascading.
|
Basic generator also treats @Embedded
attributes as equivalent of one-to-one attributes. For example, the
following mapping generates the same view as the previous one:
@Embedded
Teacher teacher
User can press ESC button as a shortcut to close popup dialog. |
User can press Enter or double click a selected row in table to display the first one-to-many or one-to-one popup dialog in the view. |
For one-to-many associations, basic generator also generates a dialog. For example, the following attribute declaration:
@OneToMany(cascade=CascadeType.ALL, orphanRemoval=true, fetch=FetchType.EAGER)
Set<Classroom> classrooms = new HashSet<>()
is represented by JButton
that if clicked will open a dialog that allows user to add or remove the list of
Classroom
entities that are associated with current entity. If this button is clicked in create operation, the
dialog can be used to populate the collection with one or more Classroom
:
If this button is clicked in update operation, the dialog can be used to add new entity to the collection, edit the value of entity in the collection, or remove an entity from the collection:
The naming convention for one-to-many attribute is the target entity name with 'AsChild’ as suffix. For example, if
target entity is Classroom
, basic generator generates ClassroomAsChildModel
, ClassroomAsChildView
, and
ClassroomAsChildController
. This is not to be confused with ClassroomModel
, ClassroomView
or
ClassroomController
that may also exists in the project.
Basic generator will also treat @ElementCollection
attributes as equivalent of one-to-many attributes. For
example, the following mapping creates the same view as the previous one:
@ElementCollection(fetch=FetchType.EAGER)
Set<Classroom> classrooms = new HashSet<>()
For bidirectional associations, basic generator generates inverse attributes as labels because they are not editable. For example, the following domain classes:
@DomainClass @Entity @Canonical
class Invoice {
String number
@OneToOne(cascade=CascadeType.ALL, orphanRemoval=true)
Delivery delivery
}
@DomainClass @Entity @Canonical(excludes='invoice')
class Delivery {
String ticketNumber
@OneToOne(mappedBy='delivery')
Invoice invoice
}
will be generated as:
Don’t forget to add excludes to @Canonical in bidirectional association to avoid infinitive recursion!
|
4.2. DDD Generator
simple-jpa also shipped with a DDD generator that can selected by using -generator
parameter such as in the following
execution:
griffon generate-all -generator=simplejpa.scaffolding.generator.ddd.DDDGenerator *
DDD generator rely on basic generator to perform most of its works. It generates views that are identical to those generated by basic generator. The only distinction is this generator will move JPA related methods and database transactions from controller into a separate repository.
For every @Entity
annotated domain classes, DDD generator will create their corresponding repository classes.
simple-jpa supports a custom Griffon’s artifact called repository
. For example, if there is an entity called
Student
, DDD generator will create a new repository called StudentRepository
in griffon-app/repositories
. This
artifact is a singleton. It is automatically injected into controller (and other Griffon’s artifacts) by defining
a variable such as studentRepository
.
To retrieve all repositories, use RepositoryManager.getRepositories()
such as:
def repositories = SimpleJpaUtil.instance.repositoryManager.repositories
println "All repositories: $repositories"
Repository artifact is lazy initialized. This means it won’t be created if it is not used. RepositoryManager.findRepository()
can be used to retrieve an instance of repository. This method will create a new instance that will be reused later (singleton)
when it is called for the first time.
InvoiceRepository repository = SimpleJpaUtil.instance.repositoryManager.findRepository('Invoice')
DDD generator disables dynamic methods in controller and adds dynamic methods to repository by adding the following line to Config.groovy
:
griffon.simplejpa.finders.injectInto = ['repository']
4.3. Customization
A generator usually has classes and multiple templates. Reusable logic for view generation is stored in
classes. They will be invoked by templates. For example, the generator class is always available to template as g
variable.
The easiest way to customize a generator is to modify its template. install-templates
command can be used to
install templates for simple-jpa built-in generator to current project:
griffon install-templates
The command will copy template files to src/templates/artifacts
. The following is list of all template files used by
simple-jpa built-in generators:
Name | Generator | Purpose |
---|---|---|
|
create-domain-class |
Domain class generation |
|
DDD |
Repository for entity |
|
Basic, DDD |
Startup group’s model |
|
Basic, DDD |
Startup group’s controller |
|
Basic, DDD |
Startup group’s view |
|
Basic, DDD |
Domain-class based model |
|
Basic, DDD |
Domain-class based view |
|
Basic |
Domain-class based controller |
|
DDD |
Domain-class based controller |
|
Basic, DDD |
Integration test case |
|
Basic, DDD |
one-to-one popup model |
|
Basic, DDD |
one-to-one popup view |
|
Basic |
one-to-one popup controller |
|
DDD |
one-to-one popup controller |
|
Basic, DDD |
one-to-many popup model |
|
Basic, DDD |
one-to-many popup view |
|
Basic |
one-to-many popup controller |
|
DDD |
one-to-many popup controller |
After these templates have been installed into current project, the next invocation of generate-all
will be based on
them.
For a more complex customization, a new generator may be created. The new generator can be extended from existing
generator or simplejpa.scaffolding.generator.Generator
. All generators have the following important methods:
-
generate(DomainClass domainClass)
will be invoked when generating files for individual domain class. -
generateStartupGroup(Map<String,DomainClass> domainClasses)
will be invoked when generating startup group. -
generateExtra(Map<String,DomainClass> domainClasses)
is an optional method that will be invoked after generating files for individual domain class.
For example, the following is a sample declaration of custom generator that does nothing:
package generator
import simplejpa.scaffolding.DomainClass
import simplejpa.scaffolding.Scaffolding
import simplejpa.scaffolding.generator.basic.BasicGenerator
class MyGenerator extends BasicGenerator {
MyGenerator(Scaffolding scaffolding) {
super(scaffolding)
}
@Override
void generate(DomainClass domainClass) {
println "Generating ${domainClass.name}"
// call super.generate(domainClass) for default operation (generating MVC artifacts)
}
@Override
void generateStartupGroup(Map<String, DomainClass> domainClasses) {
println "Generating startup group..."
}
@Override
void generateExtra(Map<String, DomainClass> domainClasses) {
println "Generating extra..."
}
}
The following command will use the custom generator:
griffon -generator=generator.MyGenerator *
5. Validation
simple-jpa uses Bean Validation API (JSR 303) for validation. By default, it uses Hibernate Validator as Bean Validation API implementation. The following are some of common annotations supported by Hibernate Validator:
Annotation | Description |
---|---|
|
Checks that the annotated value is not null |
|
Checks that the annotated character sequence is not null and the trimmed length is greater than 0. |
|
Can be used for Collection to check whether the annotated element is not null or empty. |
|
Checks if the annotated element’s size is between min and max (inclusive). |
|
Checks whether the annotated value is less than or equal to the specified maximum. |
|
Checks whether the annotated value is higher than or equal to the specified minimum. |
These annotations can be added to properties of domain class, for example:
@Canonical(excludes='lineItems')
class Invoice {
@NotBlank @Size(min=10, max=10)
String number
@NotNull @Type(type="org.jadira.usertype.dateandtime.joda.PersistentLocalDate")
LocalDate date
@ElementCollection @OrderColumn @NotEmpty
List<ItemFaktur> lineItems = []
}
5.1. Validation Method
You can use validate()
method from injection enhanced artifact to validate an object. The first argument for this
method is an object that will be validated. It is the only required argument. validate()
returns true
if requested
object is successfully validated or false
if it failed.
For example, the following code will print error message to console if invoice object is not valid:
// The following code assumes that controller is injection enhanced.
// You can also choose to enabled injection to other artifact such as repository.
if (controller.validate(invoice)) {
println "invoice is valid."
} else {
println "invoice is not valid."
}
The second parameter for validate()
determine what validation group is in effect. The default value is Default
group.
To use a different group, you can pass the group class to validate()
, for example:
@Canonical(excludes='lineItems')
class Invoice {
@NotBlank(groups=[FirstStep,SecondStep])
@Size(min=10, max=10, groups=[FirstStep,SecondStep])
String number
@NotNull(groups=[FirstStep,SecondStep])
@Type(type="org.jadira.usertype.dateandtime.joda.PersistentLocalDate")
LocalDate date
@ElementCollection @OrderColumn @NotEmpty(groups=[SecondStep])
List<ItemFaktur> lineItems = []
}
// empty line items is acceptable and treated as valid
if (!controller.validate(invoice, FirstStep)) {
println "first step validation for invoice is not valid!"
}
// now, empty line items is not valid
if (!controller.validate(invoice, SecondStep)) {
println "second step validation for invoice is not valid!"
}
The third parameter accepts an instance of view model (Griffon’s model). Use this to present error in presentation layer by highlighting related view’s component. See the next section for more information about error notification.
5.2. Notification In View
To help you in displaying information about failed validation, simple-jpa injects the following members to all view models (model in MVC group):
Members Declaration | Description |
---|---|
|
This is a bindable map that stores validation messages. |
|
Return |
All builder’s nodes also has a special errorPath
attribute that can be used to represent associated validation path
(like property name of validated objects). If errors
contains key equals to errorPath
, the respective
component will be highlighted.
For example, the following code will highlight the first JTextField
:
application(pack: true) {
flowLayout()
textField(columns: 20, errorPath: 'firstName')
textField(columns: 20, errorPath: 'lastName')
}
class MyController {
def model
def view
void mvcGroupInit(Map args) {
model.errors['firstName'] = 'First name is not valid!'
}
}
When you start typing on the highlighted JTextField
, its red background will disappear. This also remove existing
entry in model.errors
.
By default, simple-jpa use red background to notify failed validation. You can also create your own notification method by
extending ErrorNotification
. For example, the following implementation of ErrorNotification
will highlight
border on error:
package main
import java.awt.Color
import javax.swing.*
import javax.swing.border.*
import simplejpa.validation.*
class MyErrorNotification extends ErrorNotification {
Border highlightBorder = BorderFactory.createLineBorder(Color.RED)
Border normalBorder
public MyErrorNotification(JComponent node, ObservableMap errors, String errorPath) {
super(node, errors, errorPath)
normalBorder = node.getBorder()
}
void performNotification() {
if (errors[errorPath]) {
node.setBorder(highlightBorder)
} else {
node.setBorder(normalBorder)
}
}
}
To assign MyErrorNotification
to a single component, you can use errorNotification
attribute:
textField(columns: 20, errorPath: 'firstName', errorNotification: main.MyErrorNotification)
To use MyErrorNotification
on all components, add the following line to Config.groovy
:
griffon.simplejpa.validation.defaultErrorNotificationClass = 'main.MyErrorNotification'
The component responsible for removing error message is called ErrorCleaner
. The following table lists all
implementations of ErrorCleaner
by default:
Component | Implementation | Activation Condition |
---|---|---|
|
|
When user typed in text box. |
|
|
When user changed selection. |
|
|
When user clicked the button. |
|
|
When user changes date. |
|
|
When user changes selection. |
|
|
When user changes selection. |
You can create your own implementation by creating a new class that implements ErrorCleaner
. For example, the following
implementation removes error notification after one second:
package main
import simplejpa.validation.*
import javax.swing.*
import java.awt.event.ActionListener
class MyErrorCleaner implements ErrorCleaner {
void addErrorCleaning(JComponent component, ObservableMap errors, String errorPath) {
new Timer(1000, {
errors.remove(errorPath)
} as ActionListener).start()
}
}
To register this ErrorCleaner
for all JTextField
, you can add the following line to Config.groovy
:
griffon.simplejpa.validation.errorCleaners = [
'javax.swing.JTextField': 'main.MyErrorCleaner'
]
If you want to apply the new ErrorCleaner
to all components, use '*'
instead of 'javax.swing.JTextField
as key:
griffon.simplejpa.validation.errorCleaners = [
'*': 'main.MyErrorCleaner'
]
5.3. Validation Message
If validation is failed, simple-jpa stores validation message for the failed paths in model.errors
. You can use values
stored in model.errors
to determine which paths have been failed and what are their corresponding messages. You can
also change the default validation message by editing griffon-app\i18n\ValidationMessage.properties
.
The following code shows how to retrieve validation messages:
import groovy.beans.Bindable
import org.hibernate.validator.constraints.NotBlank
class MyModel {
@NotBlank String firstName
String lastName
}
application(pack: true) {
flowLayout()
textField(columns: 20, text: bind('firstName', target: model), errorPath: 'firstName')
textField(columns: 20, text: bind('lastName', target: model), errorPath: 'lastName')
button('Save', actionPerformed: controller.save)
}
class MyController {
def model
def view
def save = {
if (validate(model)) {
println 'Your input have been validated and passed.'
} else {
println 'Validation failed!'
model.errors.each { k, v ->
println "$k $v"
}
// This will display the following error message
// if first name is blank:
//
// Validation failed!
// firstName may not be empty
//
}
}
}
If you want to display validation mesage as JLabel
in view, you can use errorLabel()
node. For example, you can
change the previous view into:
application(pack: true) {
flowLayout()
textField(columns: 20, text: bind('firstName', target: model), errorPath: 'firstName')
errorLabel(path: 'firstName')
textField(columns: 20, text: bind('lastName', target: model), errorPath: 'lastName')
errorLabel(path: 'lastName')
button('Save', actionPerformed: controller.save)
}
errorLabel()
is only visible if model.errors
contains a key equals to its errorPath
.
5.4. Miscellaneous
In some cases, validation may be failed because JTextField
value in model is an empty String
rather than null
value. To ensure consistent behaviour, simple-jpa can be configured to translate all empty String
into null value
before performing validation. This feature is disabled by default. To enable it, add the following line to Config.groovy
:
griffon.simplejpa.validation.convertEmptyStringToNull = true
6. View
To assist you in building presentation layer, simple-jpa provides several custom nodes that are typically used in business application. They can only be used if your presentation layer is Swing based and you are using Groovy’s SwingBuilder (default in Griffon).
6.1. Custom Text Fields
Use numberTextField()
to bind text component with numeric property in view model. For example:
class MyModel {
@Bindable Long amount
}
application(pack: true) {
flowLayout()
numberTextField(columns: 20, bindTo: 'amount')
button('Process', actionPerformed: { println model.amount })
}
If you want to bind to a BigDecimal
property in view model, you need to use decimalTextField()
. For example:
class MyModel {
@Bindable Long amount
}
application(pack: true) {
flowLayout()
decimalTextField(columns: 20, bindTo: 'amount')
button('Process', actionPerformed: { println model.amount })
}
Both numberTextField()
and decimalTextField()
generates a JFormattedTextField
. The default formatter used by
them is DecimalFormat.getNumberInstance()
. You can use different formatter by using one of 'currency'
, 'percent'
,
or 'integer'
as value for type
attribute. For example:
application(pack: true) {
flowLayout()
label('Default')
decimalTextField(columns: 20, bindTo: 'amount')
label('Currency')
decimalTextField(columns: 20, bindTo: 'amount', type: 'currency')
label('Percent')
decimalTextField(columns: 20, bindTo: 'amount', type: 'percent')
label('Integer')
decimalTextField(columns: 20, bindTo: 'amount', type: 'integer')
button('Process', actionPerformed: { println model.amount })
}
You can also change the property of current formatter by using attribute name that starts with nf
and followed by
formatter’s property name, for example:
application(pack: true) {
flowLayout()
label('Default')
decimalTextField(columns: 20, bindTo: 'amount')
label('Custom Formatter')
decimalTextField(columns: 20, bindTo: 'amount',
nfMinimumFractionDigits: 3, nfMinimumIntegerDigits: 5)
button('Process', actionPerformed: { println model.amount })
}
If you want a MaskFormatter
, you can use maskTextField()
:
application(pack: true) {
flowLayout()
label('Default')
maskTextField(columns: 20, mask: 'AAA-#####-##', bindTo: 'identifier')
button('Process', actionPerformed: { println model.identifier })
}
In the sample above, mask 'AAA-####-##'
allows user to input number of letter for the first three characters. The rests
should be number.
6.2. Date Time Picker
You can use dateTimePicker()
to allow user to input date or time (or both). This component should be binded to date
data type from Joda Time library. If you don’t need time input or binding to property that uses Joda Time type, you can
just use JXDatePicker
from SwingX instead of this component.
dateVisible
or timeVisible
of this component determine which input is visible. By default, both values are true
.
application(pack: true) {
flowLayout()
label('Date Only: ')
dateTimePicker(dateVisible: true, timeVisible: false)
label('Time Only:')
dateTimePicker(dateVisible: false, timeVisible: true)
label('Both:')
dateTimePicker()
}
dateTimePicker()
offers several bindable properties:
Property Name | Description |
---|---|
|
Current value as |
|
Current value as |
|
Current value as |
|
Current value as |
|
Current value as |
In most cases, you will need only one of those properties, but you can also bind to multiple properties to get the same value in different types:
import org.joda.time.*
class MyModel {
@Bindable LocalDate myLocalDate
@Bindable LocalDateTime myLocalDateTime
@Bindable DateMidnight myDateMidnight
}
application(pack: true) {
flowLayout()
dateTimePicker(
localDate: bind('myLocalDate', target: model),
localDateTime: bind('myLocalDateTime', target: model),
dateMidnight: bind('myDateMidnight', target: model)
)
label('myLocalDate is ')
label(text: bind {model.myLocalDate})
label('myLocalDateTime is ')
label(text: bind {model.myLocalDateTime})
label('myDateMidnight is ')
label(text: bind {model.myDateMidnight})
}
dateTimePicker()
internally uses JXDatePicker
for date entry and JSpinner
time entry. They are exposed as properties
called datePicker
and timeSpinner
.
If you want to perform a code everytime date or time is changed, you can add a closure to selectedValueChanged
property:
application(pack: true) {
flowLayout()
dateTimePicker(
localDateTime: bind('myLocalDateTime', target: model),
selectedValueChanged: {
println "Current value is ${model.myLocalDateTime.toString('dd-MM-YYYY hh:mm')}"
}
)
}
6.3. Table
Table is one of the most important components in business application. That is why simple-jpa provides its own custom
JTable
called glazedTable()
. It is designed to be binded to EventList
from Glazed Lists library. If you don’t use
Glazed Lists, you can just use the default table()
instead of glazedTable()
.
glazedTable()
should contains one or more glazedColumn()
to represent every columns in table. Every glazedColumn()
will display property or execute method of the object contained in the binded EventList
.
For example, lets assume you have created the following domain classes:
package domain
// import statements...
@DomainClass @Entity @Canonical
class Invoice {
@NotEmpty @Size(min=5, max=5)
String number
@NotNull @Type(type="org.jadira.usertype.dateandtime.joda.PersistentLocalDate")
LocalDate date
@ElementCollection @OrderColumn @NotEmpty
List<LineItem> items = []
void add(LineItem item) {
items << item
}
BigDecimal total() {
items.sum { it.total() }
}
}
package domain
// import statements...
@Embeddable @Canonical
class LineItem {
@NotEmpty @Size(min=2, max=50)
String name
@NotNull @Min(0l)
BigDecimal price
@NotNull @Min(1l)
BigDecimal qty
BigDecimal total() {
price * qty
}
}
A very simple use case of glazedTable()
will be:
import org.joda.time.*
import ca.odell.glazedlists.BasicEventList
class MyModel {
BasicEventList myTable = new BasicEventList()
}
import domain.*
import org.joda.time.*
class MyController {
def model
def view
void mvcGroupInit(Map args) {
Invoice invoice1 = new Invoice('INV01', LocalDate.parse('2015-01-01'))
invoice1.add(new LineItem('Product1', 100, 1))
invoice1.add(new LineItem('Product2', 200, 2))
Invoice invoice2 = new Invoice('INV02', LocalDate.parse('2015-01-02'))
invoice2.add(new LineItem('Product3', 300, 3))
invoice2.add(new LineItem('Product4', 400, 4))
execInsideUISync {
model.myTable << invoice1
model.myTable << invoice2
}
}
}
application(pack: true) {
borderLayout()
scrollPane(constraints: CENTER) {
glazedTable(list: model.myTable) {
glazedColumn(name: 'Invoice Number', property: 'number')
glazedColumn(name: 'Date', property: 'date')
}
}
}
As you can see, name
attribute of glazedColumn()
set column’s caption and property
attribute determine the
column’s value. The following table lists all attributes for glazedColumn()
:
Name | Description |
---|---|
|
Set column caption. |
|
A closure that will be executed to evaluate every row in this column. |
|
A string to represent property that will be displayed for every row in this column. |
|
A |
|
A |
|
A boolean value to determine if this column should be displayed or not. This attribute is bindable. |
glazedColumn()
is an instance of TableColumn
. This means you can also set all of TableColumn
public properties
such as modelIndex
, width
, cellEditor
, and cellRenderer
in glazedColumn()
.
To retrieve column’s value based on custom calculation rather than property name, you can set a closure for expression
attribute. You can also use short name exp
instead of expression
. Inside this closure, it
refer to the object
for current row.
For example, you can add third column which values are retrieved from Invoice.total()
method:
application(pack: true) {
borderLayout()
scrollPane(constraints: CENTER) {
glazedTable(list: model.myTable) {
glazedColumn(name: 'Invoice Number', property: 'number')
glazedColumn(name: 'Date', property: 'date')
glazedColumn(name: 'Total', exp: { it.total() })
}
}
}
You can change the size of a column by using its width
property. If you pass a number to width
, it will set minWidth
,
preferredWidth
and maxWidth
of current TableColumn
to the same value. This means user won’t be able to resize
the column. You can set the value for each of minWidth
, preferredWidth
and maxWidth
by passing a List
to width
.
application(pack: true) {
borderLayout()
scrollPane(constraints: CENTER) {
glazedTable(list: model.myTable) {
glazedColumn(name: 'Invoice Number', property: 'number', width: 50)
glazedColumn(name: 'Date', property: 'date', width: [50, 100])
glazedColumn(name: 'Total', exp: { it.total() }, width: [50, 100, 200])
}
}
}
In the table above, first column is fixed and can’t be resized. The second column can be resized to a minimum 50 pixel and there is no limit for its maximum size. The third column can’t be resized to a size more than 200 pixels.
glazedColumn()
can accept instance of DefaultTableHeaderRenderer
, TableCellRenderer
or TableCellEditor
as child nodes.
The common use case is to pass a TableCellRenderer
to glazedColumn()
in order to format the presentation of this column.
You can use templateRenderer()
to create an instance of TableCellRenderer
that supports simple-jpa template renderer
expression. See Template Renderer for more information about template renderer expression.
For example, you can add uppercase format to the first column, date format to the second column and currency format to the third column as in the following view:
application(pack: true) {
borderLayout()
scrollPane(constraints: CENTER) {
glazedTable(list: model.myTable) {
glazedColumn(name: 'Invoice Number', property: 'number') {
templateRenderer('this:upperCase')
}
glazedColumn(name: 'Date', property: 'date') {
templateRenderer(exp: {it?.toString('dd-MM')?:'-'})
}
glazedColumn(name: 'Total', exp: { it.total() }) {
templateRenderer('this:currencyFormat')
}
}
}
}
Why add templateRenderer while you can convert all of your column to string by using exp in glazedColumn ?
Converting all values to string is simpler, but you will loose natural ordering when sorting a column. If you use
templateRenderer , sorting a column is not performed by comparing formatted value but instead it is based on the original
(unformatted) value.
|
Because templateRenderer()
generates an instance of JLabel
, you can set public properties of resulting JLabel
by
using their name as attribute in templateRenderer()
. For example, to right align the third column, you can use the following code:
import static javax.swing.SwingConstants.*
application(pack: true) {
borderLayout()
scrollPane(constraints: CENTER) {
glazedTable(list: model.myTable) {
glazedColumn(name: 'Invoice Number', property: 'number')
glazedColumn(name: 'Date', property: 'date') {
templateRenderer(exp: {it?.toString('dd-MM-YYYY')?:'-'})
}
glazedColumn(name: 'Total', exp: { it.total() }, columnClass: Integer) { (1)
templateRenderer('this:currencyFormat', horizontalAlignment: RIGHT) (2)
}
}
}
}
1 | This causes column header to be right aligned. |
2 | Right align every cell for this column. |
templateRenderer
also supports custom condition that set its property based on some conditions. For example, the
following view will set font color for third column to red if its value is less than $1000:
import static javax.swing.SwingConstants.*
import java.awt.Color
application(pack: true) {
borderLayout()
scrollPane(constraints: CENTER) {
glazedTable(list: model.myTable) {
glazedColumn(name: 'Invoice Number', property: 'number')
glazedColumn(name: 'Date', property: 'date') {
templateRenderer(exp: {it?.toString('dd-MM-YYYY')?:'-'})
}
glazedColumn(name: 'Total', exp: { it.total() }, columnClass: Integer) {
templateRenderer('this:currencyFormat', horizontalAlignment: RIGHT) {
condition(if_: {it < 1000}, then_property_: 'foreground', is_: Color.RED, else_is_: Color.BLACK)
condition(if_: {isSelected}, then_property_: 'foreground', is_: Color.WHITE)
}
}
}
}
}
Using condition() inside templateRenderer() is useful for simple condition. If you have a complex rendering
calculation, consider creating your own implementation of TableCellRenderer .
|
In addition to glazedColumn()
, glazedTable()
also accepts menuItem()
as child node. This will add new menu when
user right click on the table. By default, popup menu for glazedTable()
consists of only two menu items: copy cell and
print. The following is a sample view that add new popup menus:
import static javax.swing.SwingConstants.*
actions {
action(id: 'menu1', name: 'Menu 1', closure: { println 'Menu1 is selected' })
action(id: 'menu2', name: 'Menu 2', closure: { println 'Menu2 is selected' })
}
application(pack: true) {
borderLayout()
scrollPane(constraints: CENTER) {
glazedTable(list: model.myTable) {
glazedColumn(name: 'Invoice Number', property: 'number')
glazedColumn(name: 'Date', property: 'date') {
templateRenderer(exp: {it?.toString('dd-MM-YYYY')?:'-'})
}
glazedColumn(name: 'Total', exp: { it.total() }, columnClass: Integer) {
templateRenderer('this:currencyFormat', horizontalAlignment: RIGHT)
}
menuItem(action: menu1)
menuItem(action: menu2)
}
}
}
The following table lists attributes that can be used in glazedTable()
:
Name | Type | Description |
---|---|---|
|
|
Source value for this table. |
|
|
Sorting strategy for this table. If |
|
Closure |
This closure will be executed if table selection is changed. |
|
Boolean |
|
|
Boolean |
|
|
|
|
|
|
Action that will be triggered when user double click a row in this table. |
|
|
Action that will be triggered when user press Enter key while row is selected in this table. |
glazedTable()
creates an instance of JTable
so you can also set any public properties of JTable
as attribute in
glazedTable()
.
6.4. Template Renderer
simple-jpa a new templateRenderer
attribute to comboBox()
and list()
node to allow you customize renderer without
creating a new renderer class for every components. templateRenderer()
in glazedColumn()
also uses the same thing.
templateRenderer
attribute accepts a closure or string. If templateRenderer
is a closure, it will be executed
and the result is displayed. Inside the closure, it
refers to original value. You can also call the following
built-in functions from inside this closure:
Function | Description |
---|---|
|
Format a number using number style and limit fraction digits to |
|
Format a number using number style. |
|
Format a number using percent style. |
|
Format a number using currency style. |
|
Change string to lower case. |
|
Change string to upper case. |
|
Change string to title case. |
For example, the following code show how objects are displayed in JComboBox
and JList
without using templateRenderer
:
import ca.odell.glazedlists.*
import ca.odell.glazedlists.swing.*
class LatihanModel {
BasicEventList myList = new BasicEventList()
DefaultEventComboBoxModel listModel = new DefaultEventComboBoxModel(myList)
}
import domain.*
import org.joda.time.*
class LatihanController {
def model
def view
void mvcGroupInit(Map args) {
Invoice invoice1 = new Invoice('inv01', LocalDate.parse('2015-01-01'))
invoice1.add(new LineItem('Product1', 100, 1))
invoice1.add(new LineItem('Product2', 200, 2))
Invoice invoice2 = new Invoice('inv02', LocalDate.parse('2015-01-02'))
invoice2.add(new LineItem('Product3', 300, 3))
invoice2.add(new LineItem('Product4', 400, 4))
execInsideUISync {
model.myList << invoice1
model.myList << invoice2
}
}
}
application(pack: true) {
flowLayout()
comboBox(model: model.listModel)
list(model: model.listModel)
}
Here is what it looks like after adding templateRenderer
in view:
application(pack: true) {
flowLayout()
comboBox(model: model.listModel, templateRenderer: { it.number })
list(model: model.listModel, templateRenderer: { "${it.number} Qty: ${it.items.size()}" })
}
You can also pass a string to templateRenderer
. The string can be a property name or method call. If it is a method call,
it should be starts with '#'
character. You can also call built-in functions by adding ':'
followed by a function name.
import static javax.swing.SwingConstants.*
application(pack: true) {
borderLayout()
scrollPane(constraints: CENTER) {
list(model: model.listModel, templateRenderer: "#total:currencyFormat")
}
}
6.5. Dialog Utils
simple-jpa provides mvcPopupButton()
as a helper node to create a JButton
that will display a modal dialog if it is
clicked. The following steps will be performed if mvcPopupButton()
is clicked:
-
Create a new temporary MVC group specified by its
mvcGroup
attribute. Ifargs
is closure, executeargs
and pass it as arguments for the new MVC group. -
If
onBeforeDisplay
is specified, call it. The generated button andargs
will be passed as arguments foronBeforeDisplay
. -
Find a
JPanel
calledmainPanel
in view and display it in a modal dialog. It is a convension that all main panel should be namedmainPanel
. -
If
onFinish
is specified, when user closed the modal dialog,onFinish
will be executed. Temporary model, view, and controller is passed as arguments foronFinish
.
The following code show how to use mvcPopupButton()
:
mvcPopupButton(text: 'Click Me!', mvcGroup: 'anotherMVCGroup', dialogProperties:
[title: 'New Dialog', size: new Dimension(900,420)], onFinish: { m, v, c ->
println m.result
}
)
mvcPopupButton()
relies to DialogUtils
to create modal dialog. The following is lists of all available methods in DialogUtils
:
-
showMVCGroup(MVCGroup mvcGroup, GriffonView view, Map dialogProperties = [:], LayerUI layerUI = null, Closure onFinish = null)
Use this method to display view from existing MVC group instance in modal dialog.
-
showMVCGroup(String mvcGroupName, Map args = [:], GriffonView view, Map dialogProperties = [:], LayerUI layerUI, Closure onFinish = null)
This method creates a temporary MVC group instance and display its view in modal dialog. Use this method if you need to pass an
LayerUI
. -
showMVCGroup(String mvcGroupName, Map args = [:], GriffonView view, Map dialogProperties = [:], Closure onFinish = null)
This method creates a temporary MVC group instance and display its view in modal dialog.
Example:
def args = [parentList: model.items] def props = [title: 'Items'] DialogUtils.showMVCGroup('lineItemAsChild', args, view, props) { m, v, c -> model.items.clear() model.items.addAll(m.lineItemList) }
-
showAndReuseMVCGroup(String mvcGroupName, Map args = [:], GriffonView view, Map dialogProperties = [:], LayerUI layerUI = null, Closure onFinish = null)
This method creates a new MVC group instance and display its view in modal dialog. The created MVC group instance won’t be destroyed.
-
confirm(Component parent, String message, String title, int messageType = JOptionPane.QUESTION_MESSAGE)
Display a confirmation message and returns
true
if user confirms the dialog. This method always run in EDT.Example:
if (!DialogUtils.confirm(view.mainPanel, 'Do you want to continue?', 'Delete Confirmation')) { return }
-
message(Component parent, String message, String title, int messageType = JOptionPane.INFORMATION_MESSAGE)
Display message dialog. This method always run in EDT.
Example:
DialogUtils.message(null, errorMessage, 'Error', JOptionPane.ERROR_MESSAGE)
-
input(Component parent, String message, String title, int messageType = JOptionPane.QUESTION_MESSAGE)
Display input dialog and return user input as string. This method always run in EDT.
Example:
String updatedScore = DialogUtils.input(view.mainPanel, 'Please Enter your new score:', 'New Score')
7. Transaction
simple-jpa uses EntityManager
per transaction strategy. A new EntityManager
is created when transaction begins and
it is destroyed when transaction is finished. Using EntityManager
in Griffon is a bit tricky since EntityManager
is not thread-safe while Griffon may execute code in different thread. To solve this problem, simple-jpa creates
a new EntityManager
for each different threads.
7.1. @Transaction Annotation
You can use @Transaction
annotation to wrap a method in transaction. This annotation should only be used on artifacts
that have been injected by Persistence Methods. Using @Transaction
on artifacts that don’t have persistence methods
will raise errors.
The following code show the basic usage of @Transaction
:
class MyRepository {
@Transaction
void register() {
...
}
}
You can also apply @Transaction
to all methods in the class by simply add the annotation at class level.
@Transaction
class MyRepository {
void register() {
...
}
void unregister() {
...
}
}
@Transaction
accepts a value that can be Policy.NORMAL
or Policy.SKIP
.
Policy.NORMAL
(default value) supports nested transaction. If methodA()
calls methodB()
and both methods
are annotated by @Transaction
, they are executed in the same transaction. If one of methodA()
and methodB()
raises an Exception
, none of their queries will be committed.
@Transaction
class MyRepository {
void methodA() {
MyEntity entityA = new MyEntity('Entity A')
persist(entityA)
methodB()
}
void methodB() {
MyEntity entityB = new MyEntity('Entity B')
persist(entityB)
throw new RuntimeException('Something fails here!')
}
}
class MyController {
MyRepository myRepository
def test = {
// Both entityA and entityB are not persisted to database
// because methodB() fails with Exception.
myRepository.methodA()
}
}
Nested transaction only work if methods are called from within the same thread. If methodB()
is executed by using
method such as app.execFuture()
or app.eventAsync()
, simple-jpa can’t guarantee it will be part of caller
transaction. This is especially true if thread pool is involved.
@Transaction
class MyRepository {
def app
void methodA() {
MyEntity entityA = new MyEntity('Entity A')
persist(entityA)
app.execFuture { methodB() }
}
void methodB() {
MyEntity entityB = new MyEntity('Entity B')
persist(entityB)
throw new RuntimeException('Something fails here!')
}
}
class MyController {
MyRepository myRepository
def test = {
// entityA is persisted to database but
// entityB is *NOT* persisted to database!
myRepository.methodA()
}
}
To avoid unexpected random errors such as duplicate primary key or missing entity, don’t use multithreading in persistence layer! |
Policy.SKIP
is used to tell simple-jpa to not apply transaction AST transformation to the annotated method. This can
be useful if you use @Transaction
at class level and want to avoid wrapping few methods in transaction.
@Transaction
class MyController {
MyRepository myRepository
def create = {
// Calling this closure will start a new transaction
// or continue existing transaction if caller is in transaction.
}
def remove = {
// Calling this closure will start a new transaction
// or continue existing transaction if caller is in transaction.
}
@Transaction(Transaction.Policy.SKIP)
def calculate = {
// Calling this closure will *NOT* start a new transaction!
// If caller is in transaction, it will be part of that transaction.
}
}
7.2. Transaction Methods
You can also create transaction without using @Transaction
annotation. This can be done by calling transaction methods
manually. Because transaction methods is part of persistence methods, they are only available in injected artifacts. The
following is list of transaction methods:
-
beginTransaction()
Use this method to mark the beginning of new transaction.
-
commitTransaction()
Use this method to commit current transaction. This method will destroys
EntityManager
if current transaction is not nested. -
rollbackTransaction()
Use this method to rollback current transaction. This method will destroys
EntityManager
. -
withTransaction(closure)
Execute closure inside transaction. Inside this closure, you can directly call persistence methods.
For example, if controllers are injected with persistence methods, you can use the following code to create new transaction:
class MyController {
def save = {
beginTransaction()
try {
... // perform works here
commitTransaction()
} catch (Exception ex) {
rollbackTransaction()
}
}
}
withTransaction()
allows you to wrap a closure inside transaction. For example, you can replace the previous sample
code into:
class MyController {
def save = {
withTransaction {
... // perform works here
}
}
}
You can always call persistence methods directly inside the closure. This is very useful if you call withTransaction
from another class, for example:
class MyDomainClass {
MyRepository myRepository
public BigDecimal calculate() {
...
myRepository.withTransaction {
...
executeQuery("FROM Invoice i WHERE i.dueDate > :date", aDate)
...
}
...
}
}
8. Auditing
8.1. Auditable Domain Class
Classes annotated by @DomainClass
will automatically have the following auditing properties:
Name | Type |
---|---|
createdDate |
java.util.Date |
createdBy |
String |
modifiedDate |
java.util.Date |
modifiedBy |
String |
To disable automatic creation of auditing properties for a domain class, use excludeAuditing
property. For example:
@Entity @DomainClass(excludeAuditing=true) @Canonical
class Customer {
}
The value for auditing properties are set by JPA entity listener simplejpa.AuditingEntityListener
. create-simple-jpa
command by default will create griffon-app/conf/metainf/orm.xml
to register simplejpa.AuditingEntityListener
as default entity listener.
The default content of generated orm.xml
is:
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings version="2.0" xmlns="http://java.sun.com/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm orm_2_0.xsd">
<persistence-unit-metadata>
<persistence-unit-defaults>
<entity-listeners>
<entity-listener class="simplejpa.AuditingEntityListener" />
</entity-listeners>
</persistence-unit-defaults>
</persistence-unit-metadata>
</entity-mappings>
You can completely disable auditing by removing the line that register simplejpa.AuditingEntityListener in orm.xml .
|
You can also register your own custom JPA Entity Listener in orm.xml .
|
Built-in scaffolding’s generator will generate views that has auditing properties in them. By default, generated views display
java.util.Date
by its string representation such as Wed Aug 20 13:09:11 ICT 2014
. To display java.util.Date
by using
a custom formatter, style for both date and time can be specified in Config.groovy
:
griffon.simplejpa.scaffolding.dateTimeStyle = 'MEDIUM'
The configuration above will generate the following code in controller:
def selectionChanged = {
...
model.created = selected.createdDate?DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(selected.createdDate):null
model.modified = selected.modifiedDate?DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(selected.modifiedDate):null
...
}
The following is list of available styles (see http://docs.oracle.com/javase/tutorial/i18n/format/dateFormat.html for more information):
Style | Sample Value in U.S. Locale | Sample Value in French Locale |
---|---|---|
DEFAULT |
Jun 30, 2009 7:03:47 AM |
30 juin 2009 07:03:47 |
SHORT |
6/30/09 7:03 AM |
30/06/09 07:03 |
MEDIUM |
Jun 30, 2009 7:03:47 AM |
30 juin 2009 07:03:47 |
LONG |
June 30, 2009 7:03:47 AM PDT |
30 juin 2009 07:03:47 PDT |
FULL |
Tuesday, June 30, 2009 7:03:47 AM PDT |
mardi 30 juin 2009 07 h 03 PDT |
You can always change the generated code to use any custom formatter. |
8.2. Logged User
simple-jpa expects the value of current user is stored in SimpleJpaUtil.user
. Value for this property must be an
an implementation of AuditableUser
. For example, developer can create a domain class that implement AuditableUser
such as:
package domain
import ...
@DomainClass @Entity @Canonical
class User implements AuditableUser {
@NotEmpty @Size(min=2, max=50)
String name
@Override
String getUserName() {
return name
}
}
The following code can be used to set current (logged) user:
User user = new User('guest')
SimpleJpaUtil.instance.user = user
The code above will set createdBy
and modifiedBy
of new entity or updated entity to 'guest'
.
You’re not required to set current user. If SimpleJpaUtil.user is null, value for createdBy and modifiedBy
will be ignored. Regardless whether SimpleJpaUtil.user is available or not, createdTime and modifiedTime are always updated.
|
8.3. Login Dialog
simple-jpa can display a login dialog by using SwingX JXLoginPane
at startup time. To enable this feature, a service
that is derived from org.jdesktop.swingx.auth.LoginService
is required. The following is typical steps to create
such service:
-
Create a new service by using Griffon’s
create-service
command. For example:griffon create-service DatabaseLoginService
-
Change the generated service (in
griffon-app/services
folder) and extends it fromorg.jdesktop.swingx.auth.LoginService
. For example:DatabaseLoginService.groovypackage sample import domain.User import org.jdesktop.swingx.auth.LoginService import simplejpa.SimpleJpaUtil class DatabaseLoginService extends LoginService { @Override boolean authenticate(String name, char[] password, String server) throws Exception { if (name == 'jocki' && new String(password) == 'toor') { SimpleJpaUtil.instance.user = new User('jocki') return true } false } }
If you add
@Transaction
toDatabaseLoginService
and add the following line toConfig.groovy
(See [finders-2] for more information):Config.groovygriffon.simplejpa.finders.injectInto = ['controller', 'service']
You will have access to simple-jpa dynamic finders in
authenticate()
method. Typically, you will query the requested user from database and check if user’s password is correct. -
Add the service name as value for
griffon.simplejpa.auditing.loginService
inConfig.groovy
. For example:Config.groovygriffon.simplejpa.auditing.loginService = 'DatabaseLoginService'
When application is started, the following login dialog will be displayed:
If user cancels the dialog, simple-jpa will terminate the application. If user enters the correct credentials (which means
LoginService.authenticate()
returns true
), simple-jpa will display startup group as usual.
9. Finders
Finders are part of persistence methods that are used to retrieve JPA entities from database. In addition to static methods, simple-jpa also supports dynamic finder. With dynamic finder, you can find entity based on certain pattern in method name.
9.1. Query Methods
simple-jpa injects the following query methods:
-
executeNamedQuery(namedQuery, map, config)
Execute JPA named query.
-
executeQuery(jpql, config)
Execute JP QL.
-
executeNativeQuery(sql)
Execute SQL.
To use named queries, you must defined them first, for example:
@NamedQuery(name='Invoice.LargeQty', query='''
FROM Invoice inv LEFT JOIN inv.items i WHERE i.qty > :limit
''')
@DomainClass @Entity @Canonical
class Invoice {
...
}
Now you can call Invoice.LargeAmount
by using code like:
def result = myRepository.executeNamedQuery('Invoice.LargeQty', [limit: 500])
println "Invoices with large qty in one of their items:"
println result.collect { it.number }.join(',')
Named queries are parsed when application is launched. Although they make application startup slower, they are quick to serve you when you need them because they’re prepared.
As an alternative to named query, you can always directly call JP QL by using executeQuery()
, for example:
def result = myRepository.executeQuery(
'FROM Invoice inv LEFT JOIN inv.items i WHERE i.qty > :limit',
[:], [limit: 500])
Don’t forget that you can use simple-jpa-console script to launch Groovy console to try query methods described
in this chapter.
|
JPA can hurt performance if your query return a very large result. This is because JPA provider needs to translate
every rows from native SQL query into object (entities). Creating a lot of objects in a short time may leads to
memory shortage in heap. To avoid these problems, you can use executeNativeQuery()
to execute SQL:
def result = myRepository.executeNativeQuery('''
SELECT i.number, SUM(li.qty * li.price) AS total
FROM Invoice i
LEFT JOIN Invoice_Items li ON li.invoice_id = i.id
GROUP BY i.number
''')
// result is an array
println result[0][0] // print first column of first record
println result[0][1] // print second column of first record
println result[1][0] // print first column of second record
println result[1][1] // print second column of second record
Don’t use executeNativeQuery() if not necessary. Your code become prone to mismatch between query and domain
classes. If you find yourself create a lot of SQL queries, you need to consider to use native SQL approach instead of JPA.
|
9.2. Dynamic Finder
Dynamic finder works by following certain pattern in method name. For example, if you want to select all Invoice
, you
can call the following method:
List result = myRepository.findAllInvoice()
println result.collect { it.number }.join(',')
Finder starts with findAll
always return a List
. If nothing is found, it returns an empty List
.
You can add a criteria by using adding by
to the finder followed by property name, for example:
List result = myRepository.findAllInvoiceByNumber('INV02')
println result.collect { it.number }.join(',')
If you want to return only a single instance, drop all
from the finder name, for example:
Invoice result = myRepository.findInvoiceByNumber('INV02')
println result?.number
The finder returns an instance of Invoice
or null
if nothing is found.
You can combine multiple criteria by using logical operator and
and or
, for example:
myRepository.findAllInvoiceByNumberAndDate('INV02', LocalDate.parse('2015-02-01'))
In the previous examples, you’ve searched based on equality (eq
). simple-jpa dynamic finders also supports different
operators such as:
-
greaterThanEqualTo
orge
-
lessThanEqualTo
orle
-
greaterThan
orgt
-
lessThan
orlt
-
isNotMember
-
isNotEmpty
-
isNotNull
-
notEqual
orne
-
isMember
-
isEmpty
-
isNull
-
like
-
notLike
-
between
For example, to search for all invoices created in this month, you can use the following code:
def dateBegin = LocalDate.now().dayOfMonth().withMinimumValue()
def dateEnd = LocalDate.now().dayOfMonth().withMaximumValue()
def result = myRepository.findAllInvoiceByDateBetween(dateBegin, dateEnd)
In this another example, you find products by name using like
operator:
myRepository.findAllProductByNameLike('A%')
You can search for nested properties by using double underscore (__
) as separator. For example, the following
query finds all products based on supplier’s location:
myRepository.findAllProductBySupplier__City__NameLike('a city%')
9.3. Dsl Finder
In addition to using dynamic finder, you can also perform query by using Dsl finder. Such finder requires a closure as argument. The following code show how to use Dsl Finder:
def dateBegin = LocalDate.now().dayOfMonth().withMinimumValue()
def dateEnd = LocalDate.now().dayOfMonth().withMaximumValue()
myRepository.findAllInvoiceByDsl {
number eq('INV02')
and()
date between(dateBegin, dateEnd)
}
The advantage of using Dsl closure is you can build the query conditions based on certain condition. You can also add any code inside the closure. For example, the following code add query condition only if certain variables are not null:
def dateBeginSearch = LocalDate.now().dayOfMonth().withMinimumValue()
def dateEndSearch = LocalDate.now().dayOfMonth().withMaximumValue()
def numberSearch = null
myRepository.findAllInvoiceByDsl {
if (numberSearch) {
number eq(numberSearch)
}
if (dateBeginSearch && dateEndSearch) {
date between(dateBeginSearch, dateEndSearch)
}
}
You can also use nested properties by using double underscores (__
) as separator, for example:
myRepository.findAllProductByDsl {
supplier__name isIn(['supplier a', 'supplier b'])
and()
name eq('A product')
}
9.4. Named Entity Graph
Named entity graph is a new feature in JPA 2.1. It allows you to define a flexible fetch graph without modifying your current query. For example, you can define named entity graph such as:
package domain
// import statements are not shown
@NamedEntityGraph(name='Invoice.Items', attributeNodes=[
@NamedAttributeNode('items')
])
@DomainClass @Entity @Canonical(excludes='items')
class Invoice {
@NotEmpty @Size(min=5, max=5)
String number
@NotNull @Type(type="org.jadira.usertype.dateandtime.joda.PersistentLocalDate")
LocalDate date
@ElementCollection @OrderColumn @NotEmpty
List<LineItem> items = []
void add(LineItem item) {
items << item
}
BigDecimal total() {
items.sum { it.total() }
}
}
If you want to use named entity graph in simple-jpa finders, you should always start the name with domain class name
followed by a period (.
) before the actual name. To use named entity graph, add fetch
or load
to dynamic finder,
for example:
// items is lazy loaded. It will trigger SQL query when code refer to it.
// result is not safe to be passed to outside transaction because
// SQL query is required to retrieve items.
def result = myRepository.findAllInvoice()
// items has been loaded.
// anotherResult is safe to be passed to outside transaction because
// items is safe to read from anywhere.
def anotherResult = myRepository.findAllInvoiceFetchItems()
In addition to using fetch
in dynamic finder, you can also use named entity graph by passing them query configuration
using fetchGraph
or loadGraph
as key. See Query Configuration for more information.
9.5. Query Configuration
Most of finder methods described so far can receive a configuration parameter in form of Map
. You can use the following
values as its keys:
-
excludeSubclass
The value must be a string. Normal query returns all instances of current class and its subclasses. For example, if
Employee
has three subclassesTeacher
,Researcher
, andStaff
, thenfindAllEmploye()
returns instances ofEmployee
,Teacher
,Researcher
andStaff
. If you want to return onlyTeacher
andStaff
, you can use the following code:myRepository.findAllEmployee([excludeSubclass: 'Researcher'])
Assuming
Employee
is not an abstract class and it have instances, then you can use following code to only return instances ofEmployee
:myRepository.findAllEmployee([excludeSubclass: '*'])
-
flushMode
The value must be one of
FlushModeType.COMMIT
orFlushModeType.AUTO
. Use this key to override flush mode for current query. -
excludeDeleted
The value is a boolean (
true
orfalse
). If it istrue
, query will not return soft deleted entities. An entity is considered as soft deleted if itsdeleted
property is equals to'Y'
. -
orderBy
The value must be a string. Use this key to sort current query. If you have multiple attributes to sort, separate them by comma, for example:
myRepository.findAllInvoice([orderBy: 'number,date'])
If you want to sort by nested property, use double underscore (
__
) as separator. -
orderDirection
The value must be one of the following string:
asc
ordesc
. This key is used to specify the direction of order inorderBy
. For example, the following query will find all invoices sorted by number in ascending direction and date in descending direction:myRepository.findAllInvoice([orderBy: 'number,date', orderDirection: 'asc,desc'])
-
page
The value must be a number starting from
1
. Use this key together withpageSize
to limit query result. Default value forpage
is1
. -
pageSize
The value must be a positive number. Use this key to set the size for current page. If
page
is specified butpageSize
is not set, then it is assumed to be10
. If bothpage
andpageSize
is not specified, there will be no limit for this query.For example, to retrieve only 3 first invoices, you can use the following configuration:
myRepository.findAllInvoice([pageSize:3])
-
allowDuplicate
The value is a boolean (
true
orfalse
). Set it totrue
to never returns duplicate entities. This configuration addsSELECT DISTINCT
to current query. -
fetchGraph
This key accepts a
String
or an instance ofEntityGraph
created by usingentityManager.createEntityGraph()
. If you have defined named entity graph calledInvoice.Items
, you can use it in finders like:myRepository.findAllInvoice([fetchGraph: 'Invoice.Items'])
Or, you can also build the named entity graph in runtime such as in this sample:
myRepository.withTransaction { def em = getEntityManager() def g = em.createEntityGraph(Invoice) g.addAttributeNodes('items') findAllInvoice([fetchGraph: g]) }
-
loadGraph
This key accepts a
String
or an instance ofEntityGraph
created by usingentityManager.createEntityGraph()
. See the description offetchGraph
for more information.
You can also add some of the query configurations described above in Config.groovy
. This way the configuration is
globally applied to all finders that can accepts it. The following table lists all query configurations that
can be added in Config.groovy
:
Name | Default Value | Description |
---|---|---|
|
|
You can also change the flush mode to |
|
|
If this value is |
|
|
The default value always add |
10. Testing
10.1. Unit Test
In unit test, you’re usually testing domain classes. This test should never requires database connection. Unfortunately, sometimes domain class may depends on persistence methods in repository, for example:
class RegistrationRepository {
// This methods hits database to retrieve last number and increase it by one.
Integer nextNumber() {
...
}
}
class Registration {
String reserveNumber(Integer input) {
...
def repo = SimpleJpaUtil.instance.repositoryManager.getRepository('RegistrationRepository')
def number = repo.nextNumber()
...
}
}
You can’t test Registration.reserveNumber()
directly because it calls RegistrationRepository.nextNumber()
that requires
access to database. For example, the following unit test will fail:
class RegistrationTests extends GriffonUnitTestCase {
void testReserveNumber() {
Registration r = new Registration()
assertEquals('REG-001', r.reserveNumber(1))
}
}
To solve this problem, you need to create a stub repository, for example:
class RegistrationTests extends GriffonUnitTestCase {
void setUp() {
super.setUp()
super.registerMetaClass(NumberingRepository)
NumberingRepository.metaClass.reserveNumber = {
// stub for reserveNumber() that always return 0.
return 0
}
StubRepositoryManager stubRepositories = new StubRepositoryManager()
stubRepositories.instances['NumberingRepository'] = new NumberingRepository()
SimpleJpaUtil.instance.repositoryManager = stubRepositories
}
void testReserveNumber() {
Registration r = new Registration()
assertEquals('REG-001', r.reserveNumber(1))
}
}
10.2. Integration Test
simple-jpa provides DbUnitTestCase
to help you in performing integration testing that actually hits database. DbUnitTestCase
uses DbUnit to populate databases with certain records for every test case. The following sample show a basic usage of
DbUnitTestCase
:
class InvoiceTest extends DbUnitTestCase {
InvoiceRepository invoiceRepository
protected void setUp() {
super.setUp()
setUpDatabase('/project/data.xlsx')
invoiceRepository = SimpleJpaUtil.instance.repositoryManager.findRepository('Invoice')
}
void testDelete() {
Invoice newInvoice = invoiceRepository.findInvoiceById(-1l)
invoiceRepository.remove(newInvoice)
newInvoice = invoiceRepository.findInvoiceById(-1l)
assertNull(newInvoice)
}
}
When you execute the test by using griffon test-app
, setUpDatabase()
is called before executing test methods. By default,
setUpDatabase()
performs a clean insert (DatabaseOperation.CLEAN_INSERT
) on database. Tables contents are deleted first then
new records are inserted based on the value in /project/data.xlsx
. Because this process is repeated for every test methods,
you can assume that database is in the expected state when test methods begin.
DbUnitTestCase
supports the following format as data source:
-
Microsoft Excel binary format if filename ends with
.xls
. Every sheets represents a table. Rows are table records and columns are table columns. -
Microsoft Excel XML format (the newer version) if filename ends with
.xlsx
. -
XML file if filename ends with
.xml
. -
If
DbUnitTestCase
can’t decide the type of data source, it assumes the data source is in CSV format.
DbUnitTestCase
also executes the following SQL files if it founds them in root package:
-
before.sql
is executed before cleaning and executing test method. -
clean.sql
is executed before inserting data to database. -
after.sql
is executed after database has been populated. Test method begins after this script is executed.
To avoid problem related to foreign key constraints, you can drop your table manually in before.sql or clean.sql in
the correct order. If you use MySQL Server, you can use SET FOREIGN_KEY_CHECKS=0 to disable foreign key constraints and
SET FOREIGN_KEY_CHECKS=1 to enable foreign key constraints.
|
By default, DbUnitTestCase
performs DatabaseOperation.CLEAN_INSERT
for every methods. You can change this by setting
insertOperation
with a different value, for example:
class InvoiceTest extends DbUnitTestCase {
protected void setUp() {
super.setUp()
setUpDatabase('/project/data.xlsx', null, false, DatabaseOperation.UPDATE)
}
}
For advanced use case, you can always override or execute public methods provided by DbUnitTestCase
:
-
loadMVC(mvcGroup)
Create MVCGroup instance specified by the name. You can then use
model
,view
andcontroller
property to access this MVC instance members. -
beforeSetupDatabase()
Use this method to execute
before.sql
if it is found in root package. This method is called at the beginning of test method. -
cleanDataset()
Use this method to execute
clean.sql
if it is found in root package. This method is called beforeinsertOperation
is executed. -
afterSetupDatabase()
Use this method to execute
after.sql
if it is found in root package. This method is called afterinsertOperation
is executed and before test method begins. -
execute(list)
Execute a list of SQL statements.
-
cleanInsert()
Perform a
DatabaseOperation.CLEAN_INSERT
based on current data source. -
truncateTable()
Perform a
DatabaseOperation.TRUNCATE_TABLE
based on current data source. -
deleteAll()
Perform a
DatabaseOperation.DELETE_ALL
based on current data source. -
refresh()
Perform a
DatabaseOperation.REFRESH
based on current data source.
Appendix A: Script
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
A.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
-
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
-
The password used when establishing connection to the database.
database
-
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
-
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
-
The name of JPA provider that will be used. The default value for this parameter is
hibernate
. jdbc
-
The name of JDBC driver that will be used. The default value for this parameter is
mysql
. The following available values aremysql
for using MySQL JDBC orderby-embedded
for using Apache Derby embedded database JDBC. skipDatabase
-
If this argument exists,
create-simple-jpa
will not create user and database automatically. It will only write topersistence.xml
and assume required database schema and user is available.
For example, 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 -jdbc=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
If you use -jdbc=derby-embedded
, -database
should be a full path to directory such as
C:/Users/me/mydb.
A.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
-
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
A.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
-
The name of domain class the will be manipulated by the generated
MVCGroup
. Each domain class will have their ownMVCGroup
generated. If this value is*
, then all domain classes will be processed. generatedPackage
(optional)-
The target package. By default, the value for this parameter is project.
forceOverwrite
(optional)-
If exists, this script will replace existing files without any notifications.
setStartup
(optional)-
If exists, this script will 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)-
If exists, this script will not create Microsoft Excel file for integration testing (DbUnit).
startupGroup
(optional)-
The name for 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.
For example, 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
A.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
Example:
griffon install-templates
A.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.
The syntax for this command is:
simple-jpa-console
Examples:
griffon simple-jpa-console
A.6. generate-schema
This command will generate database schema based on current domain models mapping to database or scripts. Developer can use this command to retrieve SQL scripts that can be used to populate new database schema for current application.
The syntax for this command is:
generate-schema -target=database -action=[action]
or
generate-schema -target=database -action=[action] -data=[script.sql]
or
generate-schema -target=script -action=[action]
-dropTarget=[script.sql]
-createTarget=[script.sql]
target
-
One of
database
orscript
. If target isdatabase
, this script will create database objects in the database configured inpersistence.xml
. You shouldn’t need this target because by default, database objects will be dropped and generated when application is launched. If target isscript
, this command will generate SQL scripts that can be executed later (perhaps in a new database schema). action
-
Valid values are
none
,create
,drop-and-create
, anddrop
. data
(optional)-
Contains SQL script location that will be executed after database objects are created. The purpose of this script is to initialize database (for example, populating tables with initial data).
dropTarget
-
Available if
target
isscript
. This is the value of file that will be generated and contains DDL DROP scripts. createTarget
-
Available if
target
isscript
. This is the value of file that will be generated and contains DDL CREATE scripts.
Examples:
griffon generate-schema -target=database -action=drop-and-create
griffon generate-schema -target=script -action=drop-and-create
-dropTarget=drop.sql -createTarget=target.sql
A.7. obfuscate
Use this command to generate obfuscated value that can be added to configuration file or simplejpa.properties. This is useful to hide sensitive information such as database password from novice users.
The syntax for this command is:
obfuscate -generate=[value]
or
obfuscate -reverse=[value]
Examples:
griffon obfuscate -generate=mypassword
The command above will generate obfuscated:AGHJLPazOUvt5ZjzRNnKaA==
. This can be used as a substitution for
configurations that accepts string value. For example, it can be used in Config.groovy
:
griffon {
simplejpa {
entityManager {
javax.persistence.jdbc.password = "obfuscated:AGHJLPazOUvt5ZjzRNnKaA=="
}
}
}
A.8. create-repository
Use this command to create a new repository. It is recommended to generate repository automatically by using DDD generator rather than create them manually using this command.
The syntax for this command is:
create-repository [name]
This command will create new repository class in griffon-app/repositories
.
Examples:
create-repository Invoice