Showing posts with label Spring. Show all posts
Showing posts with label Spring. Show all posts
, , , , , , , ,

MongoDB - Replica Sets with Spring Data MongoDB (Part 2)

Review

In the previous section, we have introduced and discussed Replica Sets. In this section, we will install MongoDB and configure Replica Sets.


Installation

Our first step is to download and install MongoDB in each server. Since our server is running CentOS, we can download the CentOS MongoDB package by following the instructions from this link. However, it is easier to simply install the prebuilt binaries as stated in the MongoDB downloads section, so we will follow this advice instead.

1. Open a browser and visit the MongoDB download section at http://www.mongodb.org/downloads

2. Under the Production Release, choose the file that matches your operating system. In our case, it's Linux 32-bit.

3. Once downloaded, transfer the compressed file to all servers. (In each server, we have created a directory named /home/mongo. This is where we will extract the contents of the compressed file.)

4. Extract the contents by running the following command (you might need to modify the directory). Remember to do these on all servers.
tar -C /home/mongo/ -zxvf/home/mongo/mongodb-linux-i686-2.0.2.tgz 


5. Now we need to create the database directory for our MongoDB servers. By default, /data/db is used by MongoDB, so we'll create those directories by running the following command (Remember to do these on all servers):
mkdir -p/data/db 


6. Next, we will run MongoDB servers in a Replica Set using the following command (Again, do these step on all servers):
/home/mongo/mongodb-linux-i686-2.0.2/bin/./mongod --replSet cluster1


