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 has 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.
So what type of operations am I interested? 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. 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 joins.
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 removes the tables if they've already existed. The table gallery and image each has 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 complicate. 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 reference 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 cit 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 to 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 ofmappedBy
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 define 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 3 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 you 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 a implicit join table insert, you must provided 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 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. Expert might prove me wrong, but I prefer HQL. Again, let me tell you that 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 4 scenarios in the Main class, which contains the static main method. It is what we 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 above Java class does 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 3 tables.
The
testPersistence()
method will create 1 row in Gallery table, the id is set to 1. And 2 rows in Image table, the ids are 2 and 3. Since this runs after deleteAll()
, and if successful, then it proves deleteAll()
worked. IfdeleteAll()
failed to work, I use the same ids for all 3 rows, testPersistence()
will fail.
The
testPersistence2()
method will output 2 rows, one with id of 2 and one with id 3. This assumestesPersistence()
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.
No comments:
Post a Comment