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 Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

# Liming said:

Jobz,

Thanks for the kind words. It's definitly encouraging and I hope my learning experience can help others who want to learn as well.

NHibernate is just really confusing and hard to pick up.

Liming

14 April 07 at 2:18 PM

Leave a Comment

Comment Policy: No HTML allowed. URIs and line breaks are converted automatically. Your e–mail address will not show up on any public page.

(required) 
(optional)
(required)