Server 1 should output the following log:
[Server #1] /home/mongo/mongodb-linux-i686-2.0.2/bin/./mongod --replSet cluster1
Sat Feb 18 11:14:36
Sat Feb 18 11:14:36 warning: 32-bit servers don't have journaling enabled by default. Please use --journal if you want durability.
Sat Feb 18 11:14:36
Sat Feb 18 11:14:36 [initandlisten] MongoDB starting : pid=5921 port=27017 dbpath=/data/db/ 32-bit host=28125_2_85413_357231
Sat Feb 18 11:14:36 [initandlisten]
Sat Feb 18 11:14:36 [initandlisten] ** NOTE: when using MongoDB 32 bit, you are limited to about 2 gigabytes of data
Sat Feb 18 11:14:36 [initandlisten] ** see http://blog.mongodb.org/post/137788967/32-bit-limitations
Sat Feb 18 11:14:36 [initandlisten] ** with --journal, the limit is lower
Sat Feb 18 11:14:36 [initandlisten]
Sat Feb 18 11:14:36 [initandlisten] db version v2.0.2, pdfile version 4.5
Sat Feb 18 11:14:36 [initandlisten] git version: 514b122d308928517f5841888ceaa4246a7f18e3
Sat Feb 18 11:14:36 [initandlisten] build info: Linux domU-12-31-39-01-70-B4 2.6.21.7-2.fc8xen #1 SMP Fri Feb 15 12:39:36 EST 2008 i686 BOOST_LIB_VERSION=1_41
Sat Feb 18 11:14:36 [initandlisten] options: { replSet: "cluster1" }
Sat Feb 18 11:14:36 [initandlisten] waiting for connections on port 27017
Sat Feb 18 11:14:36 [websvr] admin web console waiting for connections on port 28017
Sat Feb 18 11:14:36 [initandlisten] connection accepted from 127.0.0.1:39621 #1
Sat Feb 18 11:14:36 [rsStart] replSet can't get local.system.replset config from self or any seed (EMPTYCONFIG)
Sat Feb 18 11:14:36 [rsStart] replSet info you may need to run replSetInitiate -- rs.initiate() in the shell -- if that is not already done
Sat Feb 18 11:14:46 [rsStart] replSet can't get local.system.replset config from self or any seed (EMPTYCONFIG)
Sat Feb 18 11:14:56 [rsStart] replSet can't get local.system.replset config from self or any seed (EMPTYCONFIG)

Server 2 should output the following log:
[Server #2] /home/mongo/mongodb-linux-i686-2.0.2/bin/./mongod --replSet cluster1
Sat Feb 18 11:15:34
Sat Feb 18 11:15:34 warning: 32-bit servers don't have journaling enabled by default. Please use --journal if you want durability.
Sat Feb 18 11:15:34
Sat Feb 18 11:15:34 [initandlisten] MongoDB starting : pid=13534 port=27017 dbpath=/data/db/ 32-bit host=28125_2_82937_349828
Sat Feb 18 11:15:34 [initandlisten]
Sat Feb 18 11:15:34 [initandlisten] ** NOTE: when using MongoDB 32 bit, you are limited to about 2 gigabytes of data
Sat Feb 18 11:15:34 [initandlisten] ** see http://blog.mongodb.org/post/137788967/32-bit-limitations
Sat Feb 18 11:15:34 [initandlisten] ** with --journal, the limit is lower
Sat Feb 18 11:15:34 [initandlisten]
Sat Feb 18 11:15:34 [initandlisten] db version v2.0.2, pdfile version 4.5
Sat Feb 18 11:15:34 [initandlisten] git version: 514b122d308928517f5841888ceaa4246a7f18e3
Sat Feb 18 11:15:34 [initandlisten] build info: Linux domU-12-31-39-01-70-B4 2.6.21.7-2.fc8xen #1 SMP Fri Feb 15 12:39:36 EST 2008 i686 BOOST_LIB_VERSION=1_41
Sat Feb 18 11:15:34 [initandlisten] options: { replSet: "cluster1" }
Sat Feb 18 11:15:34 [initandlisten] waiting for connections on port 27017
Sat Feb 18 11:15:34 [websvr] admin web console waiting for connections on port 28017
Sat Feb 18 11:15:34 [initandlisten] connection accepted from 127.0.0.1:46620 #1
Sat Feb 18 11:15:34 [rsStart] replSet can't get local.system.replset config from self or any seed (EMPTYCONFIG)
Sat Feb 18 11:15:34 [rsStart] replSet info you may need to run replSetInitiate -- rs.initiate() in the shell -- if that is not already done
Sat Feb 18 11:15:44 [rsStart] replSet can't get local.system.replset config from self or any seed (EMPTYCONFIG)

Server 3 should output the following log:
[Server #3] /home/mongo/mongodb-linux-i686-2.0.2/bin/./mongod --replSet cluster1
Sat Feb 18 11:15:35
Sat Feb 18 11:15:35 warning: 32-bit servers don't have journaling enabled by default. Please use --journal if you want durability.
Sat Feb 18 11:15:35
Sat Feb 18 11:15:35 [initandlisten] MongoDB starting : pid=8902 port=27017 dbpath=/data/db/ 32-bit host=28125_2_85413_357219
Sat Feb 18 11:15:35 [initandlisten]
Sat Feb 18 11:15:35 [initandlisten] ** NOTE: when using MongoDB 32 bit, you are limited to about 2 gigabytes of data
Sat Feb 18 11:15:35 [initandlisten] ** see http://blog.mongodb.org/post/137788967/32-bit-limitations
Sat Feb 18 11:15:35 [initandlisten] ** with --journal, the limit is lower
Sat Feb 18 11:15:35 [initandlisten]
Sat Feb 18 11:15:35 [initandlisten] db version v2.0.2, pdfile version 4.5
Sat Feb 18 11:15:35 [initandlisten] git version: 514b122d308928517f5841888ceaa4246a7f18e3
Sat Feb 18 11:15:35 [initandlisten] build info: Linux domU-12-31-39-01-70-B4 2.6.21.7-2.fc8xen #1 SMP Fri Feb 15 12:39:36 EST 2008 i686 BOOST_LIB_VERSION=1_41
Sat Feb 18 11:15:35 [initandlisten] options: { replSet: "cluster1" }
Sat Feb 18 11:15:35 [initandlisten] waiting for connections on port 27017
Sat Feb 18 11:15:35 [websvr] admin web console waiting for connections on port 28017
Sat Feb 18 11:15:35 [initandlisten] connection accepted from 127.0.0.1:52051 #1
Sat Feb 18 11:15:35 [rsStart] replSet can't get local.system.replset config from self or any seed (EMPTYCONFIG)
Sat Feb 18 11:15:35 [rsStart] replSet info you may need to run replSetInitiate -- rs.initiate() in the shell -- if that is not already done
Sat Feb 18 11:15:45 [rsStart] replSet can't get local.system.replset config from self or any seed (EMPTYCONFIG)
Sat Feb 18 11:15:55 [rsStart] replSet can't get local.system.replset config from self or any seed (EMPTYCONFIG)

Server 4 should output the following log:
[Server #4] /home/mongo/mongodb-linux-i686-2.0.2/bin/./mongod --replSet cluster1
Sat Feb 18 11:15:37
Sat Feb 18 11:15:37 warning: 32-bit servers don't have journaling enabled by default. Please use --journal if you want durability.
Sat Feb 18 11:15:37
Sat Feb 18 11:15:37 [initandlisten] MongoDB starting : pid=11685 port=27017 dbpath=/data/db/ 32-bit host=28125_2_84690_354582
Sat Feb 18 11:15:37 [initandlisten]
Sat Feb 18 11:15:37 [initandlisten] ** NOTE: when using MongoDB 32 bit, you are limited to about 2 gigabytes of data
Sat Feb 18 11:15:37 [initandlisten] ** see http://blog.mongodb.org/post/137788967/32-bit-limitations
Sat Feb 18 11:15:37 [initandlisten] ** with --journal, the limit is lower
Sat Feb 18 11:15:37 [initandlisten]
Sat Feb 18 11:15:37 [initandlisten] db version v2.0.2, pdfile version 4.5
Sat Feb 18 11:15:37 [initandlisten] git version: 514b122d308928517f5841888ceaa4246a7f18e3
Sat Feb 18 11:15:37 [initandlisten] build info: Linux domU-12-31-39-01-70-B4 2.6.21.7-2.fc8xen #1 SMP Fri Feb 15 12:39:36 EST 2008 i686 BOOST_LIB_VERSION=1_41
Sat Feb 18 11:15:37 [initandlisten] options: { replSet: "cluster1" }
Sat Feb 18 11:15:37 [initandlisten] waiting for connections on port 27017
Sat Feb 18 11:15:37 [websvr] admin web console waiting for connections on port 28017
Sat Feb 18 11:15:37 [initandlisten] connection accepted from 127.0.0.1:51100 #1
Sat Feb 18 11:15:37 [rsStart] replSet can't get local.system.replset config from self or any seed (EMPTYCONFIG)
Sat Feb 18 11:15:37 [rsStart] replSet info you may need to run replSetInitiate -- rs.initiate() in the shell -- if that is not already done
Sat Feb 18 11:15:47 [rsStart] replSet can't get local.system.replset config from self or any seed (EMPTYCONFIG)
Sat Feb 18 11:15:57 [rsStart] replSet can't get local.system.replset config from self or any seed (EMPTYCONFIG)

7. Next step is to configure our MongoDB servers to act as a cluster. Follow the steps below:

  1. Login to Server 1
  2. Run a MongoDB client using the following command:
    /home/mongo/mongodb-linux-i686-2.0.2/bin/./mongo

  3. Initiate the MongoDB cluster
    rs.initiate({_id: 'cluster1', members: [
    {_id: 0, host: '123.456.78.90:27017'},
    {_id: 1, host: '123.456.78.91:27017'},
    {_id: 2, host: '123.456.78.92:27017'},
    {_id: 3, host: '123.456.78.93:27017', arbiterOnly: true}]
    })


    If you have accidentally initiated an incorrect configuration, you can reconfigure the configuration with:
    rs.reconfig({_id: 'cluster1', members: [
    {_id: 0, host: '123.456.78.90:27017'},
    {_id: 1, host: '123.456.78.91:27017'},
    {_id: 2, host: '123.456.78.92:27017'},
    {_id: 3, host: '123.456.78.93:27017', arbiterOnly: true}]
    }, true)


    Notice, the last host is an arbiter only server. This will participate in electing a primary but receive no data.

    You should see the following output from MongoDB client:
    > rs.initiate({_id: 'cluster1', members: [
    bye
    [Server #1] /home/mongo/mongodb-linux-i686-2.0.2/bin/./mongo
    MongoDB shell version: 2.0.2
    connecting to: test
    > rs.initiate({_id: 'cluster1', members: [
    ... {_id: 0, host: '123.456.78.90:27017'},
    ... {_id: 1, host: '123.456.78.91:27017'},
    ... {_id: 2, host: '123.456.78.92:27017'},
    ... {_id: 3, host: '123.456.78.93:27017', arbiterOnly: true}]
    ... })
    {
    "info" : "Config now saved locally. Should come online in about a minute.",
    "ok" : 1
    }

    Examine the output from Server 1
    Sat Feb 18 11:16:22 [conn2] replSet replSetInitiate admin command received from client
    Sat Feb 18 11:16:22 [conn2] replSet replSetInitiate config object parses ok, 4 members specified
    Sat Feb 18 11:16:22 [conn2] replSet replSetInitiate all members seem up
    Sat Feb 18 11:16:22 [conn2] ******
    Sat Feb 18 11:16:22 [conn2] creating replication oplog of size: 47MB...
    Sat Feb 18 11:16:22 [FileAllocator] allocating new datafile /data/db/local.ns, filling with zeroes...
    Sat Feb 18 11:16:22 [FileAllocator] creating directory /data/db/_tmp
    Sat Feb 18 11:16:22 [FileAllocator] done allocating datafile /data/db/local.ns, size: 16MB, took 0.053 secs
    Sat Feb 18 11:16:22 [FileAllocator] allocating new datafile /data/db/local.0, filling with zeroes...
    Sat Feb 18 11:16:22 [FileAllocator] done allocating datafile /data/db/local.0, size: 16MB, took 0.106 secs
    Sat Feb 18 11:16:22 [FileAllocator] allocating new datafile /data/db/local.1, filling with zeroes...
    Sat Feb 18 11:16:23 [FileAllocator] done allocating datafile /data/db/local.1, size: 32MB, took 1.124 secs
    Sat Feb 18 11:16:23 [FileAllocator] allocating new datafile /data/db/local.2, filling with zeroes...
    Sat Feb 18 11:16:26 [FileAllocator] done allocating datafile /data/db/local.2, size: 64MB, took 3.228 secs
    Sat Feb 18 11:16:29 [conn2] ******
    Sat Feb 18 11:16:29 [rsStart] replSet can't get local.system.replset config from self or any seed (EMPTYCONFIG)
    Sat Feb 18 11:16:29 [conn2] replSet info saving a newer config version to local.system.replset
    Sat Feb 18 11:16:29 [conn2] replSet saveConfigLocally done
    Sat Feb 18 11:16:29 [conn2] replSet replSetInitiate config now saved locally. Should come online in about a minute.
    Sat Feb 18 11:16:29 [conn2] command admin.$cmd command: { replSetInitiate: { _id: "cluster1", members: [ { _id: 0.0, host: "123.456.78.90:27017" }, { _id: 1.0, host: "123.456.78.91:27017" }, { _id: 2.0, host: "123.456.78.92:27017" }, { _id: 3.0, host: "123.456.78.93:27017", arbiterOnly: true } ] } } ntoreturn:1 reslen:112 7192ms
    Sat Feb 18 11:16:39 [rsStart] replSet STARTUP2
    Sat Feb 18 11:16:39 [rsHealthPoll] replSet member 123.456.78.91:27017 is up
    Sat Feb 18 11:16:39 [rsHealthPoll] replSet member 123.456.78.92:27017 is up
    Sat Feb 18 11:16:39 [rsHealthPoll] replSet member 123.456.78.93:27017 is up

Next

In the next section, we will configure our Spring application to support Replica Sets. Click here to proceed.
Continue reading MongoDB - Replica Sets with Spring Data MongoDB (Part 2)
, , , , , , ,

MongoDB - Replica Sets with Spring Data MongoDB (Part 1)

Introduction

In this tutorial, we will study how to setup MongoDB Replica Sets using a Spring Data MongoDB-based application for testing our replica sets. We will not create a MongoDB application here but instead we will reuse an existing one from the Spring MVC 3.1 - Implement CRUD with Spring Data MongoDB guide. For this study, we will use production-ready, cloud servers to demonstrate in real-time the results.


What is a Replica Set?

Replica sets are a form of asynchronous master/slave replication, adding automatic failover and automatic recovery of member nodes.

  • A replica set consists of two or more nodes that are copies of each other. (i.e.: replicas)
  • The replica set automatically elects a primary (master). No one member is intrinsically primary; that is, this is a share-nothing design.
  • Drivers (and mongos) can automatically detect when a replica set primary changes and will begin sending writes to the new primary.

Replica sets have several common uses:
  • Data Redundancy
  • Automated Failover / High Availability
  • Distributing read load
  • Simplify maintenance (compared to "normal" master-slave)
  • Disaster recovery

Source: MongoDB Replica Sets

Servers

We have four cloud servers (for security purposes, I have modified their actual IP addresses). All servers are running with the following configuration:
OS: CentOS release 5.2 (Final)
RAM: 512MB
HD: 30GB

Here are our servers:
Server #IP addressMongoDB portComments
Server 1123.456.78.9027017primary server
Server 2123.456.78.9127017slave server
Server 3123.456.78.9227017slave server
Server 4123.456.78.9327017arbiter server (see note below)

Although Server 1 is the primary server, this is not permanent. Whenever the primary server is down, another server is elected as a primary. Server 4 is an arbiter server which is useful for certain cases in electing a primary server.

Note: Our Spring Data MongoDB-based application is hosted in Server 1.

What is an Arbiter?
Arbiters are nodes in a replica set that only participate in elections: they don't have a copy of the data and will never become the primary node (or even a readable secondary). They are mainly useful for breaking ties during elections (e.g. if a set only has two members).

When to add an arbiter
  • Two members with data : add an arbiter to have three voters. 2 out of 3 votes for a member establishes it as primary.
  • Three members with data : no need to add an arbiter. In fact having 4 voters is worse as 3 of 4 needed to elect a primary instead of 2 of 3. In theory one might add two arbiters thus making number of votes five, and 3 of 5 would be ok; however this is uncommon and generally not recommended.
  • Four members with data : add one arbiter.

Source: MongoDB - Adding an Arbiter

Do I need an Arbiter?
You need an arbiter if you have an even number of votes. As an extension to this, at most you should only ever have 1 arbiter. If you aren't sure how many votes you have, it's probably the same as the number of servers in the set you have (including slaves, hidden, arbiters).

Source: Does My MongoDB Replica Set Need An Arbiter?

Next

In the next section, we will install MongoDB on our servers and configure Replica Sets. Click here to proceed.
Continue reading MongoDB - Replica Sets with Spring Data MongoDB (Part 1)
, , , , , , , ,

MongoDB - Replica Sets with Spring Data MongoDB (Part 4)

Review

In the previous section, we have introduced MongoDB Replica Sets, installed MongoDB, configured Replica Sets, and updated our Spring application. In this section, we will test our cluster to verify if we have achieved our goal.


Testing

Start Spring App

Kindly start our Spring application from Server 1. You may need to deploy the application first (it's up to you whether to deploy it on the cloud or on a localhost machine).

Notice the log output from Server 1. It acknowledges two connections from our Spring application, and it starts allocating space for the spring_mongodb_tutorial database:
Sat Feb 18 11:18:50 [conn14] getmore local.oplog.rs query: { ts: { $gte: new Date(5710309296642719745) } } cursorid:7697632718916849147 reslen:20 4588ms
Sat Feb 18 11:18:51 [initandlisten] connection accepted from 123.456.78.90:58023 #27
Sat Feb 18 11:18:53 [initandlisten] connection accepted from 123.456.78.90:58027 #28
Sat Feb 18 11:18:53 [conn28] CMD: drop spring_mongodb_tutorial.role
Sat Feb 18 11:18:53 [conn28] CMD: drop spring_mongodb_tutorial.user
Sat Feb 18 11:18:53 [FileAllocator] allocating new datafile /data/db/spring_mongodb_tutorial.ns, filling with zeroes...
Sat Feb 18 11:18:54 [FileAllocator] done allocating datafile /data/db/spring_mongodb_tutorial.ns, size: 16MB, took 0.081 secs
Sat Feb 18 11:18:54 [FileAllocator] allocating new datafile /data/db/spring_mongodb_tutorial.0, filling with zeroes...
Sat Feb 18 11:18:56 [FileAllocator] done allocating datafile /data/db/spring_mongodb_tutorial.0, size: 16MB, took 1.534 secs
Sat Feb 18 11:18:56 [FileAllocator] allocating new datafile /data/db/spring_mongodb_tutorial.1, filling with zeroes...
Sat Feb 18 11:18:57 [FileAllocator] done allocating datafile /data/db/spring_mongodb_tutorial.1, size: 32MB, took 1.27 secs
Sat Feb 18 11:18:57 [conn28] build index spring_mongodb_tutorial.user { _id: 1 }
Sat Feb 18 11:18:57 [conn28] build index done 0 records 0.074 secs
Sat Feb 18 11:18:57 [conn28] insert spring_mongodb_tutorial.user 4131ms
Sat Feb 18 11:18:57 [conn28] build index spring_mongodb_tutorial.role { _id: 1 }
Sat Feb 18 11:18:57 [conn28] build index done 0 records 0 secs
Sat Feb 18 11:18:57 [conn14] getmore local.oplog.rs query: { ts: { $gte: new Date(5710309296642719745) } } cursorid:7697632718916849147 nreturned:4 reslen:1048 6948ms
Sat Feb 18 11:18:57 [conn13] getmore local.oplog.rs query: { ts: { $gte: new Date(5710309296642719745) } } cursorid:844650061822854635 nreturned:4 reslen:1048 5872ms

Watch how Server 2 synchronizes with Server 1
Sat Feb 18 11:18:51 [initandlisten] connection accepted from 123.456.78.90:43302 #20
Sat Feb 18 11:18:57 [FileAllocator] allocating new datafile /data/db/spring_mongodb_tutorial.ns, filling with zeroes...
Sat Feb 18 11:18:57 [FileAllocator] done allocating datafile /data/db/spring_mongodb_tutorial.ns, size: 16MB, took 0.042 secs
Sat Feb 18 11:18:57 [FileAllocator] allocating new datafile /data/db/spring_mongodb_tutorial.0, filling with zeroes...
Sat Feb 18 11:18:57 [FileAllocator] done allocating datafile /data/db/spring_mongodb_tutorial.0, size: 16MB, took 0.041 secs
Sat Feb 18 11:18:57 [rsSync] build index spring_mongodb_tutorial.user { _id: 1 }
Sat Feb 18 11:18:57 [rsSync] build index done 0 records 0 secs
Sat Feb 18 11:18:57 [rsSync] build index spring_mongodb_tutorial.role { _id: 1 }
Sat Feb 18 11:18:57 [rsSync] build index done 0 records 0 secs
Sat Feb 18 11:18:57 [FileAllocator] allocating new datafile /data/db/spring_mongodb_tutorial.1, filling with zeroes...
Sat Feb 18 11:18:58 [FileAllocator] done allocating datafile /data/db/spring_mongodb_tutorial.1, size: 32MB, took 0.078 secs

Also, observe how Server 3 synchronizes with Server 1
Sat Feb 18 11:18:51 [initandlisten] connection accepted from 123.456.78.90:52987 #20
Sat Feb 18 11:18:57 [FileAllocator] allocating new datafile /data/db/spring_mongodb_tutorial.ns, filling with zeroes...
Sat Feb 18 11:18:57 [FileAllocator] done allocating datafile /data/db/spring_mongodb_tutorial.ns, size: 16MB, took 0.043 secs
Sat Feb 18 11:18:57 [FileAllocator] allocating new datafile /data/db/spring_mongodb_tutorial.0, filling with zeroes...
Sat Feb 18 11:18:57 [FileAllocator] done allocating datafile /data/db/spring_mongodb_tutorial.0, size: 16MB, took 0.044 secs
Sat Feb 18 11:18:57 [rsSync] build index spring_mongodb_tutorial.user { _id: 1 }
Sat Feb 18 11:18:57 [rsSync] build index done 0 records 0 secs
Sat Feb 18 11:18:57 [rsSync] build index spring_mongodb_tutorial.role { _id: 1 }
Sat Feb 18 11:18:57 [rsSync] build index done 0 records 0 secs
Sat Feb 18 11:18:57 [FileAllocator] allocating new datafile /data/db/spring_mongodb_tutorial.1, filling with zeroes...
Sat Feb 18 11:18:58 [FileAllocator] done allocating datafile /data/db/spring_mongodb_tutorial.1, size: 32MB, took 0.091 secs

Server 4 does not synchronize because it's an arbiter-only server! Take note of that.

Start Killing

I don't mean killing people but rather MongoDB servers. Let's add a new record first. I have chosen mary, Mary, Jane, zzzzzzz, Regular as the properties of the new record. Feel free to vary.


Now, let's kill Server 1
Sat Feb 18 11:21:02 got kill or ctrl c or hup signal 2 (Interrupt), will terminate after current cmd ends
Sat Feb 18 11:21:02 [conn14] getmore local.oplog.rs query: { ts: { $gte: new Date(5710309296642719745) } } cursorid:7697632718916849147 exception: interrupted at shutdown code:11600 reslen:20 2565ms
Sat Feb 18 11:21:02 [conn13] getmore local.oplog.rs query: { ts: { $gte: new Date(5710309296642719745) } } cursorid:844650061822854635 exception: interrupted at shutdown code:11600 reslen:20 2565ms
Sat Feb 18 11:21:02 [interruptThread] now exiting
Sat Feb 18 11:21:02 Sat Feb 18 11:21:02 [conn14] got request after shutdown()
Sat Feb 18 11:21:02 [conn13] got request after shutdown()
dbexit:
Sat Feb 18 11:21:02 [interruptThread] shutdown: going to close listening sockets...
Sat Feb 18 11:21:02 [interruptThread] closing listening socket: 5
Sat Feb 18 11:21:02 [interruptThread] closing listening socket: 6
Sat Feb 18 11:21:02 [interruptThread] closing listening socket: 8
Sat Feb 18 11:21:02 [interruptThread] removing socket file: /tmp/mongodb-27017.sock
Sat Feb 18 11:21:02 [interruptThread] shutdown: going to flush diaglog...
Sat Feb 18 11:21:02 [interruptThread] shutdown: going to close sockets...
Sat Feb 18 11:21:02 [interruptThread] shutdown: waiting for fs preallocator...
Sat Feb 18 11:21:02 [interruptThread] shutdown: closing all files...
Sat Feb 18 11:21:02 [interruptThread] closeAllFiles() finished
Sat Feb 18 11:21:02 [interruptThread] shutdown: removing fs lock...
Sat Feb 18 11:21:02 dbexit: really exiting now
Sat Feb 18 11:21:02 [conn1] end connection 127.0.0.1:39621
Logstream::get called in uninitialized state
Sat Feb 18 11:21:02 [conn38] end connection 173.204.91.83:37424

Reload our Spring application. Notice, we still have the same data. We lost nothing. The replicas are working!

Kill More

Let's kill more servers. Add new record again. I have chosen anna, Anna, Williams, zzzzzzz, Admin as the properties of the new record.


Kill Server #2. This means only Server 3 and Server 4 are running, one slave and one arbiter remains respectively. Refresh our Spring application. However the application fails to load the data. Why?

The log explains why:
Sat Feb 18 11:23:46 [rsSync] replSet syncThread: 10278 dbclient error communicating with server: 173.204.91.84:27017
Sat Feb 18 11:23:46 [conn42] end connection 123.456.78.92:53291
Sat Feb 18 11:23:47 [rsHealthPoll] couldn't connect to 123.456.78.90:27017: couldn't connect to server 173.204.91.90:27017
Sat Feb 18 11:23:48 [rsHealthPoll] DBClientCursor::init call() failed
Sat Feb 18 11:23:48 [rsHealthPoll] replSet info 123.456.78.92:27017 is down (or slow to respond): DBClientBase::findN: transport error: 123.456.78.92:27017 query: { replSetHeartbeat: "cluster1", v: 1, pv: 1, checkEmpty: false, from: "123.456.78.93:27017" }
Sat Feb 18 11:23:48 [rsHealthPoll] replSet member 123.456.78.92:27017 is now in state DOWN
Sat Feb 18 11:23:48 [rsMgr] replSet can't see a majority, will not try to elect self

It's because our servers can't elect a primary server. In general, you need two non-arbiter servers in order to have a primary server! The arbiter is designed for breaking the tie-break between equal votes. Now let's restore the other two dead servers. Run mongod again for in Server 1 and Server 2.

Refresh our Spring application again. Notice we have all four records, including Mary and Anna. Remember when we added Anna, the Server #1 was dead!

Let's kill Server 2 and see if Anna is still displayed. Yes, it's diplayed!

Let's kill Server 3 and see if Anna is still displayed. And run Server 3 again. Yes, it's still diplayed!

Conclusion

That's it! We've successfully implemented a MongoDB Replica Set. We have shown step-by-step how to configure our servers and our Spring application. We've also made some quick tests to verify the replication feature of our servers.
Continue reading MongoDB - Replica Sets with Spring Data MongoDB (Part 4)
, , , , , , , ,

MongoDB - Replica Sets with Spring Data MongoDB (Part 3)

Review

In the previous section, we have installed and configured our servers for MongoDB replication. In this section, we will update our Spring application to support MongoDB Replica Sets.


Spring App Configuration

We have setup a MongoDB Replica Set using four servers, and we've examined the output from the logs and verified that all instances are running. Our next step is to configure our Spring MongoDB-base application.

Since we're reusing our application from the tutorial Spring MVC 3.1 - Implement CRUD with Spring Data MongoDB, adding a MongoDB Replica Set support in Spring is trivial. All we need to do is modify the spring.properties file with the following contents (this file is located under the WEB-INF directory):



Then we have to modify as well the spring-data.xml file with the following updates:



That's all we need to for our Spring application!

Next

In the next section, we will run and test our servers to verify and test our MongoDB cluster for replication. Click here to proceed.
Continue reading MongoDB - Replica Sets with Spring Data MongoDB (Part 3)
, , , , ,

Spring 3.1.0.RC1 - JPA EntityManagerFactory bootstrapping without persistence.xml

This will be a quick post. Today Spring 3.1.0.RC1 has been released. See here. This article will take a quick look on one of the enhancements in Spring 3.1:

3.1.12 JPA EntityManagerFactory bootstrapping without persistence.xml
In standard JPA, persistence units get defined through META-INF/persistence.xml files in specific jar files which will in turn get searched for @Entity classes. In many cases, persistence.xml does not contain more than a unit name and relies on defaults and/or external setup for all other concerns (such as the DataSource to use, etc). For that reason, Spring 3.1 provides an alternative: LocalContainerEntityManagerFactoryBean accepts a 'packagesToScan' property, specifying base packages to scan for @Entity classes. This is analogous to AnnotationSessionFactoryBean's property of the same name for native Hibernate setup, and also to Spring's component-scan feature for regular Spring beans. Effectively, this allows for XML-free JPA setup at the mere expense of specifying a base package for entity scanning: a particularly fine match for Spring applications which rely on component scanning for Spring beans as well, possibly even bootstrapped using a code-based Servlet 3.0 initializer.

Source: New Features and Enhancements in Spring 3.1

Here's my existing configuration:


This requires an extra META-INF/persistence.xml to make it work:


How do we improve this?

Based on the docs, we can remove the persistence.xml altogether. But how do we declare the extra configurations related to our ORM? And how does the entityManagerFactory know where our entities are?

Here's how:
1. Delete the META-INF/persistence.xml

2. Declare a packagesToScan property

3. Declare a jpaPropertyMap

Here's the final configuration:


If you want to try this with an actual project, you either create a new one, or you can play with my demo project Spring MVC: Integrating MySQL, MongoDB, RabbitMQ, and AJAX which has a Github repo.

That's all folks!
Continue reading Spring 3.1.0.RC1 - JPA EntityManagerFactory bootstrapping without persistence.xml
, , , , , , , , , ,

Spring - Hibernate: One-To-Many Association - Explicitly Specify Join Table, Cascade, and Fetch

Introduction

In this tutorial we'll expand our previous project Spring - Hibernate: One-To-Many Association by explicitly specifying the join table and join columns. We will also declare the Cascade type and Fetch strategy for the @OneToMany annotation.

Spring MVC 3 and Hibernate Tutorials Series
Spring - Hibernate: Many-To-One Association - Explicitly Specify Join Table, Cascade, and Fetch
Spring - Hibernate: One-To-Many Association - Explicitly Specify Join Table, Cascade, and Fetch
Spring - Hibernate: Many-To-One Association
Spring - Hibernate: One-To-Many Association
Spring MVC 3, Hibernate Annotations, MySQL Integration Tutorial
Spring MVC 3, Hibernate Annotations, HSQLDB Integration Tutorial

Changes

Using a diff tool, we can easily visualize the changes from the previous project and our updated project. Notice only Person.java, CreditCardService.java, and PersonService.java have changed. Everything else have remained the same.


Development

Similar with the original project, we'll split our development in three layers: Domain, Service, and Controller.

Domain Layer

For the domain layer, only Person class has changed by adding extra properties to the @OneToMany annotation.

Person.java
package org.krams.tutorial.domain;

import java.io.Serializable;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.OneToMany;
import javax.persistence.Table;

/**
* Represents a person entity
*
* @author Krams at {@link http://krams915@blogspot.com}
*/
@Entity
@Table(name = "PERSON")
public class Person implements Serializable {

private static final long serialVersionUID = -5527566248002296042L;

@Id
@Column(name = "ID")
@GeneratedValue
private Integer id;

@Column(name = "FIRST_NAME")
private String firstName;

@Column(name = "LAST_NAME")
private String lastName;

@Column(name = "MONEY")
private Double money;

@OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
@JoinTable(
name="PersonCreditCards",
joinColumns = @JoinColumn( name="PERSON_ID"),
inverseJoinColumns = @JoinColumn( name="CREDIT_ID")
)
private Set<CreditCard> creditCards;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

public Double getMoney() {
return money;
}

public void setMoney(Double money) {
this.money = money;
}

public Set<CreditCard> getCreditCards() {
return creditCards;
}

public void setCreditCards(Set<CreditCard> creditCards) {
this.creditCards = creditCards;
}
}

Pay extra attention to the @OneToMany annotation:

@Entity
@Table(name = "PERSON")
public class Person implements Serializable {
...
@OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
@JoinTable(
name="PersonCreditCards",
joinColumns = @JoinColumn( name="PERSON_ID"),
inverseJoinColumns = @JoinColumn( name="CREDIT_ID")
)
private Set<CreditCard> creditCards;
...
}
We have explicitly declared the join table name and the join column names.

Using phpymyadmin's database designer, the Hibernate auto-generated relationship between Person and CreditCard looks as follows:


Again using phpymyadmin, the auto-generated tables looks as follows:


Notice we have also specified the Cascade and Fetch strategies as well.

What does CascadeType.ALL do?
CascadeType.PERSIST: cascades the persist (create) operation to associated entities persist() is called or if the entity is managed
CascadeType.MERGE: cascades the merge operation to associated entities if merge() is called or if the entity is managed
CascadeType.REMOVE: cascades the remove operation to associated entities if delete() is called
CascadeType.REFRESH: cascades the refresh operation to associated entities if refresh() is called
CascadeType.DETACH: cascades the detach operation to associated entities if detach() is called

CascadeType.ALL: all of the above

Source: Hibernate Annotations Reference Guide

What does FetchType.EAGER do?
You have the ability to either eagerly or lazily fetch associated entities. The fetch parameter can be set to FetchType.LAZY or FetchType.EAGER. EAGER will try to use an outer join select to retrieve the associated object, while LAZY will only trigger a select when the associated object is accessed for the first time. @OneToMany and @ManyToMany associations are defaulted to LAZY and @OneToOne and @ManyToOne are defaulted to EAGER.

Source: Hibernate Annotations Reference Guide

Notice @OneToMany are defaulted to LAZY.

Service Layer

Specifying the Cascade and Fetch strategies with @OneToMany annotation requires us to change our service classes to take advantage of those settings.

PersonService.java
package org.krams.tutorial.service;

import java.util.List;
import java.util.Set;

import javax.annotation.Resource;

import org.apache.log4j.Logger;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.krams.tutorial.domain.CreditCard;
import org.krams.tutorial.domain.Person;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
* Service for processing Persons
*
* @author Krams at {@link http://krams915@blogspot.com
*/
@Service("personService")
@Transactional
public class PersonService {

protected static Logger logger = Logger.getLogger("service");

@Resource(name="sessionFactory")
private SessionFactory sessionFactory;

/**
* Retrieves all persons
*
* @return a list of persons
*/
public List<Person> getAll() {
logger.debug("Retrieving all persons");

// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Create a Hibernate query (HQL)
Query query = session.createQuery("FROM Person");

// Retrieve all
return query.list();
}

/**
* Retrieves a single person
*/
public Person get( Integer id ) {
// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Retrieve existing person
return (Person) session.get(Person.class, id);
}

/**
* Adds a new person
*/
public void add(Person person) {
logger.debug("Adding new person");

// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Persists to db
session.save(person);
}

/**
* Deletes an existing person
* @param id the id of the existing person
*/
public void delete(Integer id) {
logger.debug("Deleting existing person");

// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Retrieve record
Person person = (Person) session.get(Person.class, id);

// Delete person
session.delete(person);
}

/**
* Edits an existing person
*/
public void edit(Person person) {
logger.debug("Editing existing person");

// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Retrieve existing person via id
Person existingPerson = (Person) session.get(Person.class, person.getId());

// Assign updated values to this person
existingPerson.setFirstName(person.getFirstName());
existingPerson.setLastName(person.getLastName());
existingPerson.setMoney(person.getMoney());

// Save updates
session.save(existingPerson);
}
}

What exactly has changed from the original implementation to our new code? Here are the changes:

Get Method
old code
public Person get( Integer id ) {
// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Retrieve existing person
// Create a Hibernate query (HQL)
Query query = session.createQuery("FROM Person as p LEFT JOIN FETCH p.creditCards WHERE p.id="+id);

return (Person) query.uniqueResult();
}

new code
public Person get( Integer id ) {
// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Retrieve existing person
return (Person) session.get(Person.class, id);
}

Delete Method
old code
public void delete(Integer id) {
logger.debug("Deleting existing person");

// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Create a Hibernate query (HQL)
Query query = session.createQuery("FROM Person as p LEFT JOIN FETCH p.creditCards WHERE p.id="+id);

// Retrieve record
Person person = (Person) query.uniqueResult();

Set<:CreditCard> creditCards =person.getCreditCards();

// Delete person
session.delete(person);

// Delete associated credit cards
for (CreditCard creditCard: creditCards) {
session.delete(creditCard);
}
}

new code
public void delete(Integer id) {
logger.debug("Deleting existing person");

// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Retrieve record
Person person = (Person) session.get(Person.class, id);

// Delete person
session.delete(person);
}

CreditCardService.java
package org.krams.tutorial.service;

import java.util.ArrayList;
import java.util.List;
import javax.annotation.Resource;

import org.apache.log4j.Logger;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.krams.tutorial.domain.CreditCard;
import org.krams.tutorial.domain.Person;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
* Service for processing Credit Cards
*
* @author Krams at {@link http://krams915@blogspot.com
*/
@Service("creditCardService")
@Transactional
public class CreditCardService {

protected static Logger logger = Logger.getLogger("service");

@Resource(name="sessionFactory")
private SessionFactory sessionFactory;

/**
* Retrieves all credit cards
*/
public List<CreditCard> getAll(Integer personId) {
logger.debug("Retrieving all credit cards");

// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Create a Hibernate query (HQL)
Query query = session.createQuery("FROM Person as p WHERE p.id="+personId);

Person person = (Person) query.uniqueResult();

// Retrieve all
return new ArrayList<CreditCard>(person.getCreditCards());
}

/**
* Retrieves all credit cards
*/
public List<CreditCard> getAll() {
logger.debug("Retrieving all credit cards");

// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Create a Hibernate query (HQL)
Query query = session.createQuery("FROM CreditCard");

// Retrieve all
return query.list();
}

/**
* Retrieves a single credit card
*/
public CreditCard get( Integer id ) {
// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Retrieve existing credit card
CreditCard creditCard = (CreditCard) session.get(CreditCard.class, id);

// Persists to db
return creditCard;
}

/**
* Adds a new credit card
*/
public void add(Integer personId, CreditCard creditCard) {
logger.debug("Adding new credit card");

// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Persists to db
session.save(creditCard);

// Add to person as well
// Retrieve existing person via id
Person existingPerson = (Person) session.get(Person.class, personId);

// Assign updated values to this person
existingPerson.getCreditCards().add(creditCard);

// Save updates
session.save(existingPerson);
}

/**
* Deletes an existing credit card
*/
public void delete(Integer id) {
logger.debug("Deleting existing credit card");

// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Delete reference to foreign key credit card first
// We need a SQL query instead of HQL query here to access the third table
Query query = session.createSQLQuery("DELETE FROM PersonCreditCards " +
"WHERE CREDIT_ID="+id);

query.executeUpdate();

// Retrieve existing credit card
CreditCard creditCard = (CreditCard) session.get(CreditCard.class, id);

// Delete
session.delete(creditCard);
}

/**
* Edits an existing credit card
*/
public void edit(CreditCard creditCard) {
logger.debug("Editing existing creditCard");

// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Retrieve existing credit card via id
CreditCard existingCreditCard = (CreditCard) session.get(CreditCard.class, creditCard.getId());

// Assign updated values to this credit card
existingCreditCard.setNumber(creditCard.getNumber());
existingCreditCard.setType(creditCard.getType());

// Save updates
session.save(existingCreditCard);
}
}

What exactly has changed from the original implementation to our new code? Here are the changes:

Fetch Query
Since we have specified a Fetch strategy, we can now remove the LEFT JOIN FETCH from the HQL query.

old code
Query query = session.createQuery("FROM Person as p LEFT JOIN FETCH  p.creditCards WHERE p.id="+personId);

new code
Query query = session.createQuery("FROM Person as p WHERE p.id="+personId);

When we specified FetchType.EAGER we're letting Hibernate load the associated records automatically:
@OneToMany(fetch=FetchType.EAGER)

Delete Query
Since we have explicitly specified the join table name and join column names, we have to adjust our SQL query.

old code
Query query = session.createSQLQuery("DELETE FROM PERSON_CREDIT_CARD " +
"WHERE creditCards_ID="+id);

new code
Query query = session.createSQLQuery("DELETE FROM PersonCreditCards " +
"WHERE CREDIT_ID="+id);

Configuration

We've completed the changes necessary for the project. The next step is to declare the configuration files. But since nothing has changed in the configuration files, we won't be posting them again here. Please see the original tutorial Spring - Hibernate: One-To-Many Association if you want to see the files.

Run the Application


Setup the database

Our application uses MySQL as its database. To run the application, make sure to setup the database first.

To create the database, follow these steps.
1. Open phpmyadmin (or any tool you prefer with)
2. Create a new database named mydatabase
3. Run the application to automatically create the database schema.

To populate the database with sample data, import the mydatabase.sql SQL script which is located under the WEB-INF folder of the application:


Access the main application

To access the main application, use the following URL:
http://localhost:8080/spring-hibernate-one-to-many-jointable/krams/main/record/list

You should see the following application:


Conclusion

That's it. We've successfully expanded our previous project Spring - Hibernate: One-To-Many Association by explicitly specifying the join table and join columns. We also declared the Cascade and Fetch strategies explicitly.

Download the project
You can access the project site at Google's Project Hosting at http://code.google.com/p/spring-mvc-hibernate-annotations-integration-tutorial/

You can download the project as a Maven build. Look for the spring-hibernate-one-to-many-jointable.zip in the Download sections.

You can run the project directly using an embedded server via Maven.
For Tomcat: mvn tomcat:run
For Jetty: mvn jetty:run

If you want to learn more about Spring MVC and integration with other technologies, feel free to read my other tutorials in the Tutorials section.
Continue reading Spring - Hibernate: One-To-Many Association - Explicitly Specify Join Table, Cascade, and Fetch
, , , , , , , , , ,

Spring - Hibernate: One-To-Many Association

Introduction

In this tutorial we'll explore how to implement a one-to-many association using Hibernate Annotations and Spring MVC 3. We will use @OneToMany annotation to specify the relationship between our objects. We will not specify Cascade types or Fetch strategies, but instead rely on @OneToMany's default settings.

If you want to see how to implement this project using @ManyToOne annotation instead, see Spring - Hibernate: Many-To-One Association.

Spring MVC 3 and Hibernate Tutorials Series
Spring - Hibernate: Many-To-One Association - Explicitly Specify Join Table, Cascade, and Fetch
Spring - Hibernate: One-To-Many Association - Explicitly Specify Join Table, Cascade, and Fetch
Spring - Hibernate: Many-To-One Association
Spring - Hibernate: One-To-Many Association
Spring MVC 3, Hibernate Annotations, MySQL Integration Tutorial
Spring MVC 3, Hibernate Annotations, HSQLDB Integration Tutorial

What is One-To-Many Association?
One-to-many relationships occur when each record in TableA may have many linked records in TableB but each record in TableB may have only one corresponding record in TableA.

Source: http://databases.about.com/od/specificproducts/g/onetomany.htm

Specification

Our application is simple CRUD system for managing a list of records. Each record corresponds to a single person containing personal information and credit card details. Each person can own zero or more credit cards. Moreover, a person's person information is edited separately from his credit card details.

Below are sample screenshots from the application:




Domain Objects

Based on the specs, we have two domain objects: Person and Credit Card.

A Person object has the following properties:
  • id
  • first name
  • last name
  • money
  • credit cards

Whereas a Credit Card object has the following properties:
  • id
  • type
  • number

Notice each Person can have multiple Credit Cards hence, the one-to-many relationship. We can of course look at this relationship the other way around: many-to-one relationship. But that topic is for another tutorial.

Development

We'll split our development in three layers: Domain, Service, and Controller. Then we'll declare the configuration files. Let's start with the Domain layer.

Domain Layer

As discussed earlier, we have two domain objects: Person and Credit Card. Consequently, we'll declare two POJOs to represent our domain. Each POJO will be annotated with @Entity so that we can persist them to a database.

Person.java
package org.krams.tutorial.domain;

import java.io.Serializable;
import java.util.Set;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;

/**
* Represents a person entity
*
* @author Krams at {@link http://krams915@blogspot.com}
*/
@Entity
@Table(name = "PERSON")
public class Person implements Serializable {

private static final long serialVersionUID = -5527566248002296042L;

@Id
@Column(name = "ID")
@GeneratedValue
private Integer id;

@Column(name = "FIRST_NAME")
private String firstName;

@Column(name = "LAST_NAME")
private String lastName;

@Column(name = "MONEY")
private Double money;

@OneToMany
private Set<CreditCard> creditCards;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

public Double getMoney() {
return money;
}

public void setMoney(Double money) {
this.money = money;
}

public Set<CreditCard> getCreditCards() {
return creditCards;
}

public void setCreditCards(Set<CreditCard> creditCards) {
this.creditCards = creditCards;
}
}

Person object has been mapped to the PERSON table. The table that gets auto-generated looks as follows:


Notice the @OneToMany annotation for the field creditCards. We did not specify any Cascade type or Fetch strategy for this field. Instead, we rely on the annotation's default settings. Later we'll discover some issues for not specifying them.

@Entity
@Table(name = "PERSON")
public class Person implements Serializable {

...

@OneToMany
private Set<CreditCard> creditCards;

...
}

CreditCard.java
package org.krams.tutorial.domain;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

/**
* Represents a credit card entity
*
* @author Krams at {@link http://krams915@blogspot.com}
*/
@Entity
@Table(name = "CREDIT_CARD")
public class CreditCard implements Serializable {

private static final long serialVersionUID = 5924361831551833717L;

@Id
@Column(name = "ID")
@GeneratedValue
private Integer id;

@Column(name = "TYPE")
private String type;

@Column(name = "NUMBER")
private String number;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getType() {
return type;
}

public void setType(String type) {
this.type = type;
}

public String getNumber() {
return number;
}

public void setNumber(String number) {
this.number = number;
}
}

CreditCard object has been mapped to the CREDIT_CARD table. The table that gets auto-generated looks as follows:


Using phpymyadmin's database designer, the Hibernate auto-generated relationship between Person and CreditCard looks as follows:


Again using phpymyadmin, the auto-generated tables looks as follows:


We only declared two entities: a Person entity and a CreditCard entity. We expect two tables in the database; however, we have three tables in the database instead. Why? The default behavior of @OneToMany is it will create a third table acting as the join table.

Here's a quote from the Hibernate Annotations Reference guide:
Without describing any physical mapping, a unidirectional one to many with join table is used. The table name is the concatenation of the owner table name, _, and the other side table name. The foreign key name(s) referencing the owner table is the concatenation of the owner table, _, and the owner primary key column(s) name. The foreign key name(s) referencing the other side is the concatenation of the owner property name, _, and the other side primary key column(s) name. A unique constraint is added to the foreign key referencing the other side table to reflect the one to many.

The Hibernate Annotations Reference is stating the behavior in terms of the database structure. Later we'll discuss some issues when relying on the default settings, especially in the Service layer.

Service Layer

After declaring the Domain objects, we'll now proceed with the Service layer which contains two services: a PersonService and a CreditCardService.

PersonService
PersonService is responsible for handling CRUD operations for Person entities. Each method is eventually delegated to a Hibernate Session object.

PersonService.java
package org.krams.tutorial.service;

import java.util.List;
import java.util.Set;

import javax.annotation.Resource;

import org.apache.log4j.Logger;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.krams.tutorial.domain.CreditCard;
import org.krams.tutorial.domain.Person;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
* Service for processing Persons
*
* @author Krams at {@link http://krams915@blogspot.com
*/
@Service("personService")
@Transactional
public class PersonService {

protected static Logger logger = Logger.getLogger("service");

@Resource(name="sessionFactory")
private SessionFactory sessionFactory;

/**
* Retrieves all persons
*
* @return a list of persons
*/
public List<Person> getAll() {
logger.debug("Retrieving all persons");

// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Create a Hibernate query (HQL)
Query query = session.createQuery("FROM Person");

// Retrieve all
return query.list();
}

/**
* Retrieves a single person
*/
public Person get( Integer id ) {
// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Retrieve existing person
// Create a Hibernate query (HQL)
Query query = session.createQuery("FROM Person as p LEFT JOIN FETCH p.creditCards WHERE p.id="+id);

return (Person) query.uniqueResult();
}

/**
* Adds a new person
*/
public void add(Person person) {
logger.debug("Adding new person");

// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Persists to db
session.save(person);
}

/**
* Deletes an existing person
* @param id the id of the existing person
*/
public void delete(Integer id) {
logger.debug("Deleting existing person");

// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Create a Hibernate query (HQL)
Query query = session.createQuery("FROM Person as p LEFT JOIN FETCH p.creditCards WHERE p.id="+id);

// Retrieve record
Person person = (Person) query.uniqueResult();

Set<CreditCard> creditCards =person.getCreditCards();

// Delete person
session.delete(person);

// Delete associated credit cards
for (CreditCard creditCard: creditCards) {
session.delete(creditCard);
}
}

/**
* Edits an existing person
*/
public void edit(Person person) {
logger.debug("Editing existing person");

// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Retrieve existing person via id
Person existingPerson = (Person) session.get(Person.class, person.getId());

// Assign updated values to this person
existingPerson.setFirstName(person.getFirstName());
existingPerson.setLastName(person.getLastName());
existingPerson.setMoney(person.getMoney());

// Save updates
session.save(existingPerson);
}
}

PersonService deceptively looks simple; however it solves two main issues that deserves special attention:

Issue #1: Fetch Strategy
Retrieving a Person record (owner) doesn't load its associated CreditCard records (owned entities).

The following query retrieves a Person based on its id.
Query query = session.createQuery("FROM Person WHERE p.id="+id);

The problem with the previous query is it doesn't load the associated CreditCards. This is because we did not specify a Fetch strategy when we declared the @OneToMany annotation. To fix that we use the following query instead:

Query query = session.createQuery("FROM Person as p LEFT JOIN FETCH  p.creditCards WHERE p.id="+id);

Issue #2: Cascade Type
Deleting a Person record doesn't delete its associated CreditCard records.

The following query deletes a Person record:
session.delete(person);

The problem with the previous query is it doesn't delete the associated CreditCards. This is because we did not specify a Cascade type when we declared the @OneToMany annotation. That means we have to implement our own strategy for deleting the CreditCard records.

To resolve the issue we first make a FETCH query. Retrieve the CreditCard objects and assigned them to a temporary object. We then delete the Person record. Afterwards, we loop each credit card record, and delete them one by one.

// Create a Hibernate query (HQL)
Query query = session.createQuery("FROM Person as p LEFT JOIN FETCH p.creditCards WHERE p.id="+id);

// Retrieve record
Person person = (Person) query.uniqueResult();

Set<CreditCard> creditCards =person.getCreditCards();

// Delete person
session.delete(person);

// Delete associated credit cards
for (CreditCard creditCard: creditCards) {
session.delete(creditCard);
}

CreditCardService
CreditCardService is responsible for handling CRUD operations for CreditCard entities. Each method is eventually delegated to a Hibernate Session object.

CreditCardService.java
package org.krams.tutorial.service;

import java.util.ArrayList;
import java.util.List;
import javax.annotation.Resource;

import org.apache.log4j.Logger;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.krams.tutorial.domain.CreditCard;
import org.krams.tutorial.domain.Person;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
* Service for processing Credit Cards
*
* @author Krams at {@link http://krams915@blogspot.com
*/
@Service("creditCardService")
@Transactional
public class CreditCardService {

protected static Logger logger = Logger.getLogger("service");

@Resource(name="sessionFactory")
private SessionFactory sessionFactory;

/**
* Retrieves all credit cards
*/
public List<CreditCard> getAll(Integer personId) {
logger.debug("Retrieving all credit cards");

// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Create a Hibernate query (HQL)
Query query = session.createQuery("FROM Person as p LEFT JOIN FETCH p.creditCards WHERE p.id="+personId);

Person person = (Person) query.uniqueResult();

// Retrieve all
return new ArrayList<CreditCard>(person.getCreditCards());
}

/**
* Retrieves all credit cards
*/
public List<CreditCard> getAll() {
logger.debug("Retrieving all credit cards");

// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Create a Hibernate query (HQL)
Query query = session.createQuery("FROM CreditCard");

// Retrieve all
return query.list();
}

/**
* Retrieves a single credit card
*/
public CreditCard get( Integer id ) {
// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Retrieve existing credit card
CreditCard creditCard = (CreditCard) session.get(CreditCard.class, id);

// Persists to db
return creditCard;
}

/**
* Adds a new credit card
*/
public void add(Integer personId, CreditCard creditCard) {
logger.debug("Adding new credit card");

// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Persists to db
session.save(creditCard);

// Add to person as well
// Retrieve existing person via id
Person existingPerson = (Person) session.get(Person.class, personId);

// Assign updated values to this person
existingPerson.getCreditCards().add(creditCard);

// Save updates
session.save(existingPerson);
}

/**
* Deletes an existing credit card
*/
public void delete(Integer id) {
logger.debug("Deleting existing credit card");

// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Delete reference to foreign key credit card first
// We need a SQL query instead of HQL query here to access the third table
Query query = session.createSQLQuery("DELETE FROM PERSON_CREDIT_CARD " +
"WHERE creditCards_ID="+id);

query.executeUpdate();

// Retrieve existing credit card
CreditCard creditCard = (CreditCard) session.get(CreditCard.class, id);

// Delete
session.delete(creditCard);
}

/**
* Edits an existing credit card
*/
public void edit(CreditCard creditCard) {
logger.debug("Editing existing creditCard");

// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Retrieve existing credit card via id
CreditCard existingCreditCard = (CreditCard) session.get(CreditCard.class, creditCard.getId());

// Assign updated values to this credit card
existingCreditCard.setNumber(creditCard.getNumber());
existingCreditCard.setType(creditCard.getType());

// Save updates
session.save(existingCreditCard);
}
}

CreditCardService's delete() method deserves extra attention:

public void delete(Integer id) {
logger.debug("Deleting existing credit card");

// Retrieve session from Hibernate
Session session = sessionFactory.getCurrentSession();

// Delete reference to foreign key credit card first
// We need a SQL query instead of HQL query here to access the third table
Query query = session.createSQLQuery("DELETE FROM PERSON_CREDIT_CARD " +
"WHERE creditCards_ID="+id);

query.executeUpdate();

// Retrieve existing credit card
CreditCard creditCard = (CreditCard) session.get(CreditCard.class, id);

// Delete
session.delete(creditCard);
}
To delete a CreditCard entity, we have to delete it first from the PERSON_CREDIT_CARD table using the following SQL query:
Query query = session.createSQLQuery("DELETE FROM PERSON_CREDIT_CARD " +
"WHERE creditCards_ID="+id);
Notice PERSON_CREDIT_CARD is the join table created by Hibernate and the query is a SQL not HQL.

After running the SQL statement, we delete the CreditCard entity from the CREDIT_CARD table using
session.delete(creditCard)
.

Why do we need to undergo such process? Can't we just run session.delete(creditCard) directly? We can't because there's a foreign key constraint under PERSON_CREDIT_CARD join table.

Controller Layer

After declaring the Domain and Service objects, we'll now proceed with the Controller layer.
We'll declare two controllers: MainController and CreditCardController.

MainController
MainController is responsible for handling request for Person records. Each CRUD request is ultimately delegated to the PersonService and then returns an appropriate JSP page.

MainController.java
package org.krams.tutorial.controller;

import java.util.ArrayList;
import java.util.List;

import javax.annotation.Resource;

import org.apache.log4j.Logger;
import org.krams.tutorial.domain.Person;
import org.krams.tutorial.dto.PersonDTO;
import org.krams.tutorial.service.CreditCardService;
import org.krams.tutorial.service.PersonService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

/**
* Handles person request
*
* @author Krams at {@link http://krams915@blogspot.com
*/
@Controller
@RequestMapping("/main/record")
public class MainController {

protected static Logger logger = Logger.getLogger("controller");

@Resource(name="personService")
private PersonService personService;

@Resource(name="creditCardService")
private CreditCardService creditCardService;

/**
* Retrieves the "Records" page
*/
@RequestMapping(value = "/list", method = RequestMethod.GET)
public String getRecords(Model model) {
logger.debug("Received request to show records page");

// Retrieve all persons
List<Person> persons = personService.getAll();

// Prepare model object
List<PersonDTO> personsDTO = new ArrayList<PersonDTO>();

for (Person person: persons) {
// Create new data transfer object
PersonDTO dto = new PersonDTO();

dto.setId(person.getId());
dto.setFirstName(person.getFirstName());
dto.setLastName(person.getLastName());
dto.setMoney(person.getMoney());
dto.setCreditCards(creditCardService.getAll(person.getId()));

// Add to model list
personsDTO.add(dto);
}

// Add to model
model.addAttribute("persons", personsDTO);

// This will resolve to /WEB-INF/jsp/records.jsp
return "records";
}

/**
* Retrieves the "Add New Record" page
*/
@RequestMapping(value = "/add", method = RequestMethod.GET)
public String getAdd(Model model) {
logger.debug("Received request to show add page");

// Create new Person and add to model
model.addAttribute("personAttribute", new Person());

// This will resolve to /WEB-INF/jsp/add-record.jsp
return "add-record";
}

/**
* Adds a new record
*/
@RequestMapping(value = "/add", method = RequestMethod.POST)
public String postAdd(@ModelAttribute("personAttribute") Person person) {
logger.debug("Received request to add new record");

// Delegate to service
personService.add(person);

// Redirect to url
return "redirect:/krams/main/record/list";
}

/**
* Deletes a record including all the associated credit cards
*/
@RequestMapping(value = "/delete", method = RequestMethod.GET)
public String getDelete(@RequestParam("id") Integer personId) {
logger.debug("Received request to delete record");

// Delete person
personService.delete(personId);

// Redirect to url
return "redirect:/krams/main/record/list";
}

/**
* Retrieves the "Edit Existing Record" page
*/
@RequestMapping(value = "/edit", method = RequestMethod.GET)
public String getEdit(@RequestParam("id") Integer personId, Model model) {
logger.debug("Received request to show edit page");

// Retrieve person by id
Person existingPerson = personService.get(personId);

// Add to model
model.addAttribute("personAttribute", existingPerson);

// This will resolve to /WEB-INF/jsp/edit-record.jsp
return "edit-record";
}

/**
* Edits an existing record
*/
@RequestMapping(value = "/edit", method = RequestMethod.POST)
public String postEdit(@RequestParam("id") Integer personId,
@ModelAttribute("personAttribute") Person person) {
logger.debug("Received request to edit existing person");

// Assign id
person.setId(personId);

// Delegate to service
personService.edit(person);

// Redirect to url
return "redirect:/krams/main/record/list";
}

}

Notice the getRecords() method maps the Person and CreditCard records to a data transfer object: PersonDTO

/**
* Retrieves the "Records" page
*/
@RequestMapping(value = "/list", method = RequestMethod.GET)
public String getRecords(Model model) {
logger.debug("Received request to show records page");

// Retrieve all persons
List<Person> persons = personService.getAll();

// Prepare model object
List<PersonDTO> personsDTO = new ArrayList<PersonDTO>();

for (Person person: persons) {
// Create new data transfer object
PersonDTO dto = new PersonDTO();

dto.setId(person.getId());
dto.setFirstName(person.getFirstName());
dto.setLastName(person.getLastName());
dto.setMoney(person.getMoney());
dto.setCreditCards(creditCardService.getAll(person.getId()));

// Add to model list
personsDTO.add(dto);
}

// Add to model
model.addAttribute("persons", personsDTO);

// This will resolve to /WEB-INF/jsp/records.jsp
return "records";
}

PersonDTO acts as a data model for the records.jsp view.

PersonDTO.java
package org.krams.tutorial.dto;

import java.util.List;

import org.krams.tutorial.domain.CreditCard;

/**
* Data Transfer Object for displaying purposes
*/
public class PersonDTO {

private Integer id;
private String firstName;
private String lastName;
private Double money;
private List<CreditCard> creditCards;

public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public Double getMoney() {
return money;
}
public void setMoney(Double money) {
this.money = money;
}
public List<CreditCard> getCreditCards() {
return creditCards;
}
public void setCreditCards(List<CreditCard> creditCards) {
this.creditCards = creditCards;
}
}

CreditCardController
CreditCardController is responsible for handling request for credit card records. We won't be using all methods available in this controller. They had been added for the sake of completeness.

CreditCardController.java
package org.krams.tutorial.controller;

import javax.annotation.Resource;

import org.apache.log4j.Logger;
import org.krams.tutorial.domain.CreditCard;
import org.krams.tutorial.service.CreditCardService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

/**
* Handles credit card requests
*
* @author Krams at {@link http://krams915@blogspot.com
*/
@Controller
@RequestMapping("/main/creditcard")
public class CreditCardController {

protected static Logger logger = Logger.getLogger("controller");

@Resource(name="creditCardService")
private CreditCardService creditCardService;

/**
* Retrieves the "Add New Credit Card" page
*/
@RequestMapping(value = "/add", method = RequestMethod.GET)
public String getAdd(@RequestParam("id") Integer personId, Model model) {
logger.debug("Received request to show add page");

// Prepare model object
CreditCard creditCard = new CreditCard();

// Add to model
model.addAttribute("personId", personId);
model.addAttribute("creditCardAttribute", creditCard);

// This will resolve to /WEB-INF/jsp/add-credit-card.jsp
return "add-credit-card";
}

/**
* Adds a new credit card
*/
@RequestMapping(value = "/add", method = RequestMethod.POST)
public String postAdd(@RequestParam("id") Integer personId,
@ModelAttribute("creditCardAttribute") CreditCard creditCard) {
logger.debug("Received request to add new credit card");

// Delegate to service
creditCardService.add(personId, creditCard);

// Redirect to url
return "redirect:/krams/main/record/list";
}


/**
* Deletes a credit card
*/
@RequestMapping(value = "/delete", method = RequestMethod.GET)
public String getDelete(@RequestParam("id") Integer creditCardId) {
logger.debug("Received request to delete credit card");


// Delegate to service
creditCardService.delete(creditCardId);

// Redirect to url
return "redirect:/krams/main/record/list";
}

/**
* Retrieves the "Edit Existing Credit Card" page
*/
@RequestMapping(value = "/edit", method = RequestMethod.GET)
public String getEdit(@RequestParam("pid") Integer personId,
@RequestParam("cid") Integer creditCardId, Model model) {
logger.debug("Received request to show edit page");

// Retrieve credit card by id
CreditCard existingCreditCard = creditCardService.get(creditCardId);

// Add to model
model.addAttribute("personId", personId);
model.addAttribute("creditCardAttribute", existingCreditCard);

// This will resolve to /WEB-INF/jsp/edit-credit-card.jsp
return "edit-credit-card";
}

/**
* Edits an existing credit card
*/
@RequestMapping(value = "/edit", method = RequestMethod.POST)
public String postEdit(@RequestParam("id") Integer creditCardId,
@ModelAttribute("creditCardAttribute") CreditCard creditCard) {
logger.debug("Received request to add new credit card");

// Assign id
creditCard.setId(creditCardId);

// Delegate to service
creditCardService.edit(creditCard);

// Redirect to url
return "redirect:/krams/main/record/list";
}
}

View Layer

After discussing the Domain, Service, and Controller layers, we'll now turn our focus to the View layer. Our views are primarily composed of JSP pages. Here are the JSPs:

Show all records


records.jsp
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>Records</h1>

<c:url var="editImgUrl" value="/resources/img/edit.png" />
<c:url var="deleteImgUrl" value="/resources/img/delete.png" />
<c:url var="addUrl" value="/krams/main/record/add" />
<table style="border: 1px solid; width: 100%; text-align:center">
<thead style="background:#d3dce3">
<tr>
<th>Id</th>
<th>First Name</th>
<th>Last Name</th>
<th>Money</th>
<th colspan="2"></th>
<th>CC Type</th>
<th>CC Number</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody style="background:#ccc">
<c:forEach items="${persons}" var="person">
<c:url var="editUrl" value="/krams/main/record/edit?id=${person.id}" />
<c:url var="deleteUrl" value="/krams/main/record/delete?id=${person.id}" />

<c:if test="${!empty person.creditCards}">
<c:forEach items="${person.creditCards}" var="creditCard">
<tr>
<td><c:out value="${person.id}" /></td>
<td><c:out value="${person.firstName}" /></td>
<td><c:out value="${person.lastName}" /></td>
<td><c:out value="${person.money}" /></td>
<td><a href="${editUrl}"><img src="${editImgUrl}"></img></a></td>
<td><a href="${deleteUrl}"><img src="${deleteImgUrl}"></img></a></td>

<td><c:out value="${creditCard.type}" /></td>
<td><c:out value="${creditCard.number}" /></td>
<c:url var="addCcUrl" value="/krams/main/creditcard/add?id=${person.id}" />
<c:url var="editCcUrl" value="/krams/main/creditcard/edit?pid=${person.id}&cid=${creditCard.id}" />
<c:url var="deleteCcUrl" value="/krams/main/creditcard/delete?id=${creditCard.id}" />
<td><a href="${addCcUrl}">+</a></td>
<td><a href="${editCcUrl}"><img src="${editImgUrl}"></img></a></td>
<td><a href="${deleteCcUrl}"><img src="${deleteImgUrl}"></img></a></td>
</tr>
</c:forEach>
</c:if>

<c:if test="${empty person.creditCards}">
<tr>
<td><c:out value="${person.id}" /></td>
<td><c:out value="${person.firstName}" /></td>
<td><c:out value="${person.lastName}" /></td>
<td><c:out value="${person.money}" /></td>
<td><a href="${editUrl}"><img src="${editImgUrl}"></img></a></td>
<td><a href="${deleteUrl}"><img src="${deleteImgUrl}"></img></a></td>

<td>N/A</td>
<td>N/A</td>
<c:url var="addCcUrl" value="/krams/main/creditcard/add?id=${person.id}" />
<td><a href="${addCcUrl}">+</a></td>
<td></td>
<td></td>
</tr>
</c:if>

</c:forEach>
</tbody>
</table>

<c:if test="${empty persons}">
No records found.
</c:if>

<p><a href="${addUrl}">Create new record</a></p>

</body>
</html>

Add new record


add-record.jsp
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>

<h1>Create New Record</h1>

<c:url var="saveUrl" value="/krams/main/record/add" />
<form:form modelAttribute="personAttribute" method="POST" action="${saveUrl}">
<table>
<tr>
<td><form:label path="firstName">First Name:</form:label></td>
<td><form:input path="firstName"/></td>
</tr>

<tr>
<td><form:label path="lastName">Last Name</form:label></td>
<td><form:input path="lastName"/></td>
</tr>

<tr>
<td><form:label path="money">Money</form:label></td>
<td><form:input path="money"/></td>
</tr>
</table>

<input type="submit" value="Save" />
</form:form>

</body>
</html>

Edit existing record


edit-record.jsp
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>

<h1>Edit Existing Record</h1>

<c:url var="saveUrl" value="/krams/main/record/edit?id=${personAttribute.id}" />
<form:form modelAttribute="personAttribute" method="POST" action="${saveUrl}">
<table>
<tr>
<td><form:label path="id">Id:</form:label></td>
<td><form:input path="id" disabled="true"/></td>
</tr>

<tr>
<td><form:label path="firstName">First Name:</form:label></td>
<td><form:input path="firstName"/></td>
</tr>

<tr>
<td><form:label path="lastName">Last Name</form:label></td>
<td><form:input path="lastName"/></td>
</tr>

<tr>
<td><form:label path="money">Money</form:label></td>
<td><form:input path="money"/></td>
</tr>
</table>

<input type="submit" value="Save" />
</form:form>

</body>
</html>

Add new credit card


add-credit-card.jsp
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>

<h1>Add New Credit Card</h1>

<c:url var="saveUrl" value="/krams/main/creditcard/add?id=${personId}" />
<form:form modelAttribute="creditCardAttribute" method="POST" action="${saveUrl}">
<table>

<tr>
<td>Person Id:</td>
<td><input type="text" value="${personId}" disabled="true"/>
</tr>

<tr>
<td><form:label path="type">Type:</form:label></td>
<td><form:input path="type"/></td>
</tr>

<tr>
<td><form:label path="number">Number:</form:label></td>
<td><form:input path="number"/></td>
</tr>
</table>

<input type="submit" value="Save" />
</form:form>

</body>
</html>

Edit existing credit card


edit-credit-card.jsp
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>

<h1>Edit Existing Credit Card</h1>

<c:url var="saveUrl" value="/krams/main/creditcard/edit?id=${creditCardAttribute.id}" />
<form:form modelAttribute="creditCardAttribute" method="POST" action="${saveUrl}">
<table>

<tr>
<td>Person Id:</td>
<td><input type="text" value="${personId}" disabled="true"/>
</tr>

<tr>
<td><form:label path="type">Type:</form:label></td>
<td><form:input path="type"/></td>
</tr>

<tr>
<td><form:label path="number">Number:</form:label></td>
<td><form:input path="number"/></td>
</tr>
</table>

<input type="submit" value="Save" />
</form:form>

</body>
</html>

Configuration

We have completed the necessary Java classes. Our next step is to declare the necessary configuration files:

web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

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

<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/krams/*</url-pattern>
</servlet-mapping>

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

</web-app>

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

<!-- Declare a view resolver -->
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"
p:prefix="/WEB-INF/jsp/" p:suffix=".jsp" />

</beans>

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

<!-- Activates various annotations to be detected in bean classes -->
<context:annotation-config />

<!-- Scans the classpath for annotated components that will be auto-registered as Spring beans.
For example @Controller and @Service. Make sure to set the correct base-package-->
<context:component-scan base-package="org.krams.tutorial" />

<!-- Configures the annotation-driven Spring MVC Controller programming model.
Note that, with Spring 3.0, this tag works in Servlet MVC only! -->
<mvc:annotation-driven />

<!-- Load Hibernate related configuration -->
<import resource="hibernate-context.xml" />

</beans>

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

<context:property-placeholder location="/WEB-INF/spring.properties" />

<!-- Enable annotation style of managing transactions -->
<tx:annotation-driven transaction-manager="transactionManager" />

<!-- Declare the Hibernate SessionFactory for retrieving Hibernate sessions -->
<!-- See http://static.springsource.org/spring/docs/3.0.x/javadoc-api/org/springframework/orm/hibernate3/annotation/AnnotationSessionFactoryBean.html -->
<!-- See http://docs.jboss.org/hibernate/stable/core/api/index.html?org/hibernate/SessionFactory.html -->
<!-- See http://docs.jboss.org/hibernate/stable/core/api/index.html?org/hibernate/Session.html -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"
p:dataSource-ref="dataSource"
p:configLocation="${hibernate.config}"
p:packagesToScan="org.krams.tutorial"/>

<!-- Declare a datasource that has pooling capabilities-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close"
p:driverClass="${app.jdbc.driverClassName}"
p:jdbcUrl="${app.jdbc.url}"
p:user="${app.jdbc.username}"
p:password="${app.jdbc.password}"
p:acquireIncrement="5"
p:idleConnectionTestPeriod="60"
p:maxPoolSize="100"
p:maxStatements="50"
p:minPoolSize="10" />

<!-- Declare a transaction manager-->
<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager"
p:sessionFactory-ref="sessionFactory" />

</beans>

hibernate.cfg.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
<session-factory>
<!-- We're using MySQL database so the dialect needs to MySQL as well-->
<property name="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</property>
<!-- Enable this to see the SQL statements in the logs-->
<property name="show_sql">true</property>
<!-- This will drop our existing database and re-create a new one.
Existing data will be deleted! -->
<property name="hbm2ddl.auto">create</property>
</session-factory>
</hibernate-configuration>

spring.properties
# database properties
app.jdbc.driverClassName=com.mysql.jdbc.Driver
app.jdbc.url=jdbc:mysql://localhost/mydatabase
app.jdbc.username=root
app.jdbc.password=

#hibernate properties
hibernate.config=/WEB-INF/hibernate.cfg.xml

Run the Application


Setup the database

Our application uses MySQL as its database. To run the application, make sure to setup the database first.

To create the database, follow these steps.
1. Open phpmyadmin (or any tool you prefer with)
2. Create a new database named mydatabase
3. Run the application to automatically create the database schema.

To populate the database with sample data, import the mydatabase.sql SQL script which is located under the WEB-INF folder of the application:


Access the main application

To access the main application, use the following URL:
http://localhost:8080/spring-hibernate-one-to-many-default/krams/main/record/list

You should see the following application:


Conclusion

That's it. We've successfully created a Spring MVC application and implemented a one-to-many association in Hibernate using annotations. We've also discussed issues and solutions when using default settings of @OneToMany annotation.

Download the project
You can access the project site at Google's Project Hosting at http://code.google.com/p/spring-mvc-hibernate-annotations-integration-tutorial/

You can download the project as a Maven build. Look for the spring-hibernate-one-to-many-default.zip in the Download sections.

You can run the project directly using an embedded server via Maven.
For Tomcat: mvn tomcat:run
For Jetty: mvn jetty:run

If you want to learn more about Spring MVC and integration with other technologies, feel free to read my other tutorials in the Tutorials section.
Continue reading Spring - Hibernate: One-To-Many Association