At line 2 changed 1 line. |
This is a tutorial to show how to create and manage Hibernate relationships within [AppFuse]. This tutorial was written using AppFuse 1.8. All of the code for this tutorial is downloadable. To begin, I created a new application with an app/db name of blog/blog. |
This is a tutorial to show how to create and manage Hibernate relationships within [AppFuse]. This tutorial was written using AppFuse 1.8.2. All of the code for this tutorial is downloadable at [http://static.appfuse.org/downloads/appfuse-hr.zip]. |
At line 9 changed 2 lines. |
* [1] Create Weblog.java, Entry.java and add XDoclet tags |
* [2] [[Many-to-One] Create a new Category object and modify Entry.java to use it |
* [1] Create Weblog.java, Entry.java and Category.java domain objects |
* [2] [[Many-to-Many] A Weblogs can have many Users, a User can have many Weblogs |
At line 12 changed 2 lines. |
* [4] [[Many-to-Many] Shared Weblogs are possible for multiple Users |
* [5] [[DAO Layer] Setting up Unit Tests and DAO classes for basic CRUD |
* [4] [[Many-to-One] A Category object can be assigned to many entries |
At line 17 changed 1 line. |
In this example, the ''Weblog'' object is used to indentify a person's blog. This class has the following properties: |
The ''Weblog'' object is used to indentify a person's blog. This class has the following properties: |
At line 20 removed 1 line. |
* username |
At line 32 changed 1 line. |
<!-- TODO: Create diagram of the tables and their relationships. --> |
Below is a class diagram of these two objects, as well as the others you'll create in this tutorial. |
At line 34 changed 1 line. |
The first thing you need to do in this tutorial is these two object to persist. Create both a ''Weblog.java'' class and an ''Entry.java'' class (in the src/dao/**/model directory). The necessary XDoclet tags for these entities is included on the ''getter'' method's javadoc. You can download these files using the links below. Note that javadocs have been eliminated for brevity. |
[ER-Diagram.jpg] |
At line 34 added 2 lines. |
The first thing you need to do in this tutorial is these two object to persist. Create a __Weblog.java__ class and an __Entry.java__ class (in the src/dao/**/model directory). The necessary XDoclet tags for these entities is included on the ''getter'' method's javadoc. You can download these files using the links below. Note that javadocs have been eliminated for brevity. |
|
At line 41 changed 1 line. |
%%note __Tip:__ The equals(), hashCode() and toString() methods can be generated by your IDE. If you're using Eclipse, get the [Commonclipse|http://commonclipse.sourceforge.net/] plugin. For IDEA, the first two are built into the "Generate..." dialog, and toString() is a plugin you can download. Make sure you don't have primary key's in your ''equals()'' and ''hashCode()'' methods (as recommended by Hibernate in Action).%% |
Create a __Category.java__ object to act as an entity for persisting category information about weblog entries. Each category can have many entries. This class contains the following properties: |
At line 43 removed 4 lines. |
!![[Many-to-One] Create a new Category object and modify Entry.java to use it [#2] |
|
A category object will act as an entity for persisting category information about weblog entries. Each category can have many entries. This class contains the following properties: |
|
At line 51 changed 1 line. |
* [View Category.java|Category.java] |
* [Download Category.java|Category.java] |
At line 49 added 1 line. |
!Configure Spring |
At line 51 added 1 line. |
Add the 3 new mapping files (that will be generated) to the "sessionFactory" bean's ''mappingResources'' property in __src/org/appfuse/dao/hibernate/applicationContext-hibernate.xml__. |
At line 53 added 1 line. |
[{Java2HtmlPlugin |
At line 56 changed 1 line. |
The ''many-to-one'' relationship between __Entry__ and __Category__ can be established using XDoclet tags. Hibernate relationships can be established on either side of the relationship or bi-directionally. For this tutorial, this relationship will be maintained by an extra property and collection held by __Entry__. The list of possible categories for a weblog entry will eventually be represented as a drop-down on the UI. Add the following code to your __Entry.java__ class. |
<property name="mappingResources"> |
<list> |
<value>org/appfuse/model/Role.hbm.xml</value> |
<value>org/appfuse/model/User.hbm.xml</value> |
<value>org/appfuse/model/Weblog.hbm.xml</value> |
<value>org/appfuse/model/Entry.hbm.xml</value> |
<value>org/appfuse/model/Category.hbm.xml</value> |
</list> |
</property> |
}] |
At line 58 changed 1 line. |
[{Java2HtmlPlugin |
!![[Many-to-Many] A Weblogs can have many Users, a User can have many Weblogs [#2] |
At line 60 changed 2 lines. |
private Long categoryId; |
private Category category; |
A Weblog can have many Users. Basically the idea is of a shared weblog that is a place where many users can express themselves about a particular topic of interest. For this bit of functionality the User object will be modified to have a many-to-many relationship with Weblog. |
At line 70 added 6 lines. |
Add the following ''users'' property and accessor methods to __Weblog.java__. |
|
[{Java2HtmlPlugin |
|
private List users = new ArrayList(); |
|
At line 64 changed 2 lines. |
* @hibernate.many-to-one insert="false" update="false" cascade="none" |
* column="category_id" outer-join="true" |
* @hibernate.bag table="weblog_user" cascade="save-update" lazy="true" |
* @hibernate.collection-key column="weblog_id" |
* @hibernate.collection-many-to-many class="org.appfuse.model.User" column="username" |
At line 67 changed 2 lines. |
public Category getCategory() { |
return category; |
public List getUsers() { |
return users; |
At line 71 changed 2 lines. |
public void setCategory(Category category) { |
this.category = category; |
public void addUser(User user) { |
getUsers().add(user); |
At line 89 added 15 lines. |
public void setUsers(List users) { |
this.users = users; |
} |
}] |
|
%%note __NOTE:__ The reason a java.util.List is used instead of a java.util.Set is because this is because [Sets aren't supported for indexed properties in Struts|http://www.mail-archive.com/[email protected]/msg26289.html].%% |
|
!User modifications |
|
To allow navigation from a User objec to a list of Weblogs, you need to add a List of Weblogs to the User object. Modify ''User.java'' to add a ''weblogs'' List and accessor methods. |
|
[{Java2HtmlPlugin |
|
protected List weblogs; |
|
At line 76 changed 1 line. |
* @hibernate.property column="category_id" |
* @hibernate.bag table="weblog_user" cascade="save-update" lazy="true" |
* @hibernate.collection-key column="username" |
* @hibernate.collection-many-to-many class="org.appfuse.model.Weblog" column="weblog_id" |
At line 78 changed 2 lines. |
public Long getCategoryId() { |
return categoryId; |
public List getWeblogs() { |
return weblogs; |
At line 82 changed 2 lines. |
public void setCategoryId(Long categoryId) { |
this.categoryId = categoryId; |
public void setWeblogs(List weblogs) { |
this.weblogs = weblogs; |
At line 118 added 59 lines. |
!Test it! |
|
Create a unit test so you can verify that everything actually works. Create a __WeblogDaoTest__ class in your test/dao/**/dao directory. This file should extend GenericDaoTest. If you're using anything less than AppFuse 1.9, you may have to modify GenericDAOTest's "dao" variable so its __protected__ instead of private. Copy the code below into this test: |
|
[{Java2HtmlPlugin |
|
package org.appfuse.dao; |
|
import java.util.Date; |
|
import org.appfuse.model.User; |
import org.appfuse.model.Weblog; |
|
public class WeblogDaoTest extends GenericDAOTest { |
|
public void testWeblogAndUsers() throws Exception { |
Weblog w = new Weblog(); |
w.setBlogTitle("My New Weblog"); |
w.setDateCreated(new Date()); |
|
// add it to the database |
dao.saveObject(w); |
|
w = (Weblog) dao.getObject(Weblog.class, w.getWeblogId()); |
|
assertTrue(w.getUsers().isEmpty()); |
|
// add a user |
User u = (User) dao.getObject(User.class, "mraible"); |
w.addUser(u); |
|
dao.saveObject(w); |
|
w = (Weblog) dao.getObject(Weblog.class, w.getWeblogId()); |
|
assertTrue(w.getUsers().size() == 1); |
|
// remove the user |
w.setUsers(null); |
dao.saveObject(w); |
|
w = (Weblog) dao.getObject(Weblog.class, w.getWeblogId()); |
|
assertNull(w.getUsers()); |
} |
} |
}] |
|
Make sure you run __ant setup-db__ before running __ant test-dao -Dtestcase=UserDAO__. When running the test, you'll probably get a ''LazyInitializationException''. To solve this, change BaseDaoTestCase to extend Spring's [AbstractTransactionalDataSourceSpringContextTests|http://www.springframework.org/docs/api/org/springframework/test/AbstractTransactionalDataSourceSpringContextTests.html] if it doesn't already. In additional, you'll need to make a few changes to the existing tests so all the tests pass. [Here is a patch|LazyDaoTests.diff]. At a minimum, you'll need to fix BaseDAOTestCase and GenericDAOTest (onSetUp() -> onSetupBeforeTransaction()). |
|
Because the WeblogDaoTest is now extending AbstractTransactionalDataSourceSpringContextTests (ATDSSCT), there is a "jdbcTemplate" variable you can use to do additional querying. For instance, you could add the following at the end of your ''testWeblogAndUsers()'' method: |
|
[{Java2HtmlPlugin |
|
String qry = "select count(*) from weblog_user where username = 'mraible' " + |
" and weblog_id = " + w.getWeblogId(); |
assertEquals(0, jdbcTemplate.queryForInt(qry)); |
}] |
|
At line 89 changed 1 line. |
Now you'll the __Weblog__ object and __Entry__ object to represent the multiplicity of a weblog that can have many entries. This relationship is set on the Weblog class using a java.util.List. XDoclet tags are used to establish this relationship using a __bag__ as the Hibernate collection type. Add the following code to your __Weblog.java__ class. |
Now you'll modify the __Weblog__ object and __Entry__ object to represent the multiplicity of a weblog that can have many entries. This relationship is set on the Weblog class using a java.util.List. XDoclet tags are used to establish this relationship using a __bag__ as the Hibernate collection type. Add the following code to your __Weblog.java__ class. |
At line 98 changed 1 line. |
* @hibernate.bag name="entries" lazy="false" inverse="true" cascade="delete" |
* @hibernate.bag name="entries" lazy="false" cascade="all" |
At line 111 changed 1 line. |
Modify the Entry class is so it contains a foreign key to it's parent __Weblog__ class. |
Modify the __Entry__ class is so it contains a foreign key to its parent __Weblog__ class. |
At line 129 changed 1 line. |
!![[Many-to-Many] [#4] |
At this point, you could add an additional test method to WeblogDaoTest to test that this relationship works. Of course, you'll need to run __ant setup-db__ to make sure your database knows about the relationship. |
At line 131 changed 1 line. |
The Weblog system in this tutorial allows one additional bit of functionality. A particular Weblog can have many Users. Basically the idea is of a shared weblog that is a place where many users can express themselves about a particular topic of interest. For this bit of functionality the User object will be modified to have a many-to-many relationship with Weblog. |
!![[Many-to-One] A Category object can be assigned to many entries [#4] |
At line 133 changed 1 line. |
Add the following ''users'' property and accessor methods to __Weblog.java__. |
The ''many-to-one'' relationship between __Category__ and __Entry__ can be established using XDoclet tags. Hibernate relationships can be established on either side of the relationship or bi-directionally. For this tutorial, this relationship is maintained by a __categoryId__ property and a __Category__ object in __Entry__. The list of possible categories for a weblog entry will eventually be represented as a drop-down on the UI. Add the following code to your __Entry.java__ class. Notice that the relationship to __Category__ has ''insert="false" update="false"''. This is because the object is read-only. This can be useful for displaying a category's name on the UI when you're viewing an Entry. |
At line 137 changed 2 lines. |
private Set users = new HashSet(); |
|
private Long categoryId; |
private Category category; |
|
At line 140 changed 3 lines. |
* @hibernate.set table="weblog_user" cascade="none" lazy="false" |
* @hibernate.collection-key column="weblog_id" |
* @hibernate.collection-many-to-many class="org.appfuse.model.User" column="username" |
* @hibernate.many-to-one insert="false" update="false" cascade="none" |
* column="category_id" outer-join="true" |
At line 144 changed 2 lines. |
public Set getUsers() { |
return users; |
public Category getCategory() { |
return category; |
At line 148 changed 2 lines. |
public void addUser(User user) { |
getUsers().add(user); |
public void setCategory(Category category) { |
this.category = category; |
At line 152 removed 13 lines. |
public void setUsers(Set users) { |
this.users = users; |
} |
}] |
|
!User modifications |
|
In addition to allow navigation from a Weblog object to their list of users, you need to add a Set of Weblogs to the User object - allowing that path of navigation as well. Modify ''User.java'' to add a ''weblogs'' Set and accessor methods. You might notice the __cascade="none"__ attribute. This is because this system doesn't currenly allow a weblog to be edited from the user screen and vise-versa. You'll change this in the section where you create the UI. |
|
[{Java2HtmlPlugin |
|
protected Set weblogs; |
|
At line 166 changed 3 lines. |
* @hibernate.set table="weblog_user" cascade="none" lazy="false" |
* @hibernate.collection-key column="username" |
* @hibernate.collection-many-to-many class="org.appfuse.model.Weblog" column="weblog_id" |
* @hibernate.property column="category_id" |
At line 170 changed 2 lines. |
public Set getWeblogs() { |
return weblogs; |
public Long getCategoryId() { |
return categoryId; |
At line 174 changed 2 lines. |
public void setWeblogs(Set weblogs) { |
this.weblogs = weblogs; |
public void setCategoryId(Long categoryId) { |
this.categoryId = categoryId; |
At line 179 changed 1 line. |
!![[DAO Layer] [#5] |
After making these changes, your __WeblogDaoTest__ should still pass. Verify it does by running __ant setup-db test-dao -Dtestcase=Weblog__. |
At line 181 changed 1 line. |
In order to test that all of this works, you need to create some test classes, add some sample data, develop some DAO interfaces, and create Hibernate DAO classes. |
''__Yeah baby, Yeah!__'' |
%%(color: green) |
BUILD SUCCESSFUL\\ |
Total time: 24 seconds |
%% |
At line 183 changed 1 line. |
!!Test Classes |
Before working on the UI, it's helpful to have some sample data so you don't have to do manual data entry. Add the following XML to the bottom of __metadata/sql/sample-data.xml__. |
At line 185 removed 8 lines. |
There are a number of ways to construct the DAO layer for this application. You could have a WeblogDAO that handles CRUD for all entities, or you could have separate DAOs for each entity. This tutorial uses the separate DAO approach because this seems to be the most common method used by AppFuse users. Personally, I tend to group my logical features into a single DAO with many methods. Download the following three test classes and put them in your ''test/dao/**/dao'' directory. |
|
* [Download WeblogDaoTest.java|WeblogDAOTest.java] |
* [Download EntryDaoTest.java|EntryDAOTest.java] |
* [Download CategoryDaoTest.java|CategoryDAOTest.java] |
|
All of these tests rely on some sample data, so add the following XML to __metadata/sql/sample-data.xml__. |
|
At line 199 removed 1 line. |
<column>username</column> |
At line 204 removed 1 line. |
<value>tomcat</value> |
At line 210 removed 1 line. |
<value>mraible</value> |
At line 235 changed 1 line. |
<value><![CDATA[Struts v. Spring MVC]]></value> |
<value><![CDATA[Struts vs. Spring MVC]]></value> |
At line 258 changed 1 line. |
<value>2005-04-11</value> |
<value>2005-04-11 01:02:03.4</value> |
At line 265 changed 1 line. |
<value>2005-04-12</value> |
<value>2005-04-12 01:02:03.4</value> |
At line 272 changed 1 line. |
<value>2005-04-12</value> |
<value>2005-04-12 01:02:03.4</value> |
At line 280 removed 235 lines. |
!!DAO Interfaces |
|
Before any of these tests will compile, you need to create the DAO interfaces and their implementations. |
|
!WeblogDAO Interface |
Create a ''WeblogDAO.java'' interface in src/dao/**/dao: |
|
[{Java2HtmlPlugin |
|
package org.appfuse.dao; |
|
import java.util.List; |
import org.appfuse.model.Weblog; |
|
public interface WeblogDAO extends Dao{ |
public Weblog getWeblog(Long weblogId); |
public List getWeblogs(Weblog weblog); |
public void saveWeblog(Weblog weblog); |
public void removeWeblog(Long weblogId); |
public List getEntries(Long weblogId); |
} |
}] |
|
!EntryDAO Interface |
|
[{Java2HtmlPlugin |
|
package org.appfuse.dao; |
|
import java.util.List; |
import org.appfuse.model.Entry; |
|
public interface EntryDAO { |
public Entry getEntry(Long entryId); |
public List getEntries(Entry entry); |
public void saveEntry(Entry entry); |
public void removeEntry(Long entryId); |
} |
}] |
|
!CategoryDAO Interface |
|
[{Java2HtmlPlugin |
|
package org.appfuse.dao; |
|
import java.util.List; |
import org.appfuse.model.Category; |
|
public interface CategoryDAO { |
public Category getCategory(Long categoryId); |
public List getCategories(Category category); |
public void saveCategory(Category category); |
public void removeCategory(Long categoryId); |
} |
|
}] |
|
!!DAO Implementation |
|
Then create the Hibernate implementations of the interfaces in the src/dao/**/dao/hibernate directory. |
|
!WeblogDAOHibernate |
|
[{Java2HtmlPlugin |
|
package org.appfuse.dao.hibernate; |
|
import java.util.List; |
|
import org.appfuse.dao.WeblogDAO; |
import org.appfuse.model.Weblog; |
import org.springframework.orm.ObjectRetrievalFailureException; |
|
public class WeblogDAOHibernate extends BaseDaoHibernate implements WeblogDAO { |
|
public Weblog getWeblog(Long weblogId) { |
Weblog weblog = (Weblog) getHibernateTemplate().get(Weblog.class, |
weblogId); |
|
if (weblog == null) { |
log.warn("uh oh, weblog '" + weblogId + "' not found..."); |
throw new ObjectRetrievalFailureException(Weblog.class, weblogId); |
} |
|
return weblog; |
} |
|
public List getWeblogs(Weblog weblog) { |
return getHibernateTemplate().find( |
"from Weblog wl order by upper(wl.blogTitle)"); |
} |
|
public void saveWeblog(final Weblog weblog) { |
getHibernateTemplate().saveOrUpdate(weblog); |
} |
|
public void removeWeblog(Long weblogId) { |
getHibernateTemplate().delete(getWeblog(weblogId)); |
} |
|
public List getEntries(Long weblogId) { |
return getHibernateTemplate().find("from Entry e where e.weblogId=?", |
weblogId); |
} |
} |
}] |
|
!EntryDAOHibernate |
|
[{Java2HtmlPlugin |
|
package org.appfuse.dao.hibernate; |
|
import java.util.List; |
|
import org.appfuse.dao.EntryDAO; |
import org.appfuse.model.Entry; |
import org.springframework.orm.ObjectRetrievalFailureException; |
|
public class EntryDAOHibernate extends BaseDaoHibernate implements EntryDAO { |
|
public Entry getEntry(Long entryId) { |
Entry entry = (Entry) getHibernateTemplate().get(Entry.class, entryId); |
|
if (entry == null) { |
log.warn("uh oh, weblog '" + entryId + "' not found..."); |
throw new ObjectRetrievalFailureException(Entry.class, entryId); |
} |
|
return entry; |
} |
|
public List getEntries(Entry entry) { |
return getHibernateTemplate().find("from Entry e order by upper(e.timeCreated)"); |
} |
|
public void saveEntry(final Entry entry) { |
getHibernateTemplate().saveOrUpdate(entry); |
} |
|
public void removeEntry(Long entryId) { |
getHibernateTemplate().delete(getEntry(entryId)); |
} |
} |
|
}] |
|
!CategoryDAOHibernate |
|
[{Java2HtmlPlugin |
|
package org.appfuse.dao.hibernate; |
|
import java.util.List; |
|
import org.appfuse.dao.CategoryDAO; |
import org.appfuse.model.Category; |
import org.springframework.orm.ObjectRetrievalFailureException; |
|
public class CategoryDAOHibernate extends BaseDaoHibernate implements CategoryDAO { |
|
public Category getCategory(Long categoryId) { |
Category category = (Category) getHibernateTemplate().get(Category.class, categoryId); |
|
if (category == null) { |
log.warn("uh oh, weblog '" + categoryId + "' not found..."); |
throw new ObjectRetrievalFailureException(Category.class, categoryId); |
} |
|
return category; |
} |
|
public List getCategories(Category category) { |
return getHibernateTemplate().find("from Category cat order by upper(cat.categoryName)"); |
} |
|
public void saveCategory(final Category category) { |
getHibernateTemplate().saveOrUpdate(category); |
} |
|
public void removeCategory(Long categoryId) { |
getHibernateTemplate().delete(getCategory(categoryId)); |
} |
} |
}] |
|
!Configure Spring |
|
Modifications need to be made in __src/org/appfuse/dao/hibernate/applicationContext-hibernate.xml__ for the 3 new entities you created in this tutorial. |
|
[{Java2HtmlPlugin |
|
<property name="mappingResources"> |
<list> |
<value>org/appfuse/model/Role.hbm.xml</value> |
<value>org/appfuse/model/User.hbm.xml</value> |
<value>org/appfuse/model/UserCookie.hbm.xml</value> |
<value>org/appfuse/model/Weblog.hbm.xml</value> |
<value>org/appfuse/model/Entry.hbm.xml</value> |
<value>org/appfuse/model/Category.hbm.xml</value> |
</list> |
</property> |
}] |
|
Also, you need to add the "weblogDAO", "entryDAO", and "categoryDAO" bean definitions. |
|
[{Java2HtmlPlugin |
|
<!-- WeblogDAO: Hibernate implementation --> |
<bean id="weblogDAO" class="org.appfuse.dao.hibernate.WeblogDAOHibernate"> |
<property name="sessionFactory"><ref local="sessionFactory"/></property> |
</bean> |
|
<!-- EntryDAO: Hibernate implementation --> |
<bean id="entryDAO" class="org.appfuse.dao.hibernate.EntryDAOHibernate"> |
<property name="sessionFactory"><ref local="sessionFactory"/></property> |
</bean> |
|
<!-- CategoryDAO: Hibernate implementation --> |
<bean id="categoryDAO" class="org.appfuse.dao.hibernate.CategoryDAOHibernate"> |
<property name="sessionFactory"><ref local="sessionFactory"/></property> |
</bean> |
|
}] |
|
After making these changes, you should be able to run __ant test-dao__. All of the tests should pass successfully. |
|
''__Yeah baby, Yeah!__'' |
%%(color: green) |
BUILD SUCCESSFUL\\ |
Total time: 12 seconds |
%% |
|
|