Castle Demo AppMany To Many Relations

time to read 57 min | 11301 words

We have users and we have bugs, but it is still not enough to create a good application, we need to group the bugs to projects, and we need to be able to assign users to projects. So, without further ado, let’s write the Project class.

What do we need to have in a project? The project name, the default assignee (remember that all bugs are assigned always.), the bugs in the project and the users working on it. With that said, let’s start with building a simple Project class.

[ActiveRecord("Projects")]
public class Project : ActiveRecordBase<Project>

We define the class as usual, with an [ActiveRecord] attribute pointing to the Projects table and inheriting from ActiveRecordBase<T>.

    [PrimaryKey(PrimaryKeyType.Identity,

     Access = PropertyAccess.NosetterCamelcaseUnderscore)]

    public int Id

    {

        get { return _id; }

    }

 

    [Property]

    public string Name

    {

        get { return _name; }

        set { _name = value; }

    }

The Id and Name declarations we are already familiar with, so let’s move to the more interesting stuff. A project has a relation of one to many to bugs. We’ve already seen how we create one to many connections when we added the AssignedBugs and OpenBugs to the User class.

    [HasMany(typeof(Bug), ColumnKey = "Project", RelationType = RelationType.Set,

        CustomAccess = "NHibernate.Generics.GenericAccessor, NHibernate.Generics",

        Lazy = true)]

    public EntitySet<Bug> Bugs

    {

        get { return _bugs; }

        set { _bugs = value; }

    }

 

    [HasMany(typeof(Bug), ColumnKey = "Project", RelationType = RelationType.Set,

        CustomAccess = "NHibernate.Generics.GenericAccessor, NHibernate.Generics",

        Where = "Status = 1", Lazy = true)]

    public EntitySet<Bug> OpenBugs

    {

        get { return _openBugs; }

        set { _openBugs = value; }

    }

The only difference we have is that the ColumnKey is set to the Project column (which currently doesn’t exist, we will create it in a couple of minutes.) We define another relation to the default assignee, like this:

    [BelongsTo]

    public User DefaultAssignee

    {

        get { return _defaultAssignee.Value; }

        set { _defaultAssignee.Value = value; }

    }

All the relations between the objects are using EntityRef<T> and EntitySet<T>, which are initialized in the constructor:

    public Project()

    {

        _bugs = new EntitySet<Bug>(

            delegate(Bug bug) { bug.Project = this; },

            delegate(Bug bug) { bug.Project = null; }

            );

        _openBugs = new EntitySet<Bug>();

        _defaultAssignee = new EntityRef<User>();

    }

