Basic CRUD Web Aplikasi Menggunakan SpringFramework-MVC, Freemarker, Hibernate dan JPA

Salam hangat dan apa kabar?

Senang rasanya bisa kembali menulis pada blog ini setelah sekian lama absen. Pada sesi kali ini saya akan menulis artikel tentang membuat web aplikasi menggunakan JAVA Programming Language. Framework pilihan untuk membuat web aplikasi pada topik ini adalah:

  1. SpringFramework-MVC, sebagai web framework utama.
  2. Freemarker, sebagai template engine yang berkolaborasi dengan Spring-MVC.
  3. Hibernate, sebagai ORM framework utama untuk akses ke database.
  4. JPA, sebagai Java Persistence API layer untuk berkomunikasi dengan database.

Sebagaimana diketahui, JPA merupakan standard acuan dari berbagai produk ORM seperti Hibernate, TopLink, EclipseLink, OpenJPA dan lainnya untuk mengakses database. Dan pada topik ini, kita hanya memfokuskan pada bagaimana membuat CRUD aplikasi dengan Spring-MVC, Freemarker dan Hibernate sebagai JPA provider. Untuk database penyimpanan data kita mempergunakan MySQL.

Persiapan Environment

  1. Download, install dan konfigurasi MySQL. Setelah selesai dikonfigurasi, kemudian buat database baru dengan nama: db_jpacrud.
  2. Download dan install JAVA JDK8.
  3. Download, install IDEA favorit Anda. Pada topik pembuatan web aplikasi ini kita mempergunakan Jetbrain IntelliJ IDEA.
  4. Download, dan install java servlet: apache Tomcat 8.x. Ikuti petunjuk instalasi tomcat dari website resminya.

Setelah keempat item diatas terinstall dan terkonfigurasi dengan benar, barulah kita akan memulai pembuatan project. Tetapi mohon maaf, saya tidak akan menjelaskan bagaimana mempersiapkan dan mengkonfigurasi environment yang dibutuhkan untuk pembuatan web aplikasi ini.


Konfigurasi Project

