10 April 2007
Nhibernate Tutorial, Jumptree Forum Part 6 – ASP.NET, Generics and Map Relationships
Part I - Why I choose Nhiberneate
Part II - Spring.Net, Setup Nhibernate Support
Part 3 - Getting Ready
Part 4 - Setup and Add Category
Part 5 - Different Ways of Mapping File
** Open Source Jumptree Forum Based on these articles
V1.0
V1.1
Okay, assuming you
downloaded the source code already, let’s pick up where we
last left off.
In
Part 4 of the series,
we talked about how to set up NHibernate and we were able to add forum
categories. Let’s now move onto the rest of the forum.
First of all, let’s review the data model once again

It’s pretty easy to read, take a look at the “many-to-many”
relationship from “JumptreeForum_Discussions” to “JumptreeForum_Categoreis”, one thing I forgot to mention is that
- the “one-to-many”
relationship from “JumptreeForum_Discussions” to “JumptreeForum_DiscussionCategories”
has a referential integrity cascade upon delete
- the “one-to-many”
relationship from “JumptreeForum_Categories” to “JumptreeForum_DiscussionCategories” also
has the same thing
The reason is simple. If I delete a discussion, then I want
to delete all its association with categories. If I delete a category, then I
want to delete its association with all the discussions as well.
Looking at the model, let’s see how I implemented the “Jumptree_Discussion”
table in NHibernate.
First create an entity class that maps each column of
the table to a property of a class. Here is my “Jumptree_Discussion.cs” class.
.cf { font-family: Courier New; font-size: 10pt; color: black; background: white; border-top: windowtext 1pt solid; padding-top: 0pt; border-left: windowtext 1pt solid; padding-left: 0pt; border-right: windowtext 1pt solid; padding-right: 0pt; border-bottom: windowtext 1pt solid; padding-bottom: 0pt; }.cl { margin: 0px; }.cln { color: #2b91af; }.cb1 { color: blue; }.cb2 { color: #2b91af; }.cb3 { color: green; }
using System;
using
System.Collections;
using
System.Collections.Generic;
using
System.Text;
namespace
Jumptree.Forum.BusinessEntities
{
public partial class JumptreeForum_Discussions
{
public
JumptreeForum_Discussions()
{
}
public
JumptreeForum_Discussions(System.String
createdBy, System.String createdByIP, Nullable<System.DateTime>
createdOn, System.String
discussionDefaultComment, System.Int32
discussionID, Nullable<System.DateTime> discussionLastPostedOn, System.String discussionTitle, System.String updatedBy, Nullable<System.DateTime> updatedOn)
{
this.createdByField
= createdBy;
this.createdByIPField
= createdByIP;
this.createdOnField
= createdOn;
this.discussionDefaultCommentField
= discussionDefaultComment;
this.discussionIDField
= discussionID;
this.discussionLastPostedOnField
= discussionLastPostedOn;
this.discussionTitleField
= discussionTitle;
this.updatedByField
= updatedBy;
this.updatedOnField
= updatedOn;
}
private
System.String createdByField;
public
System.String CreatedBy
{
get
{ return this.createdByField;
}
set
{ this.createdByField = value; }
}
private
System.String createdByIPField;
public
System.String CreatedByIP
{
get
{ return this.createdByIPField;
}
set
{ this.createdByIPField = value; }
}
private
Nullable<System.DateTime>
createdOnField;
public Nullable<System.DateTime>
CreatedOn
{
get
{ return this.createdOnField;
}
set
{ this.createdOnField = value; }
}
private
System.String discussionDefaultCommentField;
public
System.String DiscussionDefaultComment
{
get
{ return this.discussionDefaultCommentField;
}
set
{ this.discussionDefaultCommentField = value; }
}
private
System.Int32 discussionIDField;
public
System.Int32 DiscussionID
{
get
{ return this.discussionIDField;
}
set
{ this.discussionIDField = value; }
}
private
Nullable<System.DateTime>
discussionLastPostedOnField;
public Nullable<System.DateTime>
DiscussionLastPostedOn
{
get
{ return this.discussionLastPostedOnField;
}
set
{ this.discussionLastPostedOnField = value; }
}
private
System.String discussionTitleField;
public
System.String DiscussionTitle
{
get
{ return this.discussionTitleField;
}
set
{ this.discussionTitleField = value; }
}
private
System.String updatedByField;
public
System.String UpdatedBy
{
get
{ return this.updatedByField;
}
set
{ this.updatedByField = value; }
}
private
Nullable<System.DateTime>
updatedOnField;
public Nullable<System.DateTime>
UpdatedOn
{
get
{ return this.updatedOnField;
}
set
{ this.updatedOnField = value; }
}
//private
IList<JumptreeForum_Categories> categories = new List<JumptreeForum_Categories>();
private
IList<JumptreeForum_Categories>
categories;
public IList<JumptreeForum_Categories>
Categories
{
get
{ return categories; }
set
{ categories = value; }
}
private
IList<JumptreeForum_DiscussionComments>
comments;
public IList<JumptreeForum_DiscussionComments>
Comments
{
get
{ return comments; }
set
{ comments = value; }
}
private
Nullable<int>
numberOfComments;
public Nullable<int>
NumberOfComments
{
get
{ return numberOfComments; }
set
{ numberOfComments = value; }
}
//private
IList categories;
//public
IList Categories
//{
// get { return categories; }
// set { categories = value; }
//}
}
}
Nothing speical here. I just added two Generics IList Property. One for categories and one for comments. Now let's talk a look at its .hbm mapping file.
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="Jumptree.Forum.BusinessEntities"
namespace="Jumptree.Forum.BusinessEntities"
>
<class
name="JumptreeForum_Discussions" lazy="false">
<id
name="DiscussionID">
<generator
class="native" />
</id>
<property
name="DiscussionTitle" />
<property
name="DiscussionLastPostedOn" />
<property
name="DiscussionDefaultComment" />
<property
name="CreatedBy" />
<property
name="CreatedByIP" />
<property
name="CreatedOn" />
<property
name="UpdatedBy" />
<property
name="UpdatedOn" />
<bag
name="Categories" table="JumptreeForum_DiscussionsCategories" lazy="true">
<key
column="DiscussionID" />
<many-to-many
class="JumptreeForum_Categories" column="CategoryID" />
</bag>
<bag name="Comments"
inverse="true"
order-by="CreatedOn"
lazy="true"
cascade="all-delete-orphan" >
<key
column="DiscussionID" />
<one-to-many
class="JumptreeForum_DiscussionComments" />
</bag>
</class>
</hibernate-mapping>
.cf { font-family: Courier New; font-size: 10pt; color: black; background: white; border-top: windowtext 1pt solid; padding-top: 0pt; border-left: windowtext 1pt solid; padding-left: 0pt; border-right: windowtext 1pt solid; padding-right: 0pt; border-bottom: windowtext 1pt solid; padding-bottom: 0pt; }.cl { margin: 0px; }.cln { color: #2b91af; }.cb1 { color: blue; }.cb2 { color: #a31515; }.cb3 { color: red; }
As you can see, I mapped all its property, one property per
database column.
One important thing to note here is that my property names are EXACTLY the same
as the database column names. If you want to map the column names to a
different property name, you can actually specificy it by adding a “column”
attribute to each property tag with values equals to the name of the database columns
and change the value of “name” attribute to the name of a class property of
your own, but we won’t get into that for now.
In addition, from some online examples, you might see people
actually specify one addition attribute “type” with values such as “String”, “DateTime”.
I didn’t do that is because NHibernate will automatically discover their data types
for me and I’m happy with the default size it provides me as well. For those authors wo had “type” attribute, normally, they wanted finer control on the
default size of say a “String” to be varchar(16) and what not, but again we won’t
get into it here. (In documentation, there is a section that teach you exactly
how to do it)
Okay, let’s take a look at how we mapped the discussion to
the categories. As you can see from the
mapping file, I created a “bag” node with the name=”Categories”. What is this “Categories”
you ask? Well, it’s actually a property in my entity class. If you look at the
entity class, my “Categories” property is a “IList” of “JumptreeForum_Categories”
class, essentially, I want to retrieve a list of categories related to any
discussion. This is important to the reason why I used “bag”. From page 53 (Collection
Mapings) of the documentation, it mentioned
” A bag is an unordered, unindexed collection which may contain the same
element multiple times. The .NET collections framework lacks an IBag interface,
hence you have to emulate it with an IList.”
Pretty clear isn’t it? “Bag” it is, let’s move on to the attribute “table”.
Since it’s a “many-to-many” relationship between “discussion” and “categories”,
the association table between them is
what matters. This table is the link between “discussion” and “categories”, so
that’s why I specified table=”JumptreeForum_DiscussionCategories”.
The attribute “lazy=true” pretty much means, when I load a discussion, if I did
not ask for categories information, then don’t load the categories associated
with the discussion. This improves performance and such the word “lazy”. If you specified “lazy=false”, then when a discussion
is loaded, in the backend, NHibernate will retrieve its categories
automatically as well. It’s sort a waste as in our Forum, we don’t load
categories when we display discussions.
Anyway, let’s get inside the “bag” element, the first thing I specified is the “Key
Column”. Remember, we are in the discussion mapping file, so from the
discussion side, the key we mapped to the many-to-many table is our “DiscussionID”.
You can see in the above database schema as well. Enough said.
Next comes to the “many-to-many” tag.
The class attribute specify the entity class “JumptreeForum_Categories”
which we have a “many-to-many” relationship to and the column to make the
connection to the “Categories” table is the “CategoryID” column of the “JumptreeForum_Categoreis”
table.
That’s all there is to be said and configured about “Discussions” and “Categories”.
Next comes the discussion comments. First, you have to understand, “Discussions”
table only holds the “title” of a discussion. The content of the discussion IS
THE FIRST COMMENT of the discussion. Does that make sense? So essentially what I’m saying is when you
save a discussion, two things will happens
- A discussion
gets created in “JumptreeForum_Discussions” table. The title and what not
will be populated
- A new
comment is inserted into “JumptreeForum_DiscussionComments” with the content
of the “Discussion” along with the newly created “DiscussionID” to serve
as the very first post of the discussion.
Any reply comments to the discussion later on will be
inserted into “JumptreeForum_DiscussonComments” as well. It’s straightforward
enough right? One discussion can have many replies, such the “One-To-Many”
relationship.
Here is the “DiscussionComments” entity class.
using System;
using
System.Collections.Generic;
using
System.Text;
namespace
Jumptree.Forum.BusinessEntities
{
public partial class JumptreeForum_DiscussionComments
{
public
JumptreeForum_DiscussionComments()
{
}
public
JumptreeForum_DiscussionComments(System.String
createdBy, System.String createdByIP, Nullable<System.DateTime>
createdOn, System.String discussionComment,
System.Int32 discussionCommentID, Nullable<System.Int32>
discussionID, System.String updatedBy, Nullable<System.DateTime>
updatedOn)
{
this.createdByField
= createdBy;
this.createdByIPField
= createdByIP;
this.createdOnField
= createdOn;
this.discussionCommentField
= discussionComment;
this.discussionCommentIDField
= discussionCommentID;
this.discussionIDField
= discussionID;
this.updatedByField
= updatedBy;
this.updatedOnField
= updatedOn;
}
private
System.String createdByField;
public
System.String CreatedBy
{
get
{ return this.createdByField;
}
set
{ this.createdByField = value; }
}
private
System.String createdByIPField;
public
System.String CreatedByIP
{
get
{ return this.createdByIPField;
}
set
{ this.createdByIPField = value; }
}
private
Nullable<System.DateTime>
createdOnField;
public Nullable<System.DateTime>
CreatedOn
{
get
{ return this.createdOnField;
}
set
{ this.createdOnField = value; }
}
private
System.String discussionCommentField;
public
System.String DiscussionComment
{
get
{ return this.discussionCommentField;
}
set
{ this.discussionCommentField = value; }
}
private
System.Int32 discussionCommentIDField;
public
System.Int32 DiscussionCommentID
{
get
{ return this.discussionCommentIDField;
}
set
{ this.discussionCommentIDField = value; }
}
private
Nullable<System.Int32>
discussionIDField;
public Nullable<System.Int32>
DiscussionID
{
get { return this.discussionIDField;
}
set
{ this.discussionIDField = value; }
}
private
System.String updatedByField;
public
System.String UpdatedBy
{
get
{ return this.updatedByField;
}
set
{ this.updatedByField = value; }
}
private
Nullable<System.DateTime>
updatedOnField;
public Nullable<System.DateTime>
UpdatedOn
{
get
{ return this.updatedOnField;
}
set
{ this.updatedOnField = value; }
}
private
JumptreeForum_Discussions discussion;
public JumptreeForum_Discussions Discussion
{
get
{ return discussion; }
set
{ discussion = value; }
}
}
}
Here is discussioncomments's mapping file
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="Jumptree.Forum.BusinessEntities"
namespace="Jumptree.Forum.BusinessEntities"
>
<class
name="JumptreeForum_DiscussionComments" lazy="false">
<id
name="DiscussionCommentID">
<generator
class="native" />
</id>
<property
name="DiscussionComment" />
<property
name="CreatedBy" />
<property
name="CreatedByIP" />
<property
name="CreatedOn" />
<property
name="UpdatedBy" />
<property
name="UpdatedOn" />
<many-to-one
name="Discussion" column="DiscussionID" not-null="true" />
</class>
</hibernate-mapping>
.cf { font-family: Courier New; font-size: 10pt; color: black; background: white; border-top: windowtext 1pt solid; padding-top: 0pt; border-left: windowtext 1pt solid; padding-left: 0pt; border-right: windowtext 1pt solid; padding-right: 0pt; border-bottom: windowtext 1pt solid; padding-bottom: 0pt; }.cl { margin: 0px; }.cln { color: #2b91af; }.cb1 { color: blue; }.cb2 { color: #2b91af; }
Okay, what's going on here?
First take a look at this page on the Hibernate http://www.hibernate.org/209.html
"In the Basic Collection pattern the mapping works like this:
1.
The parent maps the set containing the children to the appropriate
child table as a one-to-many relationship, e.g. one parent to many
children.
2. The parent marks the relationship as "inverse".
This attribute says that the parent doesn't actually update the
relationship; the child updates the relationship. We do this so that we
can deal with "NOT NULL" constraints on the child side.
"
For
1), it's pretty straight forward. In the discussion mapping file, you
can see I created a <bag> node with a "one-to-many" relationship
to the "JumptreeForum_DiscussionComments" class.
For 2).
Essential, it means without "inverse=true", when you save the
discussion along with its comments, discussion will be saved fine, but
its comments will be inserted without a "DiscussionID". By having
"inverse=true", you are telling NHibernate that make sure
"DiscussionID" is inserted along with the comments. That's all there is
to it.
If you have downloaded my source code, then please
take a look at "new.aspx", in my "Post_ServerClick", I added the
discussion like so
protected void Post_ServerClick(object
sender, EventArgs e)
{
IList<JumptreeForum_Categories> categoriesList = new List<JumptreeForum_Categories>();
IList<JumptreeForum_DiscussionComments> comments = new List<JumptreeForum_DiscussionComments>();
String
subject_txt = Subject.Value;
String
name_txt = Name_TextBox.Value;
String
content = Server.HtmlEncode(Comment.Value);
JumptreeForum_Discussions
discussion = new JumptreeForum_Discussions();
discussion.DiscussionTitle = subject_txt;
discussion.CreatedBy = name_txt;
discussion.CreatedByIP =
Request.UserHostAddress;
discussion.CreatedOn = System.DateTime.Now;
discussion.UpdatedBy =
discussion.CreatedBy;
discussion.UpdatedOn = discussion.CreatedOn;
/*
* This binding is used if
you want to allow multiple categories per dicussion
*/
/*
foreach (RepeaterItem item
in CategoryRepeater.Items)
{
CheckBox category_ck =
(CheckBox)item.FindControl("CategoryCheckBox");
HiddenField
categoryCheckBox_hf =
(HiddenField)item.FindControl("CategoryCheckBoxHidden");
if (category_ck.Checked)
{
JumptreeForum_Categories
category = new JumptreeForum_Categories();
category.CategoryID
= Convert.ToInt32(categoryCheckBox_hf.Value);
category.UpdatedBy =
"liming";
category.UpdatedOn =
System.DateTime.Now;
categoriesList.Add(category);
}
}
*/
/* This is
used to attached one cateogory per dicussion */
if
(Category.SelectedValue != "-1")
{
JumptreeForum_Categories
category = new JumptreeForum_Categories();
category.CategoryID = Convert.ToInt32(Category.SelectedValue);
category.UpdatedBy = "Liming Xu";
category.UpdatedOn = System.DateTime.Now;
categoriesList.Add(category);
}
discussion.Categories = categoriesList;
JumptreeForum_DiscussionComments
comment = new JumptreeForum_DiscussionComments();
comment.Discussion = discussion; //important
to add two way
comment.DiscussionComment = content;
comment.CreatedOn =
discussion.CreatedOn;
comment.CreatedBy =
discussion.CreatedBy;
comment.CreatedByIP =
discussion.CreatedByIP;
comment.UpdatedBy =
discussion.UpdatedBy;
comment.UpdatedOn = discussion.UpdatedOn;
comments.Add(comment);
discussion.Comments = comments; //important to
add two way
ISession
session = ExampleApplication.GetCurrentSession();
ITransaction
tx = session.BeginTransaction();
session.FlushMode = FlushMode.Commit;
/* comment
out this area because the new one-to-many, many-to-one had a null in pareint id
of child
According to document,
relationship should be saved via child
*/
//session.Save(comment);
session.Save(discussion);
tx.Commit();
Response.Redirect("index.aspx");
}
The only important code here is that you have to make sure,
you add "discussion" to "comment" first, so that comment knows about
its relationship with "discussion" and then add the comments back to
discussion, so that discussion knows it should save comments. This part is actually REALLY tricky, I'll have to dedicate a whole post later and explain this in a bit more detail.. hope it's not too confusing.
Okay,
I think I'm done for tonight. I think I'm too wordy and not good at
explaining things. If you
download the source code, then I'm sure
without reading this tutorial, you can figure it out.
Comment Policy: No HTML allowed. URIs and line breaks are converted automatically. Your e–mail address will not show up on any public page.