Note that the only collection with behavior is the _bugs collection, _OpenBugs and _defaultAssignee don’t have actions associated with them (since we don’t care about add/removing to the OpenBugs collection, and there is no reverse collection from the Default Assignee to the project. Now we only need to make the modifications to the Bug class.
First, we add an EntityRef<Project> variable, and initialize it in the constructor, notice that we pass two methods that will be executed when a project is set / replaced.

    _project = new EntityRef<Project>(AddBugToProject, RemoveBugFromProject);

Those two methods are similar to the ones we defined for Users re-assignment:

    private void AddBugToProject(Project project)

    {

        project.Bugs.Add(this);

        if (Status == BugStatus.Opened)

            project.OpenBugs.Add(this);

    }

 

    private void RemoveBugFromProject(Project project)

    {

        project.Bugs.Remove(this);

        if (Status == BugStatus.Opened)

            project.OpenBugs.Remove(this);

    }

As you recall, we would like to keep the objects and the database in sync, even across smart actions, so we make sure that status changes to the bug update the OpenBugs collection in the assigned user and in the parent project, since I started to get quite a bit of code in the property setter, I broke it into separate methods, here is how the Update Project Status method look like:

    private void UpdateProjectStatus(BugStatus value)

    {

        if (Project != null) // during database load

        {

            if (_status == BugStatus.Opened)

                Project.OpenBugs.Remove(this);

            if (value == BugStatus.Opened)

                Project.OpenBugs.Add(this);

        }

    }

Let’s just fix the database schema so that it would match what we did so far, we’ll start with the Projects table:

CREATE TABLE [dbo].[Projects](

      [Id] [int] IDENTITY(1,1) NOT NULL,

      [Name] [nvarchar](255) COLLATE Hebrew_CI_AS NOT NULL,

      [DefaultAssignee] [int] NOT NULL,

      CONSTRAINT [PK_Projects] PRIMARY KEY CLUSTERED

      (

            [Id] ASC

      )

)

Now we can update the Bugs table with the new relationship. Because we update an existing table, the process is a bit awkward:

ALTER TABLE [Bugs]

ADD [Project] INT NULL

GO

INSERT INTO Projects(Name, DefaultAssignee)

VALUES('Default Project', 1)

GO

UPDATE Bugs

SET Project = 1

GO

ALTER TABLE [Bugs]

ALTER COLUMN [Project] INT NOT NULL

GO

ALTER TABLE [dbo].[Bugs]  WITH CHECK ADD 

CONSTRAINT [FK_Bugs_Projects] FOREIGN KEY([Project])

REFERENCES [dbo].[Projects] ([Id])

GO

As a sanity check, I wrote the following test:

    [Test]

    public void ProjectBugs()

    {

        User dar = new User("Dar", "Fubar", "Dar@bar.com");

        dar.Save();

 

        Project project = new Project("Test Project", dar);

        project.Save();

 

        Bug bug = new Bug(project, "Nasty", "Figure it out!", dar);

        bug.Save();

 

        Assert.AreEqual(1, project.Bugs.Count);

        Assert.AreEqual(1, project.OpenBugs.Count);

 

        using (new SessionScope())

        {

            Project fromDb = Project.Find(project.Id);

 

            Assert.AreNotSame(project, fromDb);

 

            Assert.AreEqual(1, fromDb.Bugs.Count);

            Assert.AreEqual(1, fromDb.OpenBugs.Count);

        }

    }

As you can see, since the default mode of a bug is open, and since it is automatically added to the project on creation, we suddenly get an project bug as well as an open bug. The new SessionScope requires some explanation, though. The issue here is that Active Record is smart enough to realize that there is already an existing Project with the Id that I'm trying to load from the database, so what will happen if I won't use the sessionScope is that I'll get the same instance as I already has. This is very useful most of the time, but not when I'm testing that the instance I get from the database is valid. Because of this, I open a new SessionScope, and force Active Record to ignore the fact that it already has the object that I want in memory.

So far, we’ve gone over much of the things we’ve done before, it’s time we will try something new. I mentioned that we need to be able to assign users to projects. Users can belong to multiply projects, and a project has mutliply users assigned to it. This means that we need to use a many to many connection between Users and Projects, let's see how we implement that, shall we?

We'll start by adding a Users property to the Project class, as usual, the field will be of type EntitySet<User>, but the property type will be ICollection<User> (EntitySet and EntityRef are implementation details):

    [HasAndBelongsToMany(typeof(User),RelationType.Set,Table="UsersProjects",

        CustomAccess = "NHibernate.Generics.GenericAccessor, NHibernate.Generics",

        ColumnKey="Project",ColumnRef="`User`",Lazy=true)]

    public ICollection<User> Users

    {

        get { return _users; }

    }

Okay, a couple of new things here. First, we have the HasAndBelongToMany, which is the way Active Record define many to many relationships, then we have the table that connects the objects. We already know what CustomAccess mean (needed to support the EntitySet / EntityRef). We need to pay attention to ColumnKey and ColumnRef. The ColumnKey specify which column refers to the primary key of the current project, and the ColumnRef specify which column refers to the primary key of the associated User.

It's important to note that the User column in the ColumnRef is surrounded by backticks (the ` character, it's not dirt on your screen, I assure you.), we need it to tell Active Record to wrap the calls to User with quotes (in SQL Server case, it's sent as [User]).

Here is how we initialized the _users field:

    _users = new EntitySet<User>(

        delegate(User user) { user.Projects.Add(this); },

        delegate(User user) { user.Projects.Remove(this); },

        InitializeOnLazy.Always);

As usual, we pass delegates that handles the connection on the other side, but now we also pass a InitializeOnLazy.Always. Why do we need it? It's needed because one side of the connection must be responsible for persisting the connection. I decided that the side responsible for this would be the Project, so we need to add this value to tell the EntitySet to threat it a little differently.

We define the connection on the other side in much the same way. Here is how the Projects property on the User class looks:

    [HasAndBelongsToMany(typeof(Project), RelationType.Set, Table = "UsersProjects",

      CustomAccess = "NHibernate.Generics.GenericAccessor, NHibernate.Generics",

     ColumnKey = "`User`", ColumnRef = "Project", Lazy = true, Inverse=true)]

    public ICollection<Project> Projects

    {

        get { return _projects; }

    }

As you can see, we need to reverse the ColumnKey and ColumnRef values. We still use backticks for the User column, since it's reserved word in SQL Server. We also specify that the connection is the respnsibility of the other side by adding Inverse = true to the attribute. Initializing the _projects field is done as usual, and we don't pass any special values to it:

    _projects = new EntitySet<Project>(

        delegate(Project project) { project.Users.Add(this); },

        delegate(Project project) { project.Users.Remove(this); });

Now we can define the table UsersProjects, like this:

CREATE TABLE [dbo].[UsersProjects](

      [Id] [int] IDENTITY(1,1) NOT NULL,

      [Project] [int] NOT NULL,

      [User] [int] NOT NULL,

      CONSTRAINT [PK_UsersProjects] PRIMARY KEY CLUSTERED

      (

            [Id] ASC

      )

)

GO

ALTER TABLE [dbo].[UsersProjects]  WITH CHECK ADD 

CONSTRAINT [FK_UsersProjects_Projects] FOREIGN KEY([Project])

REFERENCES [dbo].[Projects] ([Id])

GO

ALTER TABLE [dbo].[UsersProjects]  WITH CHECK ADD 

CONSTRAINT [FK_UsersProjects_Users] FOREIGN KEY([User])

REFERENCES [dbo].[Users] ([Id])

GO

This is it, we have a many to many relationship between users and projects. Now we can test that everything works:

    [Test]

    public void ProjectsUsers()

    {

        User dar = new User("Dar", "Fubar", "Dar@bar.com");

        dar.Save();

 

        User foo = new User("Foo", "Bar", "Foo@bar.org");

        foo.Save();

 

        Project project = new Project("Test Project", dar);

        project.Save();

 

        project.Users.Add(foo);

 

        Assert.AreEqual(1, foo.Projects.Count);

        project.Save();

 

        using (new SessionScope())

        {

            Project fromDb = Project.Find(project.Id);

            Assert.AreNotSame(project, fromDb);

            Assert.AreEqual(1, fromDb.Users.Count);

 

            User userFromDb = User.Find(foo.Id);

            Assert.AreNotSame(foo, userFromDb);

            Assert.AreEqual(1, foo.Projects.Count);

        }

    }

This is quite a bit of code, but basically we create two users (the default assignee don't necessarily belongs to the project) add it to the project and save the project. It is the project state was changed. Remember that we decided that the responsability of the connection is on the Project, that is what it means. If we will save the user foo, nothing will happen in the database.

We then open a new SessionScope to avoid getting the same instance from Active Record caching, get the data from the database, and make sure that the connection is live on both ends.

So far, it's pretty simple. There is quite a bit more to Active Record,  from cascades to composite keys to life cycle and validations, all of which I'm not going to cover. What we have now are the basis of Active Record, which is more than enough to move forward. It's time we start writing some code that has some visible affects.

On my next post in this series... skimming the surface of MonoRail... stay tuned.

More posts in "Castle Demo App" series:

  1. (03 Mar 2006) ViewComponents, Security, Filters and Friends
  2. (01 Mar 2006) Code Update
  3. (01 Mar 2006) Queries and Foreign Keys
  4. (28 Feb 2006) Complex Interactions
  5. (25 Feb 2006) CRUD Operations on Projects
  6. (22 Feb 2006) Let There Be A New User
  7. (22 Feb 2006) Updating our Users
  8. (20 Feb 2006) Getting serious, the first real page
  9. (20 Feb 2006) MonoRail At A Glance
  10. (20 Feb 2006) The First MonoRail Page
  11. (19 Feb 2006) Many To Many Relations
  12. (19 Feb 2006) Lazy Loading and Scopes
  13. (17 Feb 2006) Active Record Relations
  14. (17 Feb 2006) Getting Started With Active Record