Berikut ini adalah tahapan awal dalam mengkonfigurasi project:

  1. Buka IntelliJ IDEA dan pilih Create New Project.
  2. Tunggu window dialog New Project terbuka, kemudian pilih Maven pada left sidebar dan klik button Next.
  3. Kemudian isi field: GroupId, ArtifactId dan Version. Setelah itu klik button Next.
  4. Kemudian isi field: Project name dan Project location (direktori project). Setelah itu klik button Finish.
    Tunggu beberapa saat hingga IntelliJ IDEA selesai mengkonfigurasi project yang baru dibuat. Kini struktur project telah otomatis dibuat dan selesai dikonfigurasi. Sekarang kita siap untuk bekerja pada project web aplikasi dengan menggunakan IntelliJ IDEA. Perlu diketahui bahwa struktur direktori project yang telah dibuat adalah mengikuti standard Maven Project, sehingga project juga dapat dibuka dengan Spring Tool Suite (STS) ataupun Eclipse IDEA lainnya.
  5. Buka file pom.xml, dan ketik teks dibawah ini:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.fajar-apps</groupId>
    <artifactId>JPACrud</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>JPACrud</name>

    <properties>
        <spring.version>4.2.0.RELEASE</spring.version>
        <springjpa.version>1.8.2.RELEASE</springjpa.version>
        <ehcache.version>2.10.0</ehcache.version>
        <hibernate.version>4.3.11.Final</hibernate.version>
        <hibernate-validator.version>5.2.1.Final</hibernate-validator.version>
        <javassist.version>3.20.0-GA</javassist.version>
        <freemarker.version>2.3.23</freemarker.version>
        <hikaricp.version>2.4.1</hikaricp.version>
        <mysql.version>5.1.36</mysql.version>
        <log4j.version>2.3</log4j.version>
        <slf4j.version>1.7.12</slf4j.version>
        <jboss-logging.version>3.3.0.Final</jboss-logging.version>
        <jboss-annotation.version>2.0.1.Final</jboss-annotation.version>
        <commonslang3.version>3.4</commonslang3.version>
    </properties>

    <dependencies>
        <!-- Springframework MVC, Spring-Data -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
            <exclusions>
                <exclusion>
                    <groupId>commons-logging</groupId>
                    <artifactId>commons-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- Freemarker template engine -->
        <!-- Hibernate ORM, Hibernate-validator -->
        <!-- Database connection pool -->
        <!-- MySQL jdbc driver -->
        <!-- Utilities -->
        <!-- Apache Logging framework, Anda dapat menggantinya dgn framework lain. -->
        <!-- SLF4J-logging, diperlukan hanya sebagai dependency -->
        <!-- JBoss-Logging adalah logging framework dependency bagi hibernate. -->
        
        <!-- kode lengkapnya tidak ditampilkan, tapi dapat didownload dibagian akhir dari tulisan ini... -->
    </dependencies>
    
    <build>
        <finalName>JPACrud</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.3</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.6</version>
                <configuration>
                    <packagingExcludes>
                        styles/bootstrap,
                        styles/less,
                        scripts/devel
                    </packagingExcludes>
                    <webResources>
                        <resource>
                            <!-- this is relative to the pom.xml directory -->
                            <excludes>
                                <exclude>/src/main/webapp/styles/bootstrap</exclude>
                                <exclude>/src/main/webapp/styles/less</exclude>
                                <exclude>/src/main/webapp/scripts/devel</exclude>
                            </excludes>
                        </resource>
                    </webResources>
                </configuration>
            </plugin>

            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <includes>
                        <include>**/*Tests.java</include>
                    </includes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Setelah file pom.xml selesai disunting dan diketik lengkap, simpan dan tunggu IntelliJ IDEA selesai mendownload (mengunduh) semua dependency yang diperlukan. Berapa lamanya, tergantung kecepatan koneksi internet Anda. Perlu diketahui bahwa file pom.xml ini merupakan jantung utama dari Maven Project Framework dan bertindak sebagai dependency management.


Konfigurasi Web Descriptor, Spring-MVC dan DataSource

Kemudian pada Project Explorer ataupun Project tree-structure pilih item paling atas (nama project) ataupun JPACrud-SpringMVC folder. Klik kanan dan pilih popup menuitem: Add Framework Support.

Pada daftar checkboxes, pilih: Spring, Spring Data JPA, Spring MVC, JAVA EE Persistence, dan Hibernate. Kemudian klik button OK.

Ikuti petunjuk pada layar monitor, jika ada muncul window dialog lainnya.

Kemudian pada menubar pilih menuitem: File -> Project Structure. Pada window dialog left sidebar pilih Modules -> Web. Dan pilih tanda + (New Web deployment descriptor). Pada child window dialog, arahkan lokasi file web.xml pada direktori src/main/webapp/WEB-INF jika lokasi file tidak terisi otomatis. Kemudian pilih servlet versi 3.0 ataupun 3.1 dan klik button OK pada child window dan parent window.

Perlu diketahui bahwa tomcat 7.x hanya support servlet 3.0 dan JPA 2.0, sedangkan tomcat 8.x support hingga servlet 3.1 dan JPA 2.1. Dan project web aplikasi kita menggunakan standard JPA 2.1.

Source code file src/main/webapp/WEB-INF/web.xml :

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">
    <description>Web application context configuration</description>
    <display-name>JPACrud Web Application</display-name>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            /WEB-INF/spring/app-context.xml
        </param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <servlet>
        <servlet-name>mvc-dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>mvc-dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>

Kode diatas dibuat untuk servlet 3.0, jika sewaktu membuat file web.xml memilih servlet 3.1 maka headernya akan sedikit berbeda. File web.xml ini merupakan web deployment "WAR" descriptor yang diperlukan oleh setiap http servlet engine untuk menjalankan web aplikasi yang ditempatkan padanya.

Melalui Project Explorer, pilih folder src/main/webapp/WEB-INF dan buat file Spring Configuration baru. Beri nama file tersebut dengan mvc-dispatcher-servlet.xml.

Source code file src/main/webapp/WEB-INF/mvc-dispatcher-servlet.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
       http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.2.xsd">

    <description>Spring MVC framework configuration</description>

    <mvc:annotation-driven validator="validator" conversion-service="conversionService"
                           ignore-default-model-on-redirect="true"/>
    <mvc:default-servlet-handler/>

    <!-- freemarker configuration and viewResolver -->
    <bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
        <property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
        <property name="freemarkerSettings">
            <props>
                <prop key="incompatible_improvements">2.3.23</prop>
                <prop key="template_exception_handler">rethrow</prop>
                <prop key="default_encoding">UTF-8</prop>
            </props>
        </property>
        <property name="freemarkerVariables">
            <map>
                <entry key="xml_escape" value-ref="fmXmlEscape"/>
            </map>
        </property>
    </bean>
    <bean id="fmXmlEscape" class="freemarker.template.utility.XmlEscape"/>
    <bean id="defaultViewResolver" class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
        <property name="cache" value="true"/>
        <property name="prefix" value=""/>
        <property name="suffix" value=".ftl"/>
    </bean>

    <!-- Declare SpringMVC interceptors -->
    <mvc:interceptors>
        <bean class="org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor"/>
        <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
            <property name="paramName" value="lang"/>
        </bean>
    </mvc:interceptors>

    <!-- Declare Localization resource bundles -->
    <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
        <property name="basenames" value="WEB-INF/i18n/messages"/>
        <property name="fallbackToSystemLocale" value="false"/>
    </bean>
    <bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
        <property name="cookieName" value="locale"/>
    </bean>

    <!-- Declare locale message validator dan conversion service -->
    <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
        <property name="validationMessageSource" ref="messageSource"/>
    </bean>
    <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <property name="registerDefaultFormatters" value="false" />
        <property name="formatters">
            <set>
                <bean class="org.springframework.format.number.NumberFormatAnnotationFormatterFactory" />
                <bean class="org.springframework.format.datetime.DateTimeFormatAnnotationFormatterFactory" />
            </set>
        </property>
    </bean>

    <!-- Declare cache manager for Spring-MVC -->
    <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
        <property name="cacheManager" ref="ehcache"/>
    </bean>
    <bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
        <property name="configLocation" value="classpath:ehcache.xml"/>
        <property name="shared" value="true"/>
    </bean>

    <!-- Scan classpath for annotations, eg: @Controller -->
    <context:component-scan base-package="org.fajarapps.jpacrud.controller"/>

</beans>

Melalui Project Explorer, pilih folder src/main/webapp/WEB-INF dan buat folder berikut: freemarker, i18n, spring. Kemudian pilih folder spring, dan buat file Spring Configuration baru. Beri nama file tersebut dengan app-context.xml.

Source code file src/main/webapp/WEB-INF/spring/app-context.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:jpa="http://www.springframework.org/schema/data/jpa"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
       http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">

    <description>DataSource, JPA, and Hibernate configuration</description>

    <!-- Declare database connection pool dan JPA DataSource -->
    <bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
        <property name="minimumIdle" value="4"/>
        <property name="maximumPoolSize" value="50"/>
        <property name="connectionTimeout" value="50000"/>
        <property name="idleTimeout" value="300000"/>
        <property name="maxLifetime" value="600000"/>
        <property name="validationTimeout" value="30000"/>
        <property name="leakDetectionThreshold" value="30000"/>
        <property name="dataSourceClassName" value="com.mysql.jdbc.jdbc2.optional.MysqlXADataSource"/>
        <property name="dataSourceProperties">
            <props>
                <prop key="port">3306</prop>
                <prop key="serverName">localhost</prop>
                <prop key="user">DBUSER</prop>
                <prop key="password">DBPASSWORD</prop>
                <prop key="databaseName">db_jpacrud</prop>
                <prop key="prepStmtCacheSize">250</prop>
                <prop key="cachePrepStmts">true</prop>
            </props>
        </property>
    </bean>

    <bean id="jdbcDataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
        <constructor-arg ref="hikariConfig"/>
    </bean>

    <!-- Declare SpringData-JPA configuration -->
    <!-- <context:load-time-weaver/> [ optional, please google utk informasi detailnya ] -->
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="jdbcDataSource"/>
        <property name="packagesToScan" value="org.fajarapps.jpacrud.entity"/>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <property name="generateDdl" value="true"/>
                <property name="showSql" value="true"/>
                <property name="databasePlatform" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
                <property name="database" value="MYSQL"/>
            </bean>
        </property>
        <property name="jpaProperties">
            <props>
                <prop key="hibernate.show_sql">true</prop>
                <prop key="hibernate.format_sql">true</prop>
                <prop key="hibernate.cache.use_second_level_cache">true</prop>
                <prop key="hibernate.cache.use_query_cache">true</prop>
                <prop key="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</prop>
                <prop key="hibernate.generate_statistics">true</prop>
                <prop key="hibernate.max_fetch_depth">3</prop>
                <prop key="hibernate.jdbc.fetch_size">100</prop>
                <prop key="hibernate.jdbc.batch_size">10</prop>
            </props>
        </property>
    </bean>

    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
        <property name="dataSource" ref="jdbcDataSource"/>
        <property name="jpaDialect">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect"/>
        </property>
    </bean>

    <!-- Declare JPA Entity message localization -->
    <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
        <property name="basenames" value="WEB-INF/i18n/messages"/>
        <property name="fallbackToSystemLocale" value="false"/>
    </bean>

    <!-- Declare others -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
    <jpa:repositories base-package="org.fajarapps.jpacrud.repository" transaction-manager-ref="transactionManager" />
    <jpa:auditing modify-on-creation="false" />

    <!-- Scan classpath for annotations, eg: @Component, @Service -->
    <context:annotation-config/>
    <context:component-scan base-package="org.fajarapps.jpacrud.domain"/>

</beans>

Konfigurasi Ehcache dan Logging

Pada Project Explorer pilih folder src/main/resources, dan 2 buat file baru. Beri nama file tersebut ehcache.xml dan log4j2.xml.

Sunting file src/main/resources/ehcache.xml seperti kode berikut ini:

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" updateCheck="false">

    <!-- Location of persistent caches on disk -->
    <diskStore path="java.io.tmpdir/EhCacheStores"/>

    <defaultCache eternal="false" maxEntriesLocalHeap="100000" timeToIdleSeconds="600" timeToLiveSeconds="1800"
                  overflowToDisk="false" memoryStoreEvictionPolicy="LFU"/>

    <cache name="org.fajarapps.jpacrud.entity.Person" eternal="false" maxEntriesLocalHeap="1000"
           timeToIdleSeconds="600" timeToLiveSeconds="3600"
           overflowToDisk="false" memoryStoreEvictionPolicy="LFU"/>

    <cache name="org.fajarapps.jpacrud.entity.Department" eternal="false" maxEntriesLocalHeap="1000"
           timeToIdleSeconds="600" timeToLiveSeconds="3600"
           overflowToDisk="false" memoryStoreEvictionPolicy="LFU"/>

    <cache name="org.hibernate.cache.internal.StandardQueryCache" eternal="false"
           maxEntriesLocalHeap="50000" timeToIdleSeconds="600" timeToLiveSeconds="1800"
           overflowToDisk="false" memoryStoreEvictionPolicy="LFU"/>

    <cache name="org.hibernate.cache.spi.UpdateTimestampsCache" maxEntriesLocalHeap="10000"
           eternal="true" memoryStoreEvictionPolicy="LFU">
        <persistence strategy="localTempSwap"/>
    </cache>

</ehcache>

Sunting file src/main/resources/log4j2.xml seperti kode berikut ini:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="STDOUT" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p [%c{1}:%L] - %m%n"/>
        </Console>
        <RollingRandomAccessFile name="FileRolling" fileName="../logs/jpacrud.log" immediateFlush="false"
                                 filePattern="../logs/jpacrud-%d{dd-MM-yyyy}.log.gz">
            <PatternLayout>
                <Pattern>%d{yyyy-MM-dd HH:mm:ss} %-5p [%c{1}:%L] - %m%n</Pattern>
            </PatternLayout>
            <Policies>
                <TimeBasedTriggeringPolicy/>
                <SizeBasedTriggeringPolicy size="5 MB"/>
            </Policies>
            <DefaultRolloverStrategy max="7"/>
        </RollingRandomAccessFile>
    </Appenders>

    <Loggers>
        <AsyncLogger name="org.fajarapps.jpacrud.domain" level="debug" includeLocation="true"/>
        <AsyncLogger name="org.hibernate.jdbc" level="debug" includeLocation="true"/>
        <AsyncLogger name="org.hibernate.jpa" level="debug" includeLocation="true"/>
        <AsyncLogger name="org.hibernate.sql" level="debug" includeLocation="true"/>
        <AsyncLogger name="org.hibernate.type" level="debug" includeLocation="true"/>
        <AsyncLogger name="org.fajarapps.jpacrud" level="info" includeLocation="true">
            <AppenderRef ref="FileRolling"/>
        </AsyncLogger>
        <AsyncLogger name="com.zaxxer.hikari" level="info" includeLocation="true">
            <AppenderRef ref="FileRolling"/>
        </AsyncLogger>
        <AsyncLogger name="org.hibernate" level="info" includeLocation="true">
            <AppenderRef ref="FileRolling"/>
        </AsyncLogger>
        <AsyncLogger name="org.springframework" level="info" includeLocation="true">
            <AppenderRef ref="FileRolling"/>
        </AsyncLogger>
        <AsyncLogger name="org.apache" level="info" includeLocation="true">
            <AppenderRef ref="FileRolling"/>
        </AsyncLogger>
        <Root level="info" includeLocation="true">
            <AppenderRef ref="STDOUT"/>
        </Root>
    </Loggers>
</Configuration>

Kini project telah selesai dikonfigurasi. Walaupun pada IDEA editor terdapat beberapa warning yang menyatakan error, jangan kuatir. Hal itu dikarenakan beberapa file ataupun folder belumlah dibuat. Nanti jika folder ataupun file yang dibutuhkan telah selesai dibuat, maka tanda tersebut akan hilang dengan sendirinya.

Sebagai gambaran tentang struktur direktori project dapat dilihat pada gambar berikut.

Jika ada sub-folder dari src/main/webapp yang belum dibuat maka jangan lupa untuk dibuat dari sekarang.


JPA Entity Class

Perlu diingat, pada project ini kita tidak membuat database table terlebih dahulu. Kita memakai istilah microsoft dengan visual studionya, yaitu: Code First. Yang berarti, struktur table pada database dibuat melalui JPA entity class. Tetapi pada MySQL server kita telah membuat database kosong terlebih dahulu dengan nama db_jpacrud. Database user dan password juga telah kita konfigurasi pada MySQL server.

Sekarang marilah kita memulai membuat source code project web aplikasi JPA-CRUD. Dari Project Explorer pilih folder src/main/java, klik kanan pilih popup menuitem: New -> Package. Namai package tersebut dengan org.fajarapps.jpacrud.entity. Kemudian pilih folder tersebut dan klik kanan pilih popup menuitem: New -> Java Class. Beri nama class tersebut dengan Department. File Department.java akan otomatis dibuat pada package org.fajarapps.jpacrud.entity.

Source code Department.java.

package org.fajarapps.jpacrud.entity;

/* import ... tidak ditampilkan */
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@Table(name = "department", indexes = {@Index(name = "department_x1", columnList = "dept_name", unique = true)})
public class Department
{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "dept_id")
    private Integer deptId;

    @Basic
    @NotEmpty(message = "{validation.field.notEmpty}")
    @Size(min = 3, max = 200, message = "{validation.field.size}")
    @Column(name = "dept_name", length = 200, nullable = false)
    private String deptName;

    @Basic
    @Column(name = "description", columnDefinition = "text")
    private String description;

    @Transient
    private Long numberOfPerson;

    @OneToMany(mappedBy = "department", fetch = FetchType.LAZY, targetEntity = Person.class)
    private Collection<Person> persons;

    /**
     * Class default constructor
     */
    public Department() {
    }

    /**
     * Class constructor.
     *
     * @param deptId Department identity
     * @param deptName Department name
     * @param description Description of department
     * @param numberOfPerson The number of employee within a department
     */
    public Department(Integer deptId, String deptName, String description, Long numberOfPerson) {
        this.deptId = deptId;
        this.deptName = deptName;
        this.description = description;
        this.numberOfPerson = numberOfPerson;
    }
/* method: getter, setter, equals, hashCode, dan toString ... tidak ditampilkan */
}

Masih pada package org.fajarapps.jpacrud.entity, buat class baru dengan nama Person. Berikut ini source code Person.java.

package org.fajarapps.jpacrud.entity;

/* import ... tidak ditampilkan */
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@Table(name = "person",
        indexes = {
                @Index(name = "person_x1", columnList = "fullname"),
                @Index(name = "person_x2", columnList = "dept_id"),
                @Index(name = "person_x3", columnList = "gender")
        })
