In this post, I will list out some issues that I had to deal with while working on a small project where I needed to use many to many mapping between two entities. Once I hashed out all the issues, there is a great way to represent two entity types in many-to-many relationship. This tutorial will show you why and how this is great.
Introduction
Recently, I was working on a small project where I needed to use many to many mapping between two entities. There are many examples online. Most of them did not explain some of the gotchas that I have to work out. This compels me to write this tutorial -- letting readers know some of the issues that I have to deal with. The truth is that once I hashed out all the issues, this is a great way to represent two entity types in many-to-many relationship. This tutorial will show you why and how this is great.
So what is a many to many relationship? Imagine this, you have a blog, and you want to manage your photos uploaded to your blog. You categorize the photos via galleries. There can be five images (image-1 to image-5). and there can two galleries (gal-1, gal-2). The images image-1, image-3, and image-4 are in gal-1; image-2, image-3, image-4, and image-5 are in gal-2. As you can see, the images image-3 and image-4 are in both galleries. These associations can be identified as many-to-many. I used to avoid such a complex relationship, and only deal with two entities that have direct mapping between them, use one-to-one or one-to-many. For this many-to-many relationship, it is do-able with three tables and use some type of one-to-many mapping (one-to-many between gallery and the join
table, and one-to-many between image and join
table). But now, I have realized that such complex mapping might not be a good idea. I am particularly concerned about the number of explicit SQL calls I had to make to the back end DB. With the proper many-to-many mapping, I think Hibernate can help me simplify the operations I am interested in.
So what type of operations am I interested in? Well, here are a few ones:
- I like to create galleries, and upload images, then associate images with a gallery.
- I like to delete galleries or images. To do these, I don't have to explicitly remove the association before deleting.
- I like to find all the images in a gallery and do pagination.
- I like to add or remove the association between gallery and images, yet not delete the gallery or the images.
Background
Sounds simple. How do we do these with plain SQL script? Well, I can insert a row in gallery table, and insert another row
in image
table. Finally, I add a row
into imagetogallery
(this is the join
-table) for these two. Now if I delete either the gallery or the image row, there is a way for SQL DB to automatically delete the row in the join
-table. I can also delete the row in the join
table and severe the relationship between image and gallery. If I want to find all images in a gallery, I do it with one query of two inner join
s.
To illustrate my operations, here is the test table I will be creating (I use MySQL, by the way):
DROP TABLE IF EXISTS imagetogallery;
DROP TABLE IF EXISTS gallery;
DROP TABLE IF EXISTS image;
CREATE TABLE image (
id int NOT NULL PRIMARY KEY,
filepath VARCHAR(256) NULL
);
CREATE TABLE gallery (
id int NOT NULL PRIMARY KEY,
name VARCHAR(128) NULL
);
CREATE TABLE imagetogallery (
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
imageid int NOT NULL,
galleryid int NOT NULL,
FOREIGN KEY (galleryid) REFERENCES gallery(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
FOREIGN KEY (imageid) REFERENCES image(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
The first three lines basically remove the tables if they've already existed. The table gallery and image each have two columns, the first is the primary key "id
". "id
" has type integer and cannot be NULL
. For me to test easily, I will explicitly set the "id
" values in my SQL test and my Hibernate enabled Java program. The last table, imagetogallery
, is more complicated. It has a primary key "id
". And its value is set to auto increment. Providing a new id value of this join
table automatically whenever a row is inserted is very important when I start using Hibernate. I will explain this when I get to it. The join
table also have two foreign keys, one to the gallery
table and one to the image
table. Those two foreign keys have the cascade on update
and delete
. This again is important, for running SQL statement or using Hibernate. Again, when I get there, I will explain why.
Once I created these tables, I thought it would be a good idea to run some simulations against the setup using plain SQL statements. First thing I do is:
INSERT INTO gallery (id, name) VALUES (1, 'My Gallery');
INSERT INTO image (id, filepath) VALUES (2, 'c://images//testimg1.jpg');
INSERT INTO image (id, filepath) VALUES (3, 'c://images//testimg2.jpg');
INSERT INTO imagetogallery (imageid, galleryid) VALUES (2, 1);
INSERT INTO imagetogallery (imageid, galleryid) VALUES (3, 1);
The above code snippet will create a row in gallery table, two rows in the image table, then associate the two image rows with the gallery row. Next, I want to make sure I can do a query to find all the images belong to the gallery with id equals 1. Here is my query:
SELECT image.* FROM image
INNER JOIN imagetogallery ON image.id = imagetogallery.imageid
WHERE imagetogallery.galleryid = 1
The query should succeed and produce the following output:
id | filepath |
2 | c://images//testimg1.jpg |
3 | c://images//testimg2.jpg |
Next, I would experiment with deleting a row in the image
table, say the id
equals 2
. This is done by the following SQL statement:
DELETE FROM image WHERE image.id = 2;
I use the same query that finds the images with gallery id equals 1. The query returns:
id | filepath |
3 | c://images//testimg2.jpg |
What happened? Well, I did mention that when I get to the CASCADE delete
and update
, I will explain what these are for and why they are important. Here it is. When I create the join
table, I don't have to declare the CASCADE delete
or update
. Then if I do the delete
on the image table, I would get an error back indicating the operation will fail because of foreign key constraint violation. The reason is that the join
table has a row that references the image I was about to delete. In order to correct this error, I have to first delete the row in the join
table that has the reference to the image, then I can delete the image. This is rather awkward. Now with the CASCADE delete
on the foreign keys, I can just delete a row in image or in gallery tables and the rows in the join
table that references either the gallery or the image will automatically be deleted. Wow! That is revolutionary! So what does CASCADE update
do? Imagine this, assuming I have to update the id
value for a gallery or a image. That is an update of the primary key (dangerous!) and it could fail with error because one or more rows in the join
table might have reference to this image. But it can happen and can be done. With the CASCADE update declared, I can update the id of that image (as long as the id I chose is not used already in the image table). And DB engine would automatically update the imageid
in the join
table. In fact, if I want to do this to the gallery, I can do it without the manual update to the join
table as well. Magically! I love it!
With all these tests, I am satisfied with the table design. Now I want to move all these to a Hibernate application. It is nothing fancy, just a plain old Java console application.
The Hibernate Application
I salvaged an old Spring based program, and converted into this application. The program has the following file structure:
project base directory
|____DB
|____table1.sql
|____logs
|____{nothing in this folder}
|____src
|____main
|____java
|____org
|____hanbo
|____hibernate
|____experiment
|____entities
|____Gallery.java
|____Image.java
|____ImageGalleryRepository.java
|____Main.java
|____resources
|____application-context.xml
|____log4j.properties
|____pom.xml
Step 1 -- The POM File
I will first show the pom.xml. In this file, it contains all the dependencies needed for my experimental application. It looks like this:
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.hanbo.hibernate-experiment</groupId>
<artifactId>hibernate-manytomany</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>hibernate-manytomany</name>
<url>http://maven.apache.org</url>
<properties>
<spring.version>3.2.11.RELEASE</spring.version>
<slf4j.version>1.7.5</slf4j.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>commons-pool</groupId>
<artifactId>commons-pool</artifactId>
<version>1.6</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>4.2.15.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.javax.persistence</groupId>
<artifactId>hibernate-jpa-2.0-api</artifactId>
<version>1.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>3.1.4.GA</version>
</dependency>
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.29</version>
</dependency>
</dependencies>
<build>
<finalName>hiberfnate-manytomany</finalName>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<phase>install</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
The ones from Spring framework are used for dependency injections, integration with Hibernate, and DB transactions. The ones from Apache Commons are used for logging, convenience, and connection pools. I use Log4J exclusively for logging. The other ones, are MySQL JDBC driver, JBoss logging used by Hibernate, the Hibernate library, JPA annotation, and javax transactions.
To build this application. All you need to do is:
mvn clean install
Step 3 -- The Entities Classes
To get all my scenarios to work with Hibernate, I need to define my entities. Because I am using join
table for the many-to-many association. I only need to define two entities. The entity for the Gallery
and entity for Image
. The join
table is implicitly defined in the two entities. As you can see, using the join
table, I actually don't need to define three entities, but only two. Then I can let Hibernate and database do the heavy lifting for me.
Let me show you the Java code for the Image
entity. Here it is:
package org.hanbo.hibernate.experiment.entities;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
@Entity
@Table(name = "image")
public class Image
{
@Id
@Column(name = "id", nullable = false)
private int id;
@Column(name = "filepath", nullable = true, length = 256)
private String filePath;
@ManyToMany(fetch = FetchType.LAZY, mappedBy = "associatedImages")
private Set<Gallery> associatedGalleries;
public Image()
{
associatedGalleries = new HashSet<Gallery>();
}
public int getId()
{
return id;
}
public void setId(int id)
{
this.id = id;
}
public String getFilePath()
{
return filePath;
}
public void setFilePath(String filePath)
{
this.filePath = filePath;
}
public Set<Gallery> getAssociatedGalleries()
{
return associatedGalleries;
}
public void setAssociatedGalleries(Set<Gallery> associatedGalleries)
{
this.associatedGalleries = associatedGalleries;
}
}
The most important part of this class is the many-to-many annotation:
@ManyToMany(fetch = FetchType.LAZY, mappedBy = "associatedImages")
private Set<Gallery> associatedGalleries;
You might ask, where do I find this "associatedImages
"? It is located in the Gallery
entity class. The idea of mappedBy
is to have the Image
entity looks up the Gallery
entity definition and find the collection of images. This collection property in Gallery
entity is called "associatedImages
". Now let me share the source code to the Gallery
entity definition:
package org.hanbo.hibernate.experiment.entities;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.JoinColumn;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
@Entity
@Table(name = "gallery")
public class Gallery
{
@Id
@Column(name = "id", nullable = false)
private int id;
@Column(name = "name", nullable = true, length = 128)
private String name;
@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinTable(name = "imagetogallery", joinColumns = {
@JoinColumn(name = "galleryid",
nullable = false, updatable = false)
}, inverseJoinColumns = {
@JoinColumn(name = "imageid",
nullable = false, updatable = false)
}
)
private Set<Image> associatedImages;
public Gallery()
{
setAssociatedImages(new HashSet<Image>());
}
public int getId()
{
return id;
}
public void setId(int id)
{
this.id = id;
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public Set<Image> getAssociatedImages()
{
return associatedImages;
}
public void setAssociatedImages(Set<Image> associatedImages)
{
this.associatedImages = associatedImages;
}
}
As you can see, the most complex part of the class is the following, which actually defines the many-to-many association using the join
table I have created:
@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinTable(name = "imagetogallery", joinColumns = {
@JoinColumn(name = "galleryid",
nullable = false, updatable = false)
}, inverseJoinColumns = {
@JoinColumn(name = "imageid",
nullable = false, updatable = false)
}
)
private Set<Image> associatedImages;
There are a couple things you need to know:
- The cascade type I use is
CascadeType.All
, meaning that when I do changes to Image
entity or Gallery
entity, all the changes must be propagated (cascade effect) to the join
table. - The
JoinTable
annotation defines the join
table I have created. - The
JoinTable
annotation also defines the join
columns, one for galleryid
, and one for the imageid
. These are the table columns in the join
table. - The first
join
column is the galleryid
in the join
table. The second one is the inverse join
column imageid
. These two tells Gallery
entity how to find itself in the join
table, and the images associated with the gallery. - The
join
columns has two properties. One is called nullable
, I set it to false
means for that column, the value cannot be null
. The other is called updatable
, I set it to false
. The values in these columns are primary keys to other tables, allowing them to be updatable is just not wise.
Step 4 -- The Repository Class
I also created a repository class so that I can play with the scenarios. The repository
class is in the same package as the entities. The class looks like this:
package org.hanbo.hibernate.experiment.entities;
import java.util.List;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
@Repository
@SuppressWarnings("unchecked")
public class ImageGalleryRepository
{
private static Logger _logger = LogManager.getLogger(ImageGalleryRepository.class);
@Autowired
private SessionFactory _sessionFactory;
@Transactional
public void deleteAll()
{
Session session = _sessionFactory.getCurrentSession();
Query q = session.createQuery("delete from Gallery");
q.executeUpdate();
q = session.createQuery("delete from Image");
q.executeUpdate();
}
@Transactional
public int testPersistence()
{
Session session = _sessionFactory.getCurrentSession();
Gallery gallery = new Gallery();
gallery.setId(1);
gallery.setName("My Test Gallery");
Image img1 = new Image();
img1.setId(2);
img1.setFilePath("C:\\testimages\\img1.jpg");
gallery.getAssociatedImages().add(img1);
Image img2 = new Image();
img2.setId(3);
img2.setFilePath("C:\\testimages\\img2.jpg");
gallery.getAssociatedImages().add(img2);
session.save(gallery);
return gallery.getId();
}
@Transactional
public void testPersistence2()
{
Session session = _sessionFactory.getCurrentSession();
Query query = session.createQuery(
"select image from Gallery gallery join gallery.associatedImages image"
+ " where gallery.id = :galId order by image.id desc"
).setParameter("galId", 1).
setMaxResults(2);
List<Image> imgs = query.list();
for(Image image : imgs)
{
_logger.info(String.format("Image Id: %s", image.getId()));
_logger.info(String.format("Image File Path: %s", image.getFilePath()));
}
}
@Transactional
public void testPersistence3()
{
Session session = _sessionFactory.getCurrentSession();
Gallery gal = (Gallery)session.get(Gallery.class, 1);
Image img = (Image)session.get(Image.class, 3);
gal.getAssociatedImages().remove(img);
session.update(gal);
}
}
This class is quite complicated.
First, the class is annotated as repository. Inside, there is an autowired property called _sessionFactory
. Then there are four methods, each has a purpose:
deleteAll
: can be used to clean up the two tables. Which in term deletes the rows in the join
table as well. testPersistence
: used to create one gallery, and two images. Then associate the two images to the gallery. testPersistence2
: used to find all the images that belong to a specific gallery. This is done by Hibernate Query Language. testPersistence3
: used to remove the image with id 3 from the collection of images held by the gallery. Then session updates the gallery. This should remove a row from the join
table.
The source code for deleteAll
:
@Transactional
public void deleteAll()
{
Session session = _sessionFactory.getCurrentSession();
Query q = session.createQuery("delete from Gallery");
q.executeUpdate();
q = session.createQuery("delete from Image");
q.executeUpdate();
}
This method gives me a way to delete all the rows from all three tables. The way I do this is to assume that I have everything setup correctly, especially with the CASCADE setups, both in code and in DB table setup. And when I delete all rows in one table, the rows in join
table will be gone. I can safely delete all rows in the other table, with no problem.
The source code for testPersistence
:
@Transactional
public int testPersistence()
{
Session session = _sessionFactory.getCurrentSession();
Gallery gallery = new Gallery();
gallery.setId(1);
gallery.setName("My Test Gallery");
Image img1 = new Image();
img1.setId(2);
img1.setFilePath("C:\\testimages\\img1.jpg");
gallery.getAssociatedImages().add(img1);
Image img2 = new Image();
img2.setId(3);
img2.setFilePath("C:\\testimages\\img2.jpg");
gallery.getAssociatedImages().add(img2);
session.save(gallery);
return gallery.getId();
}
The code above should be very self-explanatory. I create the Image
and Gallery
entities like regular objects, invoking the constructor and setters. One thing the reader should notice is that there is only one save of entity -- saving the Gallery
entity after the two Image
entities are added into Gallery
's collection of Image. This can be done because Hibernate does the heavy lifting for us. It actually does the insertion of the Image
entities for us. I am sure if you turn log4j level to DEBUG, you can see it.
Is that the only way we can do to these two types entities? Of course not, you can create the two Image
entities and explicitly save them, then create the Gallery
entity, add the two Image entities to its collection, then save the Gallery
. I've tested it. It works. I am sure if you want, you can create Gallery
entity, then save it. Then create the two Image entities, save them as well. Finally, somewhere down the line of CRUD operations, you can add the images to the collection for the Gallery
entity, then save the Gallery
entity.
I promised you the reader that I will explain why I needed AUTO_INCREMENT
in the join
table. The above code is the reason. As you can see, there is no code that inserts a row into the join
table. It is done by Hibernate. What it does is just insert rows with the values (galleryid = 1
, imageid = 2
or 3
). If you don't use that AUTO_INCREMENT
in the join
table for the primary key, the save()
via session will fail with an exception. This is something I wish some one could have told me. It took me a while to figure out. Now you know, with an implicit join
table insert
, you must provide a way to create unique primary key to the join
table rows automatically.
The code for testPersistence2
:
@Transactional
public void testPersistence2()
{
Session session = _sessionFactory.getCurrentSession();
Query query = session.createQuery(
"select image from Gallery gallery join gallery.associatedImages image"
+ " where gallery.id = :galId order by image.id desc"
).setParameter("galId", 1).
setMaxResults(2);
List<Image> imgs = query.list();
for(Image image : imgs)
{
_logger.info(String.format("Image Id: %s", image.getId()));
_logger.info(String.format("Image File Path: %s", image.getFilePath()));
}
}
For the above method, I was trying to do is experimenting and see if I can get just the images, all the images, of a gallery if I only have the gallery Id. I think I can do this easily with HQL, I can also do it by traversing from Gallery
entity down to its Image
collections. But it is not as easy if I want to sort the Image
collection by date in descending order, and limit the number of Image entities to a certain number or set the start index to a certain number. Experts might prove me wrong, but I prefer HQL. Again, let me tell you that the above code works. As you can see, I was doing a join
of Gallery
table and Image
table and I didn't even mention the join
table at all. Isn't this awesome?
The code for our last method testPersistence3
:
@Transactional
public void testPersistence3()
{
Session session = _sessionFactory.getCurrentSession();
Gallery gal = (Gallery)session.get(Gallery.class, 1);
Image img = (Image)session.get(Image.class, 3);
gal.getAssociatedImages().remove(img);
session.update(gal);
}
What this method does is that, assuming that I have the gallery id and I have an image id, I want to remove the association of this image from this gallery. Again, you can see how cool this method is, I look up the gallery, and the image, then I involve the getAssociatedImages()
of that gallery, then I do a remove of that image from the collection. Finally, I just save the gallery
entity. I can assure you this works. And it is just plain awesome. All because we have the CASCADE
settings properly added to the tables and to the entity classes.
Next, I want to run all the four scenarios in the Main
class, which contains the static main
method. It is what we are going to see next.
Step 5 -- The Main Class
The main
class is just a way for me to demo the test methods in my repository class. It is quite simple. Here it is:
package org.hanbo.hibernate.experiment;
import org.hanbo.hibernate.experiment.entities.ImageGalleryRepository;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main
{
public static void main(String[] argv)
{
ConfigurableApplicationContext context =
new ClassPathXmlApplicationContext("application-context.xml");
ImageGalleryRepository repo
= context.getBean(ImageGalleryRepository.class);
repo.deleteAll();
repo.testPersistence();
repo.testPersistence2();
repo.testPersistence3();
context.close();
}
}
What the above Java class does is the following:
- First, load the application context. Application context contains all the bean definition. It has to be the first thing to be loaded.
- Once the app has the application context object, I should just be able to get the bean I wanted to use. In this case, I want to get the repository object so that I can run my scenarios.
- In my application, I run my scenarios.
- Close the application context so that application can exit.
Step 6 -- The Application Context XML File
Since this is a spring application, I need to define the beans, the configuration of Hibernate, MySQL, and transaction manager, etc. Here is my application context XML file:
<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:tx="http://www.springframework.org/schema/tx"
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/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
<context:component-scan base-package="org.hanbo.hibernate.experiment.entities" />
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/hanbotest?autoReconnect=true"/>
<property name="username" value="hbuser1"/>
<property name="password" value="123test321"/>
<property name="maxActive" value="8"/>
<property name="maxIdle" value="4"/>
<property name="maxWait" value="900000"/>
<property name="validationQuery" value="SELECT 1" />
<property name="testOnBorrow" value="true" />
</bean>
<bean id="sessionFactory"
class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="packagesToScan">
<array>
<value>org.hanbo.hibernate.experiment.entities</value>
</array>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
<prop key="hibernate.cache.provider_class">
org.hibernate.cache.NoCacheProvider</prop>
</props>
</property>
</bean>
<bean id="transactionManager"
class="org.springframework.orm.hibernate4.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
If you have a little experience with Spring, this file will not be difficult to read. Essentially, I am combining the use of annotations and application context XML file. Here are the sections of my XML file:
- The component scan piece tells Spring framework that certain packages contains classes that can be found for dependency injection.
- Date source piece creates the configuration of JDBC driver against MySQL DB.
- The session factory piece creates the session factory bean for Hibernate.
- The last two pieces are for transaction manager. You the reader might have seen the annotation
@Transactional
on the methods in the repository. In order to use this annotation, I have to define these two pieces.
Step 6 -- Log4J Configuration
I also need to configure log4j
logging. It is fairly easy to do, I get a sample logging properties file from log4j
official site, and modified for my project. The content looks like this:
log4j.rootLogger=debug, stdout, R
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
# Pattern to output the caller's file name and line number.
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n
log4j.appender.R=org.apache.log4j.DailyRollingFileAppender
log4j.appender.R.File=C:/temp/logs/hibernate-manytomany.log
log4j.appender.R.DatePatttern=yyyy-MM-dd
log4j.appender.R.MaxFileSize=10MB
log4j.appender.R.MaxBackupIndex=10
log4j.appender.R.layout=org.apache.log4j.PatternLayout
log4j.appender.R.layout.ConversionPattern=%p %t %c - %m%n
The file uses a daily rolling log file. The file has max size of 10MB, and max back up copy of 10
. The very first line allows you to control the logging level. I set it to DEBUG
. If you want to tune down the level, you can change DEBUG
to INFO
. The other lines, they are unimportant. Please make sure you have the logging directory created -- C:\temp\logs\
The Execution Result
The deleteAll()
method will remove any rows in all three tables.
The testPersistence()
method will create one row in Gallery
table, the id
is set to 1
. And two rows in Image
table, the id
s are 2
and 3
. Since this runs after deleteAll()
, and if successful, then it proves deleteAll()
worked. If deleteAll()
failed to work, I use the same id
s for all three rows, testPersistence()
will fail.
The testPersistence2()
method will output two rows, one with id
of 2
and one with id
3
. This assumes tesPersistence()
worked correctly. And the output should be the details of the two Image
rows.
The last testPersistence3()
will remove the association between image with id 3
and gallery with id 1
. I did not provide any output test methods= to test this. So you just have to run query against image
table and imagetogallery
table in DB. Both rows in image
table should be there. There should be just one row in imagetogallery
table.
Points of Interest
Mapping many-to-many relationship between two entities using a join
table is probably the most complicated entities configuration you have to do. After I get to this point, I am very confident about using Hibernate with relational database. This tutorial is clear enough. There is a lot of information. I am also confident that this tutorial beats all other similar ones. It has been great fun writing this. Hope you enjoy it!
History
- 31st December, 2015: Initial draft