Fri, 07 Nov 2008

One-to-Many Relationships in Windows Azure Tables

A few people complained (some in email, at least one on Channel 9) that my blog didn’t allow people to leave comments.

Those of you who have followed this blog since PDC know that it’s built from scratch on Windows Azure, so I actually had to write some code to enable comments.  I thought I’d write a brief post describing how I did it.

Modeling Comments

Although tables in Windows Azure are non-relational, it is of course possible to model relationships on top of simple tables.  In the case of blog comments, the relationship is a one-to-many relationship (one blog post to many comments).

Today my blog posts are modeled as entities with a partition key of “which blog is this posted to” (in my case, always “smarx”) and a row key that is approximately DateTime.MaxValue.Ticks – PostedDateTime.Ticks (so blog posts come back in reverse-chronological order).

To enable efficient lookup of comments, I decided to model comments as entities with a partition key of “<blog post partition key> <blog post row key>”.  For the row key, I decided to use PostedDateTime.Ticks, so the comments will come back in chronological order.

The final model for my blog comments looks like this:


    public class BlogComment : TableStorageEntity
    {
        public string Author { get; set; }
        public string Url { get; set; }
        public string Body { get; set; }
        public DateTime Posted { get; set; }
        public override string PartitionKey { get; set; }
        public override string RowKey {get {return string.Format("{0:d10}", Posted.Ticks);} set {}}

        public BlogComment(string entryPartitionKey, string entryRowKey) : base()
        {
            PartitionKey = string.Format("{0} {1}", entryPartitionKey, entryRowKey);
        }

        public BlogComment()
        {
            Posted = DateTime.UtcNow;
        }
    }

Then I added a corresponding table to my data service context:

       public DataServiceQuery<BlogComment> BlogCommentTable
        {
            get { return CreateQuery<BlogComment>("BlogCommentTable"); }
        }

To make it easy to get the comments associated with a blog post, I first tried to create a public property on blog entries called Comments, but I soon found out that ADO.NET Data Services would want to serialize that (because it’s a public property), and I don’t want to actually store the value of that property in the table.

Instead, I created a method called GetComments() on my BlogEntry class, which looks like this:

        public IEnumerable<BlogComment> GetComments()
        {
            return from c in new BlogDataServiceContext().BlogCommentTable
                   where c.PartitionKey == string.Format("{0} {1}", PartitionKey, RowKey) select c;
        }

As you can see, my query specifies a partition key, which means that this query will be able to efficiently return all the comments associated with a particular blog post.

Handling Comments in the UI

Now that I figured out how to model the comments, I just needed to add the necessary ASP.NET MVC code to display comments and to handle input of comments.

Displaying comments was simple due to my handy GetComments() method.

Handling the form input was not challenging either.  I added a Comment action to my PostsController, and I added a form that posts data to it:

    <form method="post" action='<%= Url.Action("comment", new { id = ViewData.Model.Permalink }) %>'>
        <input name="entrypartitionkey" type="hidden" value='<%= ViewData.Model.PartitionKey %>' />
        <input name="entryrowkey" type="hidden" value='<%= ViewData.Model.RowKey %>' />
        <table class="comment">
            <tr>
                <th>Your name:</th>
                <td><%= Html.TextBox("comment.Author", null, new { size = 50 }) %></td>
            </tr>
            <tr>
                <th>Your URL<br />(will be linked from your name):</th>
                <td><%= Html.TextBox("comment.Url", null, new { size = 50 })%></td>
            </tr>
            <tr>
                <th>Your comment:</th>
                <td><%= Html.TextArea("comment.Body", null, 10, 60, null) %></td>
            </tr>
            <tr>
                <th />
                <td><input type="submit" value="Submit Comment" /></td>
            </tr>
        </table>
    </form>

Note that I’m smuggling the blog entry’s partition key and row key (needed to specify the partition key of the comment) in via hidden inputs on the form.

Here’s a trimmed down version of my Comment action (the real code validates the URL and substitutes “Anonymous” if no user name is specified):

        public ActionResult Comment(string id, [Bind] Models.BlogComment comment,
            string entrypartitionkey, string entryrowkey)
        {
            var svc = new Models.BlogDataServiceContext();
            comment.PartitionKey = string.Format("{0} {1}", entrypartitionkey, entryrowkey);
            svc.AddObject("BlogCommentTable", comment);
            svc.SaveChanges();
            return RedirectToAction("Show");
        }

Side note: I’m passing in the ID of the blog post to this action, even though it’s not actually needed.  (I have the partition and row keys already.)  The reason for this has to do with how I set up the routes on my blog.  In the code I shared (and what I used at PDC), I didn’t change the default {controller}/{action}/{id} route, but for this blog, I’ve used {controller}/{id}/{action}, which is a bit more RESTful.  However, that means that I can’t construct a link to create a comment without specifying some ID.

What Do You Think?

Now you can leave me a comment and let me know. :-)  Did this make sense?  Is there a better way for me to have modeled the data or handled it in ASP.NET MVC?