@EntityListeners(AuditingEntityListener.class)
public class Person
{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "person_id")
    private Long personId;

    @ManyToOne
    @NotNull(message = "{validation.field.notEmpty}")
    @JoinColumn(name = "dept_id", referencedColumnName = "dept_id", nullable = false)
    private Department department;

    @Basic
    @NotEmpty(message = "{validation.field.notEmpty}")
    @Size(min = 5, max = 200, message = "{validation.field.size}")
    @Column(name = "fullname", length = 200, nullable = false)
    private String fullname;

    @Basic
    @NotEmpty(message = "{validation.field.notEmpty}")
    @Column(columnDefinition = "text", nullable = false)
    private String address;

    @Basic
    @NotEmpty(message = "{validation.field.notEmpty}")
    @Size(min = 5, max = 250, message = "{validation.field.size}")
    @Column(length = 250, nullable = false)
    private String province;

    @Basic
    @Email(message = "{validation.invalid.emailAddress}")
    @Column(name = "email", length = 200)
    private String email;

    @Basic
    @Size(max = 50, message = "{validation.field.maxSize}")
    @Column(name = "home_phone", length = 50)
    private String homePhone;

    @Basic
    @Size(max = 50, message = "{validation.field.maxSize}")
    @Column(name = "work_phone", length = 50)
    private String workPhone;

    @Basic
    @Size(max = 50, message = "{validation.field.maxSize}")
    @Column(name = "mobile_phone", length = 50)
    private String mobilePhone;

    @Basic
    @Column(name = "birth_place", length = 250)
    private String birthPlace;

    @NotNull(message = "{validation.field.notEmpty}")
    @DateTimeFormat(pattern = "dd-MM-yyyy")
    @Temporal(TemporalType.DATE)
    @Column(name = "birth_date")
    private Date birthDate;

    @Basic
    @Enumerated(EnumType.STRING)
    @NotNull(message = "{validation.field.notEmpty}")
    @Column(length = 1, nullable = false)
    private Gender gender;

    @CreatedDate
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "ts_created", columnDefinition = "datetime")
    private Date tsCreated;

    @LastModifiedDate
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "ts_modified", columnDefinition = "datetime")
    private Date tsModified;

    @Version
    @Column(name = "version")
    private Integer version;

/* method: getter, setter, equals, hashCode, dan toString ... tidak ditampilkan */
}

Pada class Person ada satu data type yang tidak dikenal yakni Gender. Sekarang marilah kita membuat data type tersebut. Pilih folder src/main/java, dan buat package baru dengan nama org.fajarapps.jpacrud.domain.types. Masih pada Project Explorer popup menu, pilih popup menuitem: New -> Java Class. Isi field Name dengan Gender dan field Kind dengan Enum.

Melalui popup menuitem: New -> Java Class kita dapat membuat Enum, Interface dan sebagainya. Tetapi pada project ini kita hanya membuat Class, Enum dan Interface saja.

Berikut ini source code Gender.java.

package org.fajarapps.jpacrud.domain.types;

public enum Gender
{
    L("Laki-Laki"),
    P("Perempuan");

    private final String label;

    Gender(String name) {
        this.label = name;
    }

    public String getLabel() {
        return label;
    }
}

Spring JPA Repository Class

JPA Repository class merupakan interface untuk mengakses database melalui JPA. Pada repository interface ini kita dapat menempatkan JPQL (Java Persistence Query Language) dan beberapa annotasi lainnya yang diperlukan untuk mengakses database.

Spring-Data-JPA telah mempermudah kita mengakses JPA API dan Hibernate API melalui repository interface ala Spring. Kita tidak dipusingkan bagaimana menggunakan JPA dan Hibernate API agar kita dapat mengakses database dan melakukan operasi CRUD pada MySQL database.

Selanjutnya marilah kita membuat class Repository Interface. Pada Project Explorer pilih package org.fajarapps.jpacrud, lalu pilih popup menuitem: New -> Package, dan namai dengan repository. Selanjutnya pilih package org.fajarapps.jpacrud.repository dan pada popup menuitem: New -> Java Class, buat interface baru dan namai dengan IDepartmentRepository.

Berikut ini source code IDepartmentRepository.java:

package org.fajarapps.jpacrud.repository;

/* import ... tidak ditampilkan */

public interface IDepartmentRepository extends PagingAndSortingRepository<Department, Integer>
{
    /**
     * Menampilkan daftar departemen.
     *
     * @param sort Sorting method
     * @return The slice collection or data-paging collection of Department object.
     */
    @QueryHints(value = {@QueryHint(name = "org.hibernate.cacheable", value = "true")})
    Iterable<Department> findAll(Sort sort);

    /**
     * Menampilkan daftar departemen dan jumlah employee per-departemen.
     *
     * @param pageable Paging and sorting method
     * @return The slice collection or data-paging collection of Department object.
     */
    @QueryHints(value = {@QueryHint(name = "org.hibernate.cacheable", value = "true")})
    @Query("SELECT new Department(d.deptId, d.deptName, d.description, COUNT(p.personId) AS numberOfPerson) " +
            "FROM Department d LEFT JOIN d.persons p GROUP BY d.deptId")
    Page<Department> listAllWithStats(Pageable pageable);

}

Class interface IDepartmentRepository ini diperlukan untuk mengakses table department ataupun JPA-entity Department. Kita dapat melakukan operasi CRUD terhadap table department melalui class interface ini.

Dengan cara yang sama, kita buat class interface baru yakni: IPersonRepository. Class interface ini diperlukan untuk mengakses table person ataupun JPA-entity Person dan melakukan operasi CRUD terhadap table person.

Berikut ini source code IPersonRepository.java:

package org.fajarapps.jpacrud.repository;

/* import ... tidak ditampilkan */

public interface IPersonRepository extends PagingAndSortingRepository<Person, Long>
{
    /**
     * Menampilkan daftar personil dengan menggunakan field 'department' sebagai filter kriteria pencarian.
     *
     * @param department Object kriteria filter
     * @param pageable   Paging and sorting method
     * @return The slice collection or data-paging collection of Person object.
     */
    @QueryHints(value = {@QueryHint(name = "org.hibernate.cacheable", value = "true")})
    @Query("SELECT p FROM Person p JOIN p.department d WHERE d = :dept")
    Page<Person> findByDepartement(@Param("dept") Department department, Pageable pageable);

    /**
     * Menampilkan daftar personil dengan menggunakan field 'department' dan 'fullname'
     * sebagai filter kriteria pencarian.
     *
     * @param department Object kriteria filter
     * @param term       String term pencarian
     * @param pageable   Paging and sorting method
     * @return The slice collection or data-paging collection of Person object.
     */
    @QueryHints(value = {@QueryHint(name = "org.hibernate.cacheable", value = "true")})
    @Query("SELECT p FROM Person p JOIN p.department d WHERE d = :dept AND p.fullname LIKE :term")
    Page<Person> findByDepartementAndTerm(@Param("dept") Department department, @Param("term") String term,
                                          Pageable pageable);

    /**
     * Menampilkan daftar personil dengan menggunakan field 'fullname' sebagai filter kriteria pencarian.
     *
     * @param term     String term pencarian
     * @param pageable Paging and sorting method
     * @return The slice collection or data-paging collection of Person object.
     */
    @QueryHints(value = {@QueryHint(name = "org.hibernate.cacheable", value = "true")})
    @Query("SELECT p FROM Person p WHERE p.fullname LIKE :term")
    Page<Person> findByTerm(@Param("term") String term, Pageable pageable);

    /**
     * Menampilkan daftar personil.
     *
     * @param pageable Paging and sorting method
     * @return The slice collection or data-paging collection of Person object.
     */
    @QueryHints(value = {@QueryHint(name = "org.hibernate.cacheable", value = "true")})
    Page<Person> findAll(Pageable pageable);
}

Spring Project Model Class

Tentunya kita akan bertanya-tanya, sudah ada class repository interface untuk akses ke database kenapa masih diperlukan class Model? Class Model diperlukan untuk mengurangi beban pada class Controller, sehingga Controller tidak dipenuhi oleh source code validasi dan sebagainya yang mengakibatkan source code Controller sangat panjang dan tidak efisien.

JAVA sangat dikenal dengan OOP (Object Oriented Programming) yang terstruktur dan SpringFramework sangat dikenal dengan AOP (Aspect Oriented Programming) modelnya. Dengan kedua model programming tersebut, alur program dan object dependency menjadi lebih terstruktur dan efisien serta source code program menjadi lebih teratur.

Berikut ini adalah diagram alur yang menjelaskan secara umum hubungan antara: Model, Views, Controller, JPA-Repository dan JPA-Entity.

Dari diagram diatas dapat kita petik kesimpulan bahwa Controller membutuhkan Model untuk melakukan beragam aktifitasnya. Dan Model diperlukan karena ada beberapa hal yang tidak dapat dilakukan oleh Repository tetapi dapat dilakukan oleh Model.

Selanjutnya marilah kita membuat class Model untuk project web aplikasi JPA-CRUD.

Pada Project Explorer dibawah package org.fajarapps.jpacrud kita menemukan package domain.types. Ini mungkin akan menyulitkan bagi yang belum terbiasa menggunakan IntelliJ IDEA. Tetapi bagi yang sudah terbiasa, ini mungkin bukanlah masalah yang besar. Pilih package org.fajarapps.jpacrud dan pada popup menuitem: New -> Java Class buat class baru dengan nama: domain.DepartmentModel. Maka class baru tersebut akan kita jumpai tepat dibawah package org.fajarapps.jpacrud.domain. Ketik source code berikut untuk DepartmentModel.java.

package org.fajarapps.jpacrud.domain;

/* import ... tidak ditampilkan */

@Repository
@Transactional
public class DepartmentModel
{
    private final Logger logger = LogManager.getLogger(getClass());
    @Autowired
    private IDepartmentRepository repository;

    /**
     * Menambahkan record Department baru.
     *
     * @param entity Data department
     */
    public void create(Department entity) {
        Department result = repository.save(entity);
        logger.info("Creating Department entity with data: {}", result);
    }

    /**
     * Menghapus record Department dari database.
     *
     * @param id Department identity
     * @throws EntityNotFoundException
     */
    public void delete(Integer id) throws EntityNotFoundException {
        Department dept = repository.findOne(id);
        if (dept == null) {
            throw new EntityNotFoundException("entity.department.NotFound");
        }

        repository.delete(id);
        logger.info("A department entity have been deleted with data: {}", dept);
    }

