Introduction
There are many open source O/R mapping frameworks that have been appearing on .NET platform during the last year. Most of them are ugly and only a few of them are ready for real-world development. Among the open source solutions, NEO and NHibernate are at the top in the O/R mapping world. That is why we will compare them in this series of articles, face to face.
In this article we will introduce NEO Framework. I have very good experience with NEO, since we use it in TargetProcess project - ASP.NET based agile project management system. The project is quite mature and this is a real experience from a real production project.
Short Intro
NEO
NEO is a framework originally created for .NET. It is quite simple and quite featured to solve almost all the usual O/R mapping problems. Please visit the site: codehaus.
NHibernate
NHibernate is a .NET port of a very popular Java based Hibernate framework. There is a very clear trend these years to port all good tools from Java world to .NET. Hibernate is widely used and has been evolved into one of the most powerful O/R mapping solutions. Please visit: NHibernate.
NEO Framework
Usage of almost any O/R mapping tool is quite uniform with some slight deviations. These are the six steps to start using NEO:
- Create domain model (UML diagram, text description in your mind).
- Create domain descriptor in XML format for NEO.
- Generate C# classes and SQL queries for domain model.
- Include C# classes into project.
- Create the database and execute SQL queries.
- Use NEO Framework to handle persistence objects.
Creating mappings
Any O/R mapping tool works with domain model. Some of them are closer to database while others are closer to domain model. NEO is on the middle ground. You may create a database and generate domain model descriptor file or you may create domain model descriptor and then generate the database. I prefer the second way.
Let's take a very simple domain mode. This is a kind of very basic bug tracking system. You may add a project, add bugs, assign them to users and add comments to the bugs. Here is the class diagram:
Now we should translate this fancy UML picture to NEO language - model descriptor in XML format. We are starting with project class mapping. As you know, each class in the domain model has a table in a database (we'll not consider inheritance for now). So, all the objects of Project
class will be stored in a table projects.
="1.0" ="ISO-8859-1" ="no"
<!DOCTYPE database SYSTEM "Resources/norque.dtd">
<database name="tp" package="Neo.Web.Model"
defaultIdMethod="native" defaultJavaNamingMethod="underscore">
<table name="projects" javaName="Project">
. . .
</table>
</database>
Well, maybe you are slightly confused with the javaName
property used in C# based framework. But a rational description exists as you may have guessed. The fact is that NEO uses DTD of Torque O/R mapping tool that was written in Java and for Java.
We have mapped the class and now we should do the same for the class properties. Our Project
class has four properties:
Name
,
Description
,
StartDate
,
ProjectId
In the database, ProjectId
will be an auto incremented Primary Key column. Mapping for all the other properties is very simple.
<table name="projects" javaName="Project">
<column name="project_id" primaryKey="true"
autoIncrement="true" required="true" type="INTEGER" />
<column name="name" required="true"
type="VARCHAR" size="150" />
<column name="description" required="false"
type="VARCHAR" size="600" />
<column name="start_date" required="true"
type="DATE" />
</table>
Let's skip mapping of User
class since it is almost the same and look right into the Bug
class. According to the Domain model, each project has a collection of Bugs. This is a one-to-many relation and in a relational database realm, represented by a simple foreign key.
<table name="bugs" javaName="Bug">
<column name="bug_id" primaryKey="true" autoIncrement="true"
required="true" type="INTEGER" />
<column name="name" required="true" type="VARCHAR" size="250" />
<column name="description" required="false"
type="VARCHAR" size="2500" />
<column name="effort" required="false" type="DECIMAL" />
<column name="status" required="true" type="INTEGER" />
<column name="project_id" required="true" type="INTEGER" />
<foreign-key foreignTable="projects" name="Project"
onDelete="cascade">
<reference local="project_id" foreign="project_id" />
</foreign-key>
</table>
All the Bug
objects are stored in bugs table, bug_id
is the primary key and project_id
is the foreign key in projects table.
<foreign-key foreignTable="projects" name="Project" onDelete="cascade">
Name
attribute sets the name of relation. In this case each Bug
object will have a reference to Project
object and in the code you will be able to write something like this:
String projectName = bug.Project.Name;
<reference local="project_id" foreign="project_id" />
Reference
sets columns for foreign key. In this case project_id
column should exist in bugs table and in projects table.
If we want to have collection of bugs in a Project
object, we should add information about bugs into the Project
class definition:
<table name="projects" javaName="Project">
. . .
<iforeign-key foreignTable="bugs" name="Bugs" onDelete="cascade">
<ireference local="project_id" foreign="project_id" />
</iforeign-key>
</table>
public ICollection GetBugs()
{
return new BugList(this.Bugs);
}
Finally, the full mapping will look like this:
="1.0" ="ISO-8859-1" ="no"
<!DOCTYPE database SYSTEM "Resources/norque.dtd">
<database name="tp" package="Neo.Web.Model" defaultIdMethod="native"
defaultJavaNamingMethod="underscore">
<table name="projects" javaName="Project">
<column name="project_id" primaryKey="true" autoIncrement="true"
required="true" type="INTEGER" />
<column name="name" required="true" type="VARCHAR" size="150" />
<column name="description" required="false" type="VARCHAR"
size="600" />
<column name="start_date" required="true" type="DATE" />
<iforeign-key foreignTable="bugs" name="Bugs" onDelete="cascade">
<ireference local="project_id" foreign="project_id" />
</iforeign-key>
</table>
<table name="users" javaName="User">
<column name="user_id" primaryKey="true" autoIncrement="true"
required="true" type="INTEGER" />
<column name="name" required="true" type="VARCHAR" size="150" />
<column name="login" required="true" type="VARCHAR" size="50" />
<column name="password" required="true" type="VARCHAR"
size="50" />
</table>
<table name="bugs" javaName="Bug">
<column name="bug_id" primaryKey="true" autoIncrement="true"
required="true" type="INTEGER" />
<column name="name" required="true" type="VARCHAR" size="250" />
<column name="description" required="false" type="VARCHAR"
size="2500" />
<column name="effort" required="false" type="DECIMAL" />
<column name="status" required="true" type="INTEGER" />
<column name="project_id" required="true" type="INTEGER" />
<column name="user_id" required="true" type="INTEGER" />
<foreign-key foreignTable="projects" name="Project"
onDelete="cascade">
<reference local="project_id" foreign="project_id" />
</foreign-key>
<foreign-key foreignTable="users" name="Owner">
<reference local="user_id" foreign="user_id" />
</foreign-key>
<iforeign-key foreignTable="comments" name="Comments"
onDelete="cascade">
<ireference local="bug_id" foreign="bug_id" />
</iforeign-key>
</table>
<table name="comments" javaName="Comment">
<column name="comment_id" primaryKey="true" autoIncrement="true"
required="true" type="INTEGER" />
<column name="text" required="false" type="VARCHAR" size="1000" />
<column name="date" required="true" type="DATE" />
<column name="user_id" required="true" type="INTEGER" />
<column name="bug_id" required="true" type="INTEGER" />
<foreign-key foreignTable="users" name="User">
<reference local="user_id" foreign="user_id" />
</foreign-key>
<foreign-key foreignTable="bugs" name="Bug" onDelete="cascade">
<reference local="bug_id" foreign="bug_id" />
</foreign-key>
</table>
</database>
Generating model classes
Now we have all the four classes mapped and ready to generate SQL and �# code for them. NEO has a code generation tool neo.exe that can be found in the [neo root folder]\src\Tools\CmdLineTool folder. It is convenient to create BAT file with all the required commands.
neo.exe -r Resources -f -sql true -sqldrop true -o Sql model.xml
neo.exe -r Resources -user true model.xml
neo.exe -r Resources -support true -o Base model.xml
The first command generates SQL queries and puts them into Sql folder. Using these queries you may create database for sample application. The second command generates model classes that can be modified by the user (will not be regenerated by NEO in future). The third command creates base model classes that will be regenerated by NEO when the model is changed. Finally, we will have the structure as shown in the picture below:
After BAT file execution, we should include new classes into our project and change the namespaces.
Now we should create a database and execute the generated model.sql script from the Sql folder.
Connecting to database
Let's do the last part of preparations - connecting to database. First of all, create DataStoreFactory
class that will create database connections. There will be two parameters in Web.config.
<add key="ConnectionString"
value="data source=(local);initial catalog=ormapping;uid=sa;pwd=" />
<add key="DatabaseServer" value="Neo.SqlClient.SqlDataStore,Neo" />
DataStoreFactory
has just one Create
method:
public IDataStore Create()
{
string className =
ConfigurationSettings.AppSettings["DatabaseServer"];
string conn =
ConfigurationSettings.AppSettings["ConnectionString"];
return (IDataStore)Activator.CreateInstance(
Type.GetType(className + ",Neo"),new object[] {conn});
}
It's time to have fun with NEO framework. The first application's page will add a new project and show the project's list. Oh, we did not finish the connection operation. OK, that's easy. Just instantiate IDataStore
and create an instance of ObjectContext
. Actually, ObjecyContext
is very interesting and it is a unit of work pattern. It stores all the changes of loaded objects and is mandatory for all operations with NEO.
DataStoreFactory store = new DataStoreFactory();
IDataStore dataStore = store.Create();
context = new ObjectContext(dataStore);
Persistence
There is a web form on the page, as usual, with several text boxes and a button. When the user fills in the form and pushes the button, a new project is added into the database. The following event handler will do all the actions:
private void btnAddProject_Click(object sender, System.EventArgs e)
{
ProjectFactory projectFactory = new ProjectFactory(context);
Project project = projectFactory.CreateObject();
project.Name = txtName.Text;
project.Description = txtDescription.Text;
project.StartDate = Convert.ToDateTime(txtStartDate.Text);
context.SaveChanges();
}
First the ProjectFactory
class is instantiated. This class is responsible for all operations with the projects: create, retrieve, delete and so on. Then we create a new project. Note that this new project is not there in the database right now. In the next step we set all the project properties from the form's fields. And, finally, we save all the changes in ObjectContext
into the database. In our case, NEO will generate the INSERT
query.
Now it is required to get all the projects from the database and show them in a list. The code in Page_Load
handler is very simple:
private void Page_Load(object sender, System.EventArgs e)
{
DataStoreFactory store = new DataStoreFactory();
IDataStore dataStore = store.Create();
context = new ObjectContext(dataStore);
ProjectList projects = new ProjectFactory(context).FindAllObjects();
lstProjects.DataSource = projects;
lstProjects.DataBind();
}
Again, we use ProjectFactory
to retrieve all the projects from the database and use the list as a DataSource
for lstProjects
DataGrid. That's it.
Conclusion
Do you remember the time when dynamic SQL or stored procedures were used for database operations? NEO or any other good O/R mapping solution makes life easier and code lighter. A lot!
That was just a quick start with NEO. In the next article we will check more complex topics, like complex queries, possible architectural solutions for ObjectContext
handling, detached database and TDD with NEO, inheritance problem and so on.