    /**
     * Menghapus beberapa record Department dari database.
     *
     * @param ids Koleksi identity Department
     * @return Jumlah record yang berhasil dihapus
     */
    public int delete(Iterable<Integer> ids) {
        int count = 0;

        for (Integer Id : ids) {
            repository.delete(Id);
            count++;
        }
        logger.info("{} records of Department entity have been deleted.", count);

        return count;
    }

    /**
     * Menampilkan data informasi Department.
     *
     * @param id Department identity
     * @return Record department
     */
    public Department find(Integer id) {
        logger.info("Find Department entity with ID: {}.", id);
        return repository.findOne(id);
    }

    /**
     * Menampilkan daftar Department.
     *
     * @param pageable Paging and sorting method
     * @return The slice collection or data-paging collection of Department entity.
     */
    @Transactional(readOnly = true)
    public Page<Department> listAll(Pageable pageable) {
        logger.info("Display list of Department entity.");
        return repository.listAllWithStats(pageable);
    }

    public void update(Department entity) throws EntityNotFoundException {
        if (entity == null || entity.getDeptId() == null) {
            throw new EntityNotFoundException("entity.department.NotFound");
        }
        Department record = repository.findOne(entity.getDeptId());
        if (record == null) {
            throw new EntityNotFoundException("entity.department.NotFound");
        }

        repository.save(entity);
        logger.info("Updating department: {}", entity);
    }
}

Selanjutnya pada Project Explorer pilih package org.fajarapps.jpacrud.domain dan melalui popup menuitem buat class baru yakni: PersonModel. Ketik source code berikut untuk PersonModel.java.

package org.fajarapps.jpacrud.domain;

/* import ... tidak ditampilkan */

@Repository
@Transactional(readOnly = false)
public class PersonModel
{
    private final Logger logger = LogManager.getLogger(getClass());
    @Autowired
    private IPersonRepository repository;
    @Autowired
    private IDepartmentRepository departmentRepository;

    /**
     * Menambahkan record personil baru.
     *
     * @param entity Data personil
     */
    public void create(Person entity) {
        Person result = repository.save(entity);
        logger.info("Creating Person entity with data: {}", result);
    }

    /**
     * Menghapus record personil dari database.
     *
     * @param personId Personil identity
     * @throws EntityNotFoundException
     */
    public void delete(Long personId) throws EntityNotFoundException {
        Person person = repository.findOne(personId);
        if (person == null) {
            throw new EntityNotFoundException("entity.person.NotFound");
        }

        repository.delete(personId);
        logger.info("A Person entity have been deleted with data: {}", person);
    }

    /**
     * Menghapus beberapa record personil dari database.
     *
     * @param personIds Koleksi identity personil
     * @return Jumlah record personil yang berhasil dihapus.
     */
    public int delete(Iterable<Long> personIds) {
        int count = 0;

        for (Long personId : personIds) {
            repository.delete(personId);
            count++;
        }
        logger.info("{} records of Person entity have been deleted.", count);

        return count;
    }

    /**
     * Menampilkan data informasi personil.
     *
     * @param id Person identity
     * @return Record personil
     */
    @Transactional(readOnly = true)
    public Person find(Long id) {
        logger.info("Find Person entity with ID: {}.", id);
        return repository.findOne(id);
    }

    /**
     * Menampilkan daftar personil berdasarkan kriteria filter tertentu.
     *
     * @param deptId   Department identity
     * @param term     Kriteria pencarian berdasarkan nama lengkap personil
     * @param pageable Paging and sorting method
     * @return The slice collection or data-paging collection of Person entity.
     */
    @Transactional(readOnly = true)
    public Page<Person> findAllByCriteria(Integer deptId, String term, Pageable pageable) {
        logger.info("Display list of Person entity with criteria: departement = {} and fullname = {}.", deptId, term);

        if (deptId == null && StringUtils.isBlank(term)) {
            return repository.findAll(pageable);
        }
        else if (deptId != null && StringUtils.isNotBlank(term)) {
            Department dept = departmentRepository.findOne(deptId);
            return repository.findByDepartementAndTerm(dept, "%" + term + "%", pageable);
        }
        else if (deptId != null && StringUtils.isBlank(term)) {
            Department dept = departmentRepository.findOne(deptId);
            return repository.findByDepartement(dept, pageable);
        }
        else {
            return repository.findByTerm("%" + term + "%", pageable);
        }
    }

    /**
     * Menampilkan daftar personil.
     *
     * @param pageable Paging and sorting method
     * @return The slice collection or data-paging collection of Person entity.
     */
    @Transactional(readOnly = true)
    public Page<Person> listAll(Pageable pageable) {
        logger.info("Display list of Person entity.");
        return repository.findAll(pageable);
    }

    /**
     * Memperbarui record personil.
     *
     * @param entity Record personil yang telah diperbarui
     * @throws EntityNotFoundException
     */
    public void update(Person entity) throws EntityNotFoundException {
        if (entity == null || entity.getPersonId() == null) {
            throw new EntityNotFoundException("entity.person.NotFound");
        }

        Person record = repository.findOne(entity.getPersonId());
        if (record == null) {
            throw new EntityNotFoundException("entity.person.NotFound");
        }

        updateFields(entity, record);
        repository.save(record);
        logger.info("Updating person: {}", entity);
    }

    /**
     * Update persistence entity with transient entity.
     *
     * @param source transient entity
     * @param target persistence entity
     */
    private void updateFields(Person source, Person target) {
        target.setDepartment(source.getDepartment());
        target.setFullname(source.getFullname());
        target.setAddress(source.getAddress());
        target.setProvince(source.getProvince());
        target.setEmail(source.getEmail());
        target.setBirthDate(source.getBirthDate());
        target.setBirthPlace(source.getBirthPlace());
        target.setHomePhone(source.getHomePhone());
        target.setMobilePhone(source.getMobilePhone());
        target.setWorkPhone(source.getWorkPhone());
        target.setGender(source.getGender());
    }
}

Pagination Helper Model Class

Helper class ini kita perlukan agar data table yang tampil pada halaman web memiliki pagination. Helper class ini kita tempatkan pada package org.fajarapps.jpacrud.domain. Selanjutnya buatlah 3 class helper yakni: AbstractDataGrid, DepartmentDataGrid dan EmployeeDataGrid.

Source code AbstractDataGrid.java:

package org.fajarapps.jpacrud.domain;

import org.springframework.data.domain.Sort;

public abstract class AbstractDataGrid
{
    public static final int PAGESIZE = 15;
    protected int numberOfItems;
    protected int page;
    protected String sortDir;
    protected String sortField;
    protected Sort sort;
    protected long totalItems;
    protected int totalPages;

    /**
     * Returns number of items in current page.
     */
    public int getNumberOfItems() {
        return numberOfItems;
    }

    /**
     * Sets number of items in current page.
     *
     * @param numberOfItems number of items
     * @return Fluent interface, returns itself.
     */
    public AbstractDataGrid setNumberOfItems(int numberOfItems) {
        this.numberOfItems = numberOfItems;
        return this;
    }

    /**
     * Returns the page number of current page.
     */
    public int getPage() {
        return page;
    }

    /**
     * Sets the page number for current page.
     *
     * @param page page number
     * @return Fluent interface, returns itself.
     */
    public AbstractDataGrid setPage(int page) {
        this.page = page;
        return this;
    }

    /**
     * Returns maximum items within a page.
     */
    public int getPageSize() {
        return AbstractDataGrid.PAGESIZE;
    }

    /**
     * Returns the sorting method.
     */
    public Sort getSort() {
        return sort;
    }

    /**
     * Sets the sorting method.
     *
     * @param sort The sorting method.
     * @return Fluent interface, returns itself.
     */
    public AbstractDataGrid setSort(Sort sort) {
        this.sort = sort;
        return this;
    }

    /**
     * Get sorting method specification.
     *
     * @param defaultField the default sort field
     * @return The sorting method
     */
    public Sort getSortSpec(String defaultField) {
        if (sort != null) {
            return sort;
        }

        if (sortDir == null || sortDir.isEmpty()) {
            sortDir = "asc";
        }
        if (sortField == null || sortField.isEmpty()) {
            sortField = defaultField;
        }

        sort = new Sort(Sort.Direction.fromString(sortDir), sortField);
        return sort;
    }

    /**
     * Returns the sort direction.
     */
    public String getSortDir() {
        return sortDir;
    }

    /**
     * Sets the sort direction.
     *
     * @param sortdir sort direction
     * @return Fluent interface, returns itself.
     */
    public AbstractDataGrid setSortDir(String sortdir) {
        this.sortDir = sortdir;
        return this;
    }

    /**
     * Returns the field to sort.
     */
    public String getSortField() {
        return sortField;
    }

    /**
     * Sets the field to sort.
     *
     * @param sortfield The field to sort
     * @return Fluent interface, returns itself.
     */
    public AbstractDataGrid setSortField(String sortfield) {
        this.sortField = sortfield;
        return this;
    }

    /**
     * Gets the number of total items returns from a query resultset.
     */
    public long getTotalItems() {
        return totalItems;
    }

    /**
     * Sets the number of total items returns from a query resultset.
     *
     * @param totalItems Number of total items
     * @return Fluent interface, returns itself.
     */
    public AbstractDataGrid setTotalItems(long totalItems) {
        this.totalItems = totalItems;
        return this;
    }

    /**
     * Returns the number of total pages.
     */
    public int getTotalPages() {
        return totalPages;
    }

    /**
     * Sets the number of total pages.
     *
     * @param totalPages Number of total pages
     * @return Fluent interface, returns itself.
     */
    public AbstractDataGrid setTotalPages(int totalPages) {
        this.totalPages = totalPages;
        return this;
    }
}

Class AbstractDataGrid diperlukan agar class DepartmentDataGrid dan EmployeeDataGrid lebih terstruktur dan kita tidak membuat code program yang sama berulang-ulang, atau biasa dikenal dengan istilah DRY (Don't Repeat Yourself).

Source code DepartmentDataGrid.java:

package org.fajarapps.jpacrud.domain;

/* import ... tidak ditampilkan */

public class DepartmentDataGrid extends AbstractDataGrid
{
    List entries;

    /**
     * Default class constructor.
     */
    public DepartmentDataGrid() {
    }

    /**
     * Class constructor.
     */
    public DepartmentDataGrid(Page<Department> pages) {
        setPageable(pages);
    }

    public List getEntries() {
        return entries;
    }

    public void setEntries(List<Department> entries) {
        this.entries = entries;
    }

    /**
     * Menempatkan data hasil query ke dalam datagrid buffer.
     *
     * @param pages Data hasil query
     * @return Fluent interface, returns itself.
     */
    public DepartmentDataGrid setPageable(Page<Department> pages) {
        setEntries(pages.getContent());
        setNumberOfItems(pages.getNumberOfElements());
        setPage(pages.getNumber() + 1);
        setTotalItems(pages.getTotalElements());
        setTotalPages(pages.getTotalPages());

        return this;
    }
}

Source code EmployeeDataGrid.java:

package org.fajarapps.jpacrud.domain;

/* import ... tidak ditampilkan */

public class EmployeeDataGrid extends AbstractDataGrid
{
    private List entries;
    private Integer departmentId;
    private String term;

    /**
     * Default class constructor.
     */
    public EmployeeDataGrid() {
    }

    /**
     * Class constructor.
     */
    public EmployeeDataGrid(Page<Person> pages) {
        setPageable(pages);
    }

    /* method: getter dan setter ... tidak ditampilkan */

    /**
     * Menempatkan data hasil query ke dalam datagrid buffer.
     *
     * @param pages Data hasil query
     * @return Fluent interface, returns itself.
     */
    public EmployeeDataGrid setPageable(Page<Person> pages) {
        setEntries(pages.getContent());
        setNumberOfItems(pages.getNumberOfElements());
        setPage(pages.getNumber() + 1);
        setTotalItems(pages.getTotalElements());
        setTotalPages(pages.getTotalPages());

        return this;
    }
}

Spring Controller Class dan Freemarker View Templates

Sebelum membuat class Controller, kita terlebih dahulu membuat class Message dan tempatkan pada package org.fajarapps.jpacrud.domain.

Source code Message.java:

package org.fajarapps.jpacrud.domain;

import java.io.Serializable;

/**
 * Message binding helper yang dipergunakan untuk menampilkan flash message pada halaman html.
 */
public final class Message implements Serializable
{
    private static final long serialVersionUID = 7503750389106359368L;
    public static final String ERROR = "error";
    public static final String SUCCESS = "success";
    private String message;
    private String type;

    public Message(String type, String message) {
        this.message = message;
        this.type = type;
    }

   /* getter dan setter ...  tidak ditampilkan */

    @Override
    public String toString() {
        return "Message{" +
                "message='" + message + '\'' +
                ", type='" + type + '\'' +
                '}';
    }
}

Class ini diperlukan untuk menampilkan pesan error ataupun success pada halaman web setelah operasi CRUD dilaksanakan.

Selanjutnya pada Project Explorer pilih package org.fajarapps.jpacrud dan buat package baru yakni controller. Pada package yang baru dibuat ini kita buat 3 class controller yakni: HomeController, DepartmentController dan EmployeeController.

Sedangkan semua file freemarker template kita tempatkan pada folder src/main/webapp/WEB-INF/freemarker dan memiliki extension .ftl.

Source code HomeController.java:

package org.fajarapps.jpacrud.controller;

/* import ... tidak ditampilkan */

/**
 * Controller untuk menampilkan halaman utama.
 */
@Controller
@RequestMapping(value = "/")
public class HomeController
{
    @RequestMapping(method = RequestMethod.GET)
    public String index(Model model) {
        model.addAttribute("pageTitle", "Welcome to JPA-Crud Project");

        return "index";
    }
}

Class HomeController dipergunakan untuk menampilkan halaman landing page (halaman utama).

Source code DepartmentController.java:

package org.fajarapps.jpacrud.controller;

/* import ... tidak ditampilkan */

/**
 * Controller untuk mengelola data Department.
 */
@Controller
@RequestMapping(value = "/department")
public class DepartmentController
{
    @Autowired
    private DepartmentModel departmentModel;
    @Autowired
    private MessageSource messageSource;


    @InitBinder
    public void initBinder(WebDataBinder binder) {
//        true treats empty string as NULL
        binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
    }

    // Menyimpan entity Department yang baru dibuat
    @RequestMapping(value = "/create", method = RequestMethod.POST)
    public String createDepartment(@ModelAttribute("department") @Valid Department department, BindingResult result,
                                   Model model, RedirectAttributes redirectAttributes, Locale locale) {
        model.addAttribute("pageTitle", "New Department - JPA-Crud Project");

        if (result.hasErrors()) {
            model.addAttribute("messages", new Message(Message.ERROR,
                                                       messageSource.getMessage("validation.field.errors",
                                                                                new Object[]{}, locale)));
            return "department/form";
        }

        try {
            departmentModel.create(department);
            redirectAttributes.addFlashAttribute("messages", new Message(Message.SUCCESS, messageSource.getMessage(
                    "entity.department.CreateSuccess", new Object[]{}, locale)));
        } catch (Exception exc) {
            model.addAttribute("messages", new Message(Message.ERROR, exc.getMessage()));
            return "department/form";
        }

        return "redirect:/department";
    }

    // Menampilkan halaman form: menambah Department baru
    @RequestMapping(value = "/create", method = RequestMethod.GET)
    public String createDepartmentForm(Model model) {
        Department entity = new Department();
        model.addAttribute("department", entity);
        model.addAttribute("pageTitle", "New Department - JPA-Crud Project");

        return "department/form";
    }

    // Menghapus entity Department
    @RequestMapping(value = "/delete", method = RequestMethod.POST)
    public String deleteDepartment(@RequestParam("dept") List<Integer> deptIds, RedirectAttributes redirectAttributes,
                                   Locale locale) {
        try {
            int num = departmentModel.delete(deptIds);
            redirectAttributes.addFlashAttribute("messages", new Message(Message.SUCCESS, messageSource.getMessage(
                    "entity.department.DeleteSuccess", new Object[]{num}, locale)));
        } catch (Exception exc) {
            redirectAttributes.addFlashAttribute("messages", new Message(Message.SUCCESS, messageSource.getMessage(
                    "entity.department.DeleteFailed", new Object[]{}, locale)));
        }

        return "redirect:/department";
    }

    // Menampilkan halaman daftar Department (halaman awal)
    @RequestMapping(method = RequestMethod.GET)
    public String displayAll(Model model) {
        DepartmentDataGrid dataGrid = new DepartmentDataGrid();
        Page result = departmentModel.listAll(
                new PageRequest(0, dataGrid.getPageSize(), Sort.Direction.ASC, "deptName"));
        dataGrid.setPageable(result).setSortDir("asc").setSortField("deptName");

        model.addAttribute("dataGrid", dataGrid);
        model.addAttribute("pages", result);
        model.addAttribute("pageTitle", "Department - JPA-Crud Project");

        return "department/list";
    }

    // Menampilkan halaman daftar Department (response dari action: next-page, last-page, previous-page, dan first-page)
    @RequestMapping(method = RequestMethod.POST)
    public String displayAllBinding(DepartmentDataGrid dataGrid, Model model) {
        int page = dataGrid.getPage() - 1;
        int pageSize = dataGrid.getPageSize();
        Page result = departmentModel.listAll(
                new PageRequest(page, pageSize, dataGrid.getSortSpec("deptName")));
        dataGrid.setPageable(result);
        model.addAttribute("dataGrid", dataGrid);
        model.addAttribute("pages", result);
        model.addAttribute("pageTitle", "Department - JPA-Crud Project");

        return "department/list";
    }

    // Menyimpan pembaruan entity Department
    @RequestMapping(value = "/edit", method = RequestMethod.POST)
    public String updateDepartment(@ModelAttribute("department") @Valid Department department, BindingResult result, Model model,
                                   RedirectAttributes redirectAttributes, Locale locale) {
        model.addAttribute("pageTitle", "Edit Department - JPA-Crud Project");

        if (result.hasErrors()) {
            model.addAttribute("messages", new Message(Message.ERROR,
                                                       messageSource.getMessage("validation.field.errors",
                                                                                new Object[]{}, locale)));
            return "department/form";
        }

        try {
            departmentModel.update(department);
            redirectAttributes.addFlashAttribute("messages", new Message(Message.SUCCESS, messageSource.getMessage(
                    "entity.department.UpdateSuccess", new Object[]{}, locale)));
        } catch (Exception exc) {
            model.addAttribute("messages", new Message(Message.ERROR, exc.getMessage()));
            return "department/form";
        }

        return "redirect:/department";
    }

    // Menampilkan halaman form: Sunting Department
    @RequestMapping(value = "/edit/{id}", method = RequestMethod.GET)
    public String updateDepartmentForm(@PathVariable("id") Integer id, Model model) {
        Department department = departmentModel.find(id);
        model.addAttribute("department", department);
        model.addAttribute("pageTitle", "Edit Department - JPA-Crud Project");

        return "department/form";
    }
}

Class DepartmentController dipergunakan untuk memproses halaman web yang berhubungan dengan menu Department.

Source code template src/main/webapp/WEB-INF/freemarker/department/form.ftl:

<#import "/spring.ftl" as spring />
<#assign xhtmlCompliant = true in spring>
<!doctype html>
<html>
<head>
<#include "../head-meta.ftl"/>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
  <div class="container">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar"
              aria-expanded="false" aria-controls="navbar">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="/<@spring.url "/"/>">JPA-Crud Project</a>
    </div>
    <div id="navbar" class="navbar-collapse collapse">
      <ul class="nav navbar-nav">
        <li><a href="/<@spring.url "/"/>">Home</a></li>
        <li><a href="/<@spring.url "/employee"/>">Employee</a></li>
        <li class="active"><a href="/<@spring.url "/department"/>">Department</a></li>
      </ul>
    </div>
  </div>
</nav>

<section class="container">
  <div class="page-header">
    <h1><#if (department.deptId)??>Sunting<#else>Tambah</#if> Departemen</h1>
  </div>
<#include "../messages.ftl"/>
  <form method="post" action="<#if (department.deptId)??><@spring.url "/department/edit"/>
  <#else><@spring.url "/department/create"/></#if>" id="deptForm" class="form-horizontal">
    <div class="form-group">
    <@spring.bind "department.deptName"/>
      <label for="${spring.status.expression}" class="control-label field-primary col-sm-3 col-md-2">Departemen</label>
      <div class="col-sm-9 col-md-7">
      <@spring.formInput "department.deptName", "class=\"form-control required\" minlength=\"3\""/>
      <#list spring.status.errorMessages as error><label class="error">${error}</label></#list>
      </div>
    </div>
    <div class="form-group">
    <@spring.bind "department.description"/>
      <label for="${spring.status.expression}" class="control-label col-sm-3 col-md-2">Keterangan</label>
      <div class="col-sm-9 col-md-10">
      <@spring.formInput "department.description", "class=\"form-control\""/>
      <#list spring.status.errorMessages as error><label class="error">${error}</label></#list>
      </div>
    </div>
    <@spring.formHiddenInput "department.deptId"/>
    <div class="form-group">
      <label class="control-label col-sm-3 col-md-2"></label>
      <div class="col-sm-9 col-md-10">
        <button type="submit" class="btn btn-primary">Simpan</button>
        <a class="btn btn-default" href="/<@spring.url "/department"/>" role="button">Batal</a>
      </div>
    </div>
  </form>
</section>
<#include "../footer.ftl"/>
<script type="text/javascript">
  $(document).ready(function () {
    var frm = $("#deptForm");
    $(".container").tooltip({selector: "[data-toggle=tooltip]", placement: "top", container: "body"});
    frm.validate({
      ignore: ""
    });
    frm.find(":input").first().focus();
  });
</script>
</body>
</html>

Template diatas dipergunakan untuk menampilkan halaman Tambah Departemen dan Sunting Departemen.

Source template src/main/webapp/WEB-INF/freemarker/department/list.ftl:

<#import "/spring.ftl" as spring />
<#assign xhtmlCompliant = true in spring>
<!doctype html>
<html>
<head>
<#include "../head-meta.ftl"/>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
  <div class="container">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar"
              aria-expanded="false" aria-controls="navbar">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="/<@spring.url "/"/>">JPA-Crud Project</a>
    </div>
    <div id="navbar" class="navbar-collapse collapse">
      <ul class="nav navbar-nav">
        <li><a href="/<@spring.url "/"/>">Home</a></li>
        <li><a href="/<@spring.url "/employee"/>">Employee</a></li>
        <li class="active"><a href="/<@spring.url "/department"/>">Department</a></li>
      </ul>
    </div>
  </div>
</nav>

<#assign caret>
<span class="pull-right glyphicon glyphicon-triangle-<#if dataGrid.sortDir == "asc">top<#else>bottom</#if>"></span>
</#assign>
<section class="container">
  <div class="page-header">
    <h1>Daftar Departemen</h1>
  </div>
<#include "../messages.ftl"/>
  <form method="post" action="<@spring.url "/department"/>" id="tableForm" class="form-horizontal">
    <article class="table-responsive">
      <table class="table htgrid">
        <thead>
        <tr>
          <th class="htgrid-cell-header text-right">#</th>
          <th class="htgrid-cell-header text-center"><label><input type="checkbox" id="toggle-check"/></label></th>
          <th class="htgrid-cell-header">
            <div class="cell-header-inner" title="Klik untuk mengurutkan" data-toggle="tooltip" rel="deptName">
            <#if dataGrid.sortField == "deptName">${caret}</#if>Departemen
            </div>
          </th>
          <th class="htgrid-cell-header hidden-xs">Keterangan</th>
          <th class="htgrid-cell-header">
            <div class="cell-header-inner" title="Klik untuk mengurutkan" data-toggle="tooltip"
                 rel="COUNT(p.personId)"><#if dataGrid.sortField == "COUNT(p.personId)">${caret}</#if># Employee
            </div>
          </th>
          <th class="htgrid-cell-header text-center hidden-xs">Action</th>
        </tr>
        </thead>
        <tbody>
        <#escape x as x?html>
            <#assign startNumber="${(dataGrid.page - 1) * dataGrid.pageSize}"/>
            <#list dataGrid.entries as item>
            <tr>
                <#assign offset="${(startNumber?number + item_index + 1)}">
              <td class="text-right">${offset}</td>
              <td class="text-center"><label>
                <input type="checkbox" id="cb${offset}" name="dept" value="${item.deptId}"/></label></td>
              <td>${item.deptName}</td>
              <td class="hidden-xs">${item.description}</td>
              <td>${item.numberOfPerson!}</td>
              <td class="text-center hidden-xs">
                <a class="btn btn-xs btn-default" href="/<@spring.url "/department/edit/${item.deptId}"/>"
                   title="Sunting" data-toggle="tooltip" role="button">
                  <span class="glyphicon glyphicon-edit"></span></a>
              </td>
            </tr>
            </#list>
        </#escape>
        </tbody>
      </table>
    </article>
  <#include "../tablegrid-footer.ftl"/>
  </form>
  <div class="form-group">
    <div class="pull-right">
      <a class="btn btn-default" href="/<@spring.url "/department/create"/>" title="Tambah departemen baru"
         data-toggle="tooltip" role="button"><span class="glyphicon glyphicon-plus"></span> New Department</a>
      <button type="button" id="delete-department" class="btn btn-danger" title="Hapus departemen"
              data-toggle="tooltip"><span class="glyphicon glyphicon-trash"></span> Delete Department</button>
    </div>
  </div>
</section>
<#include "../footer.ftl"/>
<script type="text/javascript">
  $(document).ready(function () {
    $(".container").tooltip({selector: "[data-toggle=tooltip]", placement: "top", container: "body"});
    $("#toggle-check").checkAll();
    $("#tableForm").HTGridAction(${dataGrid.totalPages});
    $("#delete-department").click(function () {
      var nchk = 0;
      $(":input[id^=cb]").each(function () {
        if (this.checked == true) {
          nchk++;
        }
      });
      if (nchk > 0) {
        $("#tableForm").attr("action", "<@spring.url "/department/delete"/>").submit();
      }
    });
  });
</script>
</body>
</html>

Template diatas dipergunakan untuk menampilkan halaman Daftar Departemen dalam bentuk dataGrid.

Source code EmployeeController.java:

package org.fajarapps.jpacrud.controller;

/* import ... tidak ditampilkan */

/**
 * Controller untuk mengelola daftar personil.
 */
@Controller
@RequestMapping(value = "/employee")
public class EmployeeController
{
    @Autowired
    private IDepartmentRepository departmentRepository;
    @Autowired
    private PersonModel personModel;
    @Autowired
    private MessageSource messageSource;

    @InitBinder
    public void initBinder(WebDataBinder binder) {
//        true treats empty string as NULL
        binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
    }

    // Menyimpan entity Person baru
    @RequestMapping(value = "/create", method = RequestMethod.POST)
    public String createPerson(@ModelAttribute("person") @Valid Person person, BindingResult result, Model model,
                               RedirectAttributes redirectAttributes, Locale locale) {
        model.addAttribute("pageTitle", "New Employee - JPA-Crud Project");

        if (result.hasErrors()) {
            model.addAttribute("messages", new Message(Message.ERROR,
                                                       messageSource.getMessage("validation.field.errors",
                                                                                new Object[]{}, locale)));
            return "employee/form";
        }

        try {
            personModel.create(person);
            redirectAttributes.addFlashAttribute("messages", new Message(Message.SUCCESS, messageSource.getMessage(
                    "entity.person.CreateSuccess", new Object[]{}, locale)));
        } catch (Exception exc) {
            model.addAttribute("messages", new Message(Message.ERROR, exc.getMessage()));
            return "employee/form";
        }

        return "redirect:/employee";
    }

    // Menampilkan halaman form: Tambah Employee
    @RequestMapping(value = "/create", method = RequestMethod.GET)
    public String createPersonForm(Model model) {
        Person person = new Person();
        model.addAttribute("person", person);
        model.addAttribute("gender", person);
        model.addAttribute("pageTitle", "New Employee - JPA-Crud Project");

        return "employee/form";
    }

    // Menghapus entity Person
    @RequestMapping(value = "/delete", method = RequestMethod.POST)
    public String deletePerson(@RequestParam("pid") List<Long> personIds, RedirectAttributes redirectAttributes,
                               Locale locale) {
        try {
            int num = personModel.delete(personIds);
            redirectAttributes.addFlashAttribute("messages", new Message(Message.SUCCESS, messageSource.getMessage(
                    "entity.person.DeleteSuccess", new Object[]{num}, locale)));
        } catch (Exception exc) {
            redirectAttributes.addFlashAttribute("messages", new Message(Message.SUCCESS, messageSource.getMessage(
                    "entity.person.DeleteFailed", new Object[]{}, locale)));
        }

        return "redirect:/employee";
    }

    // Menampilkan halaman Daftar Employee (halaman awal)
    @RequestMapping(method = RequestMethod.GET)
    public String displayAll(Model model) {
        EmployeeDataGrid dataGrid = new EmployeeDataGrid();
        Page result = personModel.listAll(
                new PageRequest(0, dataGrid.getPageSize(), Sort.Direction.ASC, "fullname"));
        dataGrid.setPageable(result).setSortDir("asc").setSortField("fullname");

        model.addAttribute("dataGrid", dataGrid);
        model.addAttribute("pages", result);
        model.addAttribute("pageTitle", "Employee - JPA-Crud Project");

        return "employee/list";
    }

    // Menampilkan halaman Daftar Employee (response dari action: next-page, last-page, previous-page dan first-page)
    @RequestMapping(method = RequestMethod.POST)
    public String displayAllBinding(EmployeeDataGrid employeeGrid, Model model) {
        int page = employeeGrid.getPage() - 1;
        int pageSize = employeeGrid.getPageSize();

        PageRequest paging = new PageRequest(page, pageSize, employeeGrid.getSortSpec("fullname"));
        Page result = personModel.findAllByCriteria(employeeGrid.getDepartmentId(), employeeGrid.getTerm(),
                                                            paging);
        employeeGrid.setPageable(result);
        model.addAttribute("dataGrid", employeeGrid);
        model.addAttribute("pages", result);
        model.addAttribute("pageTitle", "Employee - JPA-Crud Project");

        return "employee/list";
    }

    @ModelAttribute(value = "departments")
    public Iterable<Department> listDepartments() {
        return departmentRepository.findAll(new Sort(Sort.Direction.ASC, "deptName"));
    }

    // Menyimpan pembaruan entity Person
    @RequestMapping(value = "/edit", method = RequestMethod.POST)
    public String updatePerson(@ModelAttribute("person") @Valid Person person, BindingResult result, Model model,
                               RedirectAttributes redirectAttributes, Locale locale) {
        model.addAttribute("pageTitle", "Edit Employee - JPA-Crud Project");

        if (result.hasErrors()) {
            model.addAttribute("messages", new Message(Message.ERROR,
                                                       messageSource.getMessage("validation.field.errors",
                                                                                new Object[]{}, locale)));
            return "employee/form";
        }

        try {
            personModel.update(person);
            redirectAttributes.addFlashAttribute("messages", new Message(Message.SUCCESS, messageSource.getMessage(
                    "entity.person.UpdateSuccess", new Object[]{}, locale)));
        } catch (Exception exc) {
            model.addAttribute("messages", new Message(Message.ERROR, exc.getMessage()));
            return "employee/form";
        }

        return "redirect:/employee";
    }

    // Menampilkan halaman form: Sunting Employee
    @RequestMapping(value = "/edit/{id}", method = RequestMethod.GET)
    public String updatePersonForm(@PathVariable("id") Long id, Model model) {
        Person person = personModel.find(id);
        model.addAttribute("person", person);
        model.addAttribute("pageTitle", "Edit Employe - JPA-Crud Project");

        return "employee/form";
    }
}

Class EmployeeController dipergunakan untuk memproses halaman web yang berhubungan dengan menu Employee.

Source code template src/main/webapp/WEB-INF/freemarker/employee/form.ftl:

<#import "/spring.ftl" as spring />
<#assign xhtmlCompliant = true in spring>
<!doctype html>
<html>
<head>
<#include "../head-meta.ftl"/>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
  <div class="container">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar"
              aria-expanded="false" aria-controls="navbar">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="/<@spring.url "/"/>">JPA-Crud Project</a>
    </div>
    <div id="navbar" class="navbar-collapse collapse">
      <ul class="nav navbar-nav">
        <li><a href="/<@spring.url "/"/>">Home</a></li>
        <li class="active"><a href="/<@spring.url "/employee"/>">Employee</a></li>
        <li><a href="/<@spring.url "/department"/>">Department</a></li>
      </ul>
    </div>
  </div>
</nav>

<div class="container">
  <div class="page-header">
    <h1><#if (person.personId)??>Sunting<#else>Tambah</#if> Employee</h1>
  </div>
    <#include "../messages.ftl"/>
  <form method="post" action="<#if (person.personId)??><@spring.url "/employee/edit"/>
  <#else><@spring.url "/employee/create"/></#if>" id="personForm" class="form-horizontal">
    <div class="form-group">
        <@spring.bind "person.fullname"/>
      <label for="${spring.status.expression}" class="control-label field-primary col-sm-3 col-md-2">Nama
        Lengkap</label>
      <div class="col-sm-9 col-md-7">
          <@spring.formInput "person.fullname", "class=\"form-control required\" minlength=\"5\""/>
          <#list spring.status.errorMessages as error><label class="error">${error}</label></#list>
      </div>
    </div>
    <div class="form-group">
        <@spring.bind "person.department"/>
      <label for="departments" class="control-label field-primary col-sm-3 col-md-2">Departemen</label>
      <div class="col-sm-9 col-md-5">
        <select name="${spring.status.expression}.deptId" class="chosen-select required"
                data-placeholder="-- Pilih Departemen --">
          <option value=""></option>
            <#list departments as dept>
              <option value="${dept.deptId}" <#if (person.department.deptId)?? && dept.deptId == person.department.deptId>
                      selected="selected"</#if> >${dept.deptName}</option>
            </#list>
        </select>
          <#list spring.status.errorMessages as error><label class="error">${error}</label></#list>
      </div>
    </div>
    <div class="form-group">
        <@spring.bind "person.address"/>
      <label for="${spring.status.expression}" class="control-label field-primary col-sm-3 col-md-2">Alamat</label>
      <div class="col-sm-9 col-md-7">
          <@spring.formTextarea "person.address", "class=\"form-control required\""/>
          <#list spring.status.errorMessages as error><label class="error">${error}</label></#list>
      </div>
    </div>
    <div class="form-group">
        <@spring.bind "person.province"/>
      <label for="${spring.status.expression}" class="control-label field-primary col-sm-3 col-md-2">Propinsi</label>
      <div class="col-sm-9 col-md-7">
          <@spring.formInput "person.province", "class=\"form-control required\""/>
          <#list spring.status.errorMessages as error><label class="error">${error}</label></#list>
      </div>
    </div>
    <div class="form-group">
        <@spring.bind "person.homePhone"/>
      <label for="${spring.status.expression}" class="control-label col-sm-3 col-md-2">Telpon Rumah</label>
      <div class="col-sm-5 col-md-3">
          <@spring.formInput "person.homePhone", "class=\"form-control\""/>
          <#list spring.status.errorMessages as error><label class="error">${error}</label></#list>
      </div>
    </div>
    <div class="form-group">
        <@spring.bind "person.workPhone"/>
      <label for="${spring.status.expression}" class="control-label col-sm-3 col-md-2">Telpon Kantor</label>
      <div class="col-sm-5 col-md-3">
          <@spring.formInput "person.workPhone", "class=\"form-control\""/>
          <#list spring.status.errorMessages as error><label class="error">${error}</label></#list>
      </div>
    </div>
    <div class="form-group">
        <@spring.bind "person.mobilePhone"/>
      <label for="${spring.status.expression}" class="control-label col-sm-3 col-md-2">No. Handphone</label>
      <div class="col-sm-5 col-md-3">
          <@spring.formInput "person.mobilePhone", "class=\"form-control\""/>
          <#list spring.status.errorMessages as error><label class="error">${error}</label></#list>
      </div>
    </div>
    <div class="form-group">
        <@spring.bind "person.email"/>
      <label for="${spring.status.expression}" class="control-label col-sm-3 col-md-2">Email</label>
      <div class="col-sm-5 col-md-3">
          <@spring.formInput "person.email", "class=\"form-control email\""/>
          <#list spring.status.errorMessages as error><label class="error">${error}</label></#list>
      </div>
    </div>
    <div class="form-group">
        <@spring.bind "person.gender"/>
      <label for="${spring.status.expression}" class="control-label field-primary col-sm-3 col-md-2">Jenis Kelamin</label>
      <div class="col-sm-7 col-md-6">
        <div class="btn-group" data-toggle="buttons">
          <label class="btn btn-primary <#if (person.gender)?? && person.gender == "L">active</#if>">
            <input type="radio" name="${spring.status.expression}" id="option1" autocomplete="off" value="L"
                   <#if (person.gender)?? && person.gender == "L">checked</#if>>Laki-Laki
          </label>
          <label class="btn btn-primary <#if (person.gender)?? && person.gender == "P">active</#if>">
            <input type="radio" name="${spring.status.expression}" id="option2" autocomplete="off" value="P"
                   <#if (person.gender)?? && person.gender == "P">checked</#if>>Perempuan
          </label>
        </div>
          <#list spring.status.errorMessages as error><label class="error">${error}</label></#list>
      </div>
    </div>
    <div class="form-group">
        <@spring.bind "person.birthPlace"/>
      <label for="${spring.status.expression}" class="control-label col-sm-3 col-md-2">Tempat Lahir</label>
      <div class="col-sm-9 col-md-7">
          <@spring.formInput "person.birthPlace", "class=\"form-control\""/>
          <#list spring.status.errorMessages as error><label class="error">${error}</label></#list>
      </div>
    </div>
    <div class="form-group">
        <@spring.bind "person.birthDate"/>
      <label for="${spring.status.expression}" class="control-label field-primary col-sm-3 col-md-2">Tanggal Lahir</label>
      <div class="col-sm-5 col-md-3">
          <@spring.formInput "person.birthDate", "class=\"form-control\" placeholder=\"format: dd-mm-yyyy\""/>
          <#list spring.status.errorMessages as error><label class="error">${error}</label></#list>
      </div>
    </div>
      <@spring.formHiddenInput "person.personId"/>
    <div class="form-group">
      <label class="control-label col-sm-3 col-md-2"></label>
      <div class="col-sm-9 col-md-10">
        <button type="submit" class="btn btn-primary">Simpan</button>
        <a class="btn btn-default" href="/<@spring.url "/employee"/>" role="button">Batal</a>
      </div>
    </div>
  </form>
</div>
<#include "../footer.ftl"/>
<script type="text/javascript">
  $(document).ready(function () {
    var frm = $("#personForm");
    $(".container").tooltip({selector: "[data-toggle=tooltip]", placement: "top", container: "body"});
    $(".chosen-select").chosen({ allow_single_deselect: true });
    frm.validate({
      ignore: "",
      rules : {email: true}
    });
    frm.find(":input").first().focus();
  });
</script>
</body>
</html>

Template diatas dipergunakan untuk menampilkan halaman form: Tambah Employee dan Sunting Employee.

Source code template src/main/webapp/WEB-INF/freemarker/employee/list.ftl:

<#import "/spring.ftl" as spring />
<#assign xhtmlCompliant = true in spring>
<!doctype html>
<html>
<head>
<#include "../head-meta.ftl"/>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
  <div class="container">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar"
              aria-expanded="false" aria-controls="navbar">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="/<@spring.url "/"/>">JPA-Crud Project</a>
    </div>
    <div id="navbar" class="navbar-collapse collapse">
      <ul class="nav navbar-nav">
        <li><a href="/<@spring.url "/"/>">Home</a></li>
        <li class="active"><a href="/<@spring.url "/employee"/>">Employee</a></li>
        <li><a href="/<@spring.url "/department"/>">Department</a></li>
      </ul>
    </div>
  </div>
</nav>

<#assign caret>
<span class="pull-right glyphicon glyphicon-triangle-<#if dataGrid.sortDir == "asc">top<#else>bottom</#if>"></span>
</#assign>
<section class="container">
  <div class="page-header">
    <h1>Daftar Employee</h1>
  </div>
<#include "../messages.ftl"/>
  <form method="post" action="<@spring.url "/employee"/>" id="tableForm" class="form-horizontal">
    <div class="clearfix" style="margin-bottom: 20px">
      <div class="col-xs-5 col-sm-4 col-md-3 col-lg-2 pull-right">
        <div class="row">
          <select id="departments" name="departmentId" class="chosen-select" data-placeholder="-- Pilih Departemen --">
            <option value=""></option>
          <#list departments as dept>
            <option value="${dept.deptId}"
                    <#if (dataGrid.departmentId)?? && dataGrid.departmentId == dept.deptId>selected</#if>>${dept.deptName}</option>
          </#list>
          </select>
        </div>
      </div>
      <div class="col-xs-6 col-sm-4">
        <div class="row">
          <div class="input-group input-group">
          <@spring.formInput "dataGrid.term", "class=\"form-control\" data-toggle=\"tooltip\" placeholder=\"Ketik nama employee\""/>
            <span class="input-group-btn">
                  <button type="submit" class="btn btn-default"><i class="glyphicon glyphicon-search"></i>
                  </button>
              </span>
          </div>
        </div>
      </div>
    </div>
    <article class="table-responsive">
      <table class="table htgrid">
        <thead>
        <tr>
          <th class="htgrid-cell-header text-right">#</th>
          <th class="htgrid-cell-header text-center"><label><input type="checkbox" id="toggle-check"/></label></th>
          <th class="htgrid-cell-header">
            <div class="cell-header-inner text-nowrap" title="Klik untuk mengurutkan" data-toggle="tooltip"
                 rel="fullname">
            <#if dataGrid.sortField == "fullname">${caret}</#if>Nama Lengkap
            </div>
          </th>
          <th class="htgrid-cell-header">
            <div class="cell-header-inner" title="Klik untuk mengurutkan" data-toggle="tooltip"
                 rel="department.deptName"><#if dataGrid.sortField == "department.deptName">${caret}</#if>Departemen
            </div>
          </th>
          <th class="htgrid-cell-header hidden-xs hidden-sm hidden-md">Alamat</th>
          <th class="htgrid-cell-header">
            <div class="cell-header-inner" title="Klik untuk mengurutkan" data-toggle="tooltip" rel="province">
            <#if dataGrid.sortField == "province">${caret}</#if>Propinsi
            </div>
          </th>
          <th class="htgrid-cell-header hidden-xs">
            <div class="cell-header-inner" title="Klik untuk mengurutkan" data-toggle="tooltip" rel="birthPlace">
            <#if dataGrid.sortField == "birthPlace">${caret}</#if>Tempat Lahir
            </div>
          </th>
          <th class="htgrid-cell-header hidden-xs hidden-sm">
            <div class="cell-header-inner" title="Klik untuk mengurutkan" data-toggle="tooltip" rel="birthDate">
            <#if dataGrid.sortField == "birthDate">${caret}</#if>Tgl. Lahir
            </div>
          </th>
          <th class="htgrid-cell-header hidden-xs hidden-sm">
            <div class="cell-header-inner" title="Klik untuk mengurutkan" data-toggle="tooltip" rel="tsCreated">
            <#if dataGrid.sortField == "tsCreated">${caret}</#if>Tgl. Terdaftar
            </div>
          </th>
          <th class="htgrid-cell-header hidden-xs hidden-sm">Action</th>
        </tr>
        </thead>
        <tbody>
        <#escape x as x?html>
            <#assign startNumber="${(dataGrid.page - 1) * dataGrid.pageSize}"/>
            <#list dataGrid.entries as person>
            <tr>
                <#assign offset="${startNumber?number + person_index + 1}">
              <td class="text-right">${offset}</td>
              <td class="text-center"><label>
                <input type="checkbox" id="cb${offset}" name="pid" value="${person.personId}"/></label></td>
              <td class="text-nowrap">${person.fullname}</td>
              <td>${person.department.deptName}</td>
              <td class="hidden-xs hidden-sm hidden-md">${person.address}</td>
              <td>${person.province}</td>
              <td class="hidden-xs">${person.birthPlace}</td>
              <td class="hidden-xs hidden-sm">${person.birthDate?string("dd-MM-yyyy")}</td>
              <td class="text-nowrap">${person.tsCreated?string("dd-MM-yyyy HH:mm:ss")}</td>
              <td class="text-center hidden-xs hidden-sm">
                <a class="btn btn-xs btn-default" href="/<@spring.url "/employee/edit/${person.personId}"/>"
                   title="Sunting" data-toggle="tooltip" role="button">
                  <span class="glyphicon glyphicon-edit"></span></a>
              </td>
            </tr>
            </#list>
        </#escape>
        </tbody>
      </table>
    </article>
  <#include "../tablegrid-footer.ftl"/>
  </form>
  <div class="form-group">
    <div class="pull-right">
      <a class="btn btn-default" href="/<@spring.url "/employee/create"/>" role="button">
        <span class="glyphicon glyphicon-plus"></span> New Employee</a>
      <button type="button" id="delete-employee" class="btn btn-danger"><span class="glyphicon glyphicon-trash"></span>
        Delete Employee
      </button>
    </div>
  </div>
</section>
<#include "../footer.ftl"/>
<script type="text/javascript">
  $(document).ready(function () {
    $(".container").tooltip({selector: "[data-toggle=tooltip]", placement: "top", container: "body"});
    $("#toggle-check").checkAll();
    $(".chosen-select").chosen({ allow_single_deselect: true });
    $("#tableForm").HTGridAction(${dataGrid.totalPages});
    $("#delete-employee").click(function () {
      var nchk = 0;
      $(":input[id^=cb]").each(function () {
        if (this.checked == true) {
          nchk++;
        }
      });
      if (nchk > 0) {
        $("#tableForm").attr("action", "<@spring.url "/employee/delete"/>").submit();
      }
    });
  });
</script>
</body>
</html>

Template diatas dipergunakan untuk menampilkan halaman Daftar Employee dalam bentuk dataGrid.


Pagination Template Fragments

Template fragments ini dipergunakan oleh .../freemarker/department/list.ftl dan .../freemarker/employee/list.ftl untuk membuat pagination page pada dataGrid. Dapat dilihat pada cuplikan kode berikut ini:

<html>
<!-- Kode pada bagian ini tidak ditampilkan... -->
<section class="container">
<!-- Kode pada bagian ini tidak ditampilkan... -->
    <article class="table-responsive">
        <table class="table htgrid">
        <!-- Kode pada bagian ini tidak ditampilkan... -->
        </table>        
    </article>
    <#include "../tablegrid-footer.ftl"/>
    <!-- Kode pada bagian ini tidak ditampilkan... -->
</section>
<!-- Kode pada bagian ini tidak ditampilkan... -->
</html>

Source code template src/main/webapp/WEB-INF/freemarker/tablegrid-footer.ftl:

<div class="htgrid-footer">
  <div class="row">
    <div class="col-sm-5 text-muted">Menampilkan&nbsp;
    <#if pages.numberOfElements == 0>0<#else>${startNumber?number + 1}</#if>
      - ${startNumber?number + pages.numberOfElements} dari ${pages.totalElements} items
    </div>
    <div class="col-sm-7">
      <nav class="pull-right">
        <ul class="pagination">
        <#if pages.first>
          <li class="disabled">
            <a href="#"><i class="glyphicon glyphicon-step-backward"></i></a>
          </li>
          <li class="disabled">
            <a href="#"><i class="glyphicon glyphicon-chevron-left"></i></a>
          </li>
        <#else>
          <li>
            <a href="#" class="pager-first" title="Halaman awal" data-toggle="tooltip">
              <i class="glyphicon glyphicon-step-backward"></i></a>
          </li>
          <li>
            <a href="#" class="pager-prev" title="Halaman sebelumnya" data-toggle="tooltip"><i
                    class="glyphicon glyphicon-chevron-left"></i></a>
          </li>
        </#if>
          <li class="disabled"><a href="#">Page:</a></li>
          <li>
          <#if pages.totalPages == 0>
            <input type="hidden" name="page" value="1"/>
            <input type="text" name="pagenumber" value="0" class="form-control" disabled/>
          <#else>
            <input type="text" name="page" class="form-control number" value="${dataGrid.page}"/>
          </#if>
          </li>
          <li class="disabled" style="font-weight:600"><a href="#">/&nbsp;&nbsp;${pages.totalPages}</a></li>
        <#if pages.last>
          <li class="disabled">
            <a href="#"><i class="glyphicon glyphicon-chevron-right"></i></a>
          </li>
          <li class="disabled">
            <a href="#"><i class="glyphicon glyphicon-step-forward"></i></a>
          </li>
        <#else >
          <li>
            <a href="#" class="pager-next" title="Halaman selanjutnya" data-toggle="tooltip">
              <i class="glyphicon glyphicon-chevron-right"></i></a>
          </li>
          <li>
            <a href="#" class="pager-last" title="Halaman terakhir" data-toggle="tooltip">
              <i class="glyphicon glyphicon-step-forward"></i></a>
          </li>
        </#if>
        </ul>
      </nav>
    </div>
  </div>
  <div class="hidden">
    <input type="hidden" name="sortField" value="${dataGrid.sortField}"/>
    <input type="hidden" name="sortDir" value="${dataGrid.sortDir}"/>
    <input type="hidden" name="pageSize" value="${dataGrid.pageSize}"/>
  </div>
</div>

Summary

Sebelum menjalankan project melalui IntelliJ IDEA, konfigurasi terlebih dahulu tomcat-servlet 8.x agar dapat dijalankan via IDEA. Kemudian lengkapi beberapa file seperti:

  1. Javascript jquery.min.js, bootstrap.min.js, jquery.plugins.min.js dan main.js, tempatkan pada folder: src/main/webapp/scripts.
  2. Css files: bootstrap.min.css dan default.css, tempatkan pada folder: src/main/webapp/styles.
  3. Bootstrap assets glyphicons-*.*, tempatkan pada folder: src/main/webapp/fonts.
  4. Image assets, ditempatkan pada folder: src/main/webapp/assets.
  5. Beberapa template fragments yang tidak saya bahas.
  6. Beberapa java class yang tidak saya bahas.

Setelah konfigurasi dan file-file yang dibutuhkan dilengkapi barulah kita dapat menjalankan project melalui IntelliJ IDEA. Dari menubar, pilih menuitem: Run -> Run Tomcat 8 (shift+F10). Dibawah ini adalah contoh tampilan halaman web aplikasi JPA-CRUD.

Ada beberapa hal yang tidak saya bahas disini, seperti javascript: jQuery, Bootstrap, jQuery.plugins serta javascript buatan saya dan dipakai pada project web aplikasi, serta file css untuk mempercantik tampilan halaman web. Walaupun tidak dibahas Anda dapat melihat dan mendownload source code lengkap web aplikasi ini pada repository saya di Github. (Note: pilih tags v1.x, agar source code kontennya sesuai dengan pembahasan pada artikel ini).

Saya berharap Anda menyukai artikel ini dan dapat melengkapi pengetahuan tentang Spring-MVC, JPA dan Freemarker.

 

Met happy coding,..
Wassalam.