Creating an ORM in C# - Part 3

6/9/2009

Today is another post concerning the HaterAide ORM that I've been working on lately.  The last couple of posts have dealt with the reflection and class definition portions of the code. Today I'm going to start posting about the SQL portion of the ORM. Specifically I'm going to discuss how I've gone about creating a database and tables from the information that the class definitions give us.

The first item that I need to deal with is the fact that this implementation needs to be extensible. Different databases have slightly different implementations/key words. So I decided to go with a provider model. The code is rather straightforward and basic. As such I'm just going to have the code in the download at the bottom of the post, but the classes that are of interest are the ProviderCollection, ProviderConfigSection, and ProviderManager (and also the IProvider class). You'll also note that in the download, a lot of the classes from Part 1 and Part 2 of this series have been slightly modified.

There provider model allows me to plug in classes for each database that I want to deal with. At present though, all I care about is SQL Server.  Since that's the case I've also added an SQL Server provider (and a number of helper classes that are in various states of completion). Anyway, the provider manager starts up, creates any and all providers, and uses the one that we set as the default in the Web.Config file (once again, I'm only developing this for web apps so slight modifications would be needed to make this a bit more general purpose). It then calls the create function on the provider which creates the database.

If you look at the Create function in the Provider class:

   1: internal override void Create()
   2: {
   3:     string Database = Regex.Match(ConnectionString, "Initial Catalog=(.*?;)").Value.Replace("Initial Catalog=", "").Replace(";", "");
   4:     string TempConnectionString = Regex.Replace(ConnectionString, "Initial Catalog=(.*?;)", "");
   5:     SQLBuilder.CreateDatabase(TempConnectionString, Database, false);
   6:     foreach (Type Key in ClassManager.Keys)
   7:     {
   8:         SQLBuilder.CreateTable(ConnectionString, ClassManager[Key], ClassManager, false);
   9:     }
  10: }

You will see that the only things it does, is parses the connection string finding the name of the database that we're connecting to and calling two functions. One function that is called is the CreateDatabase function and the other being CreateTable.

The CreateDatabase function can be seen below:

   1: public static void CreateDatabase(string ConnectionString, string DatabaseName, bool RecreateIfExists)
   2: {
   3:     bool Exists = false;
   4:     Utilities.SQLHelper.SQLHelper Helper = new Utilities.SQLHelper.SQLHelper("SELECT * FROM Master.sys.Databases where name=@DatabaseID", ConnectionString, CommandType.Text);
   5:     if (!RecreateIfExists)
   6:     {
   7:         try
   8:         {
   9:             Helper.Open();
  10:             Helper.AddParameter("@DatabaseID", DatabaseName, 100);
  11:             Helper.ExecuteReader();
  12:             if (Helper.Read())
  13:             {
  14:                 Exists = true;
  15:             }
  16:         }
  17:         catch { }
  18:         finally { Helper.Close(); }
  19:     }
  20:     if (!Exists)
  21:     {
  22:         Helper.Command = "CREATE DATABASE " + DatabaseName.Replace("'", "");
  23:         try
  24:         {
  25:             Helper.Open();
  26:             Helper.ExecuteNonQuery();
  27:         }
  28:         catch { }
  29:         finally { Helper.Close(); }
  30:     }
  31: }

You'll notice that it uses the SQLHelper class from my utility library, but it's just to simplify connecting to the database. Anyway, the function simply does a call to the Master database asking what databases exist (and specifically if the database we want exists). If it doesn't exist, it creates the database. If it does exist, it skips that step. The CreateTable function is where the interesting parts occur:

   1: public static void CreateTable(string ConnectionString, Class Class, ClassManager Manager, bool RecreateIfExists)
   2: {
   3:     bool Exists = false;
   4:     string ClassName = Class.OriginalType.Name;
   5:     Utilities.SQLHelper.SQLHelper Helper = new Utilities.SQLHelper.SQLHelper("Select * from sys.Tables where name=@TableID", ConnectionString, CommandType.Text);
   6:     if (!RecreateIfExists)
   7:     {
   8:         try
   9:         {
  10:             Helper.Open();
  11:             Helper.AddParameter("@TableID", ClassName, 200);
  12:             Helper.ExecuteReader();
  13:             if (Helper.Read())
  14:             {
  15:                 Exists = true;
  16:             }
  17:         }
  18:         catch { }
  19:         finally { Helper.Close(); }
  20:     }
  21:     if (!Exists)
  22:     {
  23:         CreateTable Table = new CreateTable(Class, Manager);
  24:         Helper.Command = Table.ToString();
  25:         try
  26:         {
  27:             Helper.Open();
  28:             Helper.ExecuteNonQuery();
  29:         }
  30:         catch { }
  31:         finally { Helper.Close(); }
  32:     }
  33: }

The beginning portion of the function is very similar to the CreateDatabase function, simply asking if the table exists. After that it simply creates the table. However it does this by creating a CreateTable object:

   1: internal class CreateTable:IStatement
   2: {
   3:     public CreateTable(Class Class,ClassManager Manager)
   4:     {
   5:         _Class = Class;
   6:         foreach (Property TempProperty in Class.Properties)
   7:         {
   8:             if (TempProperty.Attribute.AttributeType == AttributeType.ID || TempProperty.Attribute.AttributeType == AttributeType.Reference)
   9:             {
  10:                 Columns.Add(GlobalFunctions.GetSQLType(TempProperty.Attribute, Class, Manager));
  11:             }
  12:         }
  13:     } 
  14:  
  15:     private List<IDataType> Columns = new List<IDataType>();
  16:     private Class _Class = null; 
  17:  
  18:     public override string ToString()
  19:     {
  20:         StringBuilder Builder = new StringBuilder("CREATE TABLE " + _Class.OriginalType.Name + "(");
  21:         StringBuilder ListBuilder = new StringBuilder();
  22:         string Splitter = "";
  23:         foreach (IDataType Column in Columns)
  24:         {
  25:             if (Column != null&&!Column.Type.Equals("List",StringComparison.CurrentCultureIgnoreCase))
  26:             {
  27:                 Builder.Append(Splitter + Column.ToString());
  28:                 Splitter = ",";
  29:             }
  30:             else if (Column != null && Column.Type.Equals("List", StringComparison.CurrentCultureIgnoreCase))
  31:             {
  32:                 ListBuilder.Append(" " + Column.ToString());
  33:             }
  34:         }
  35:         Builder.Append(")"); 
  36:  
  37:         return Builder.ToString() + ListBuilder.ToString();
  38:     }
  39: }

The CreateTable object is one of the many helper classes (in this case a statement class) that the system uses to actually form the SQL functions that it queries against the database. In this case the CreateTable constructor goes through each property within the class and (assuming it's an ID or Reference type since we're not dealing with classes yet) creates an IDataType class that it puts into a list. The IDataType class can be seen below:

   1: internal class IDataType
   2: {
   3:     public IDataType()
   4:     {
   5:     } 
   6:  
   7:     public IDataType(Attribute Attribute)
   8:     {
   9:         Name = Attribute.Name;
  10:         if (Attribute.AttributeType == AttributeType.ID)
  11:             Constraints.Add(new PrimaryKey());
  12:     } 
  13:  
  14:     public string Name { get; set; }
  15:     public List<IConstraint> Constraints = new List<IConstraint>(); 
  16:  
  17:     public string Type { get; set; } 
  18:  
  19:     public override string ToString()
  20:     {
  21:         StringBuilder Builder = new StringBuilder(Name + " " + Type);
  22:         foreach (IConstraint Constraint in Constraints)
  23:         {
  24:             Builder.Append(Constraint.ToString());
  25:         }
  26:         return Builder.ToString();
  27:     }
  28: }

The IDataType classes are created by a global function which uses the type of the property to determine which one to create. For instance a double would create a Float class, a List<int> would throw back a List class, and a bool would pass back a Bit class. These individual classes then look at the attributes of the property and determine what if any sort of constraints to place on the object. For instance, if this is our ID, it creates a PrimaryKey class and adds it to a constraints list and each different type may have its own individual constraints that it needs (for instance the NVarChar class uses the Size constraint). All of these are straightforward and rather uniform with one exception, the List class:

   1: internal class Int : IDataType
   2: {
   3:     public Int(Attribute Attribute)
   4:         : base(Attribute)
   5:     {
   6:         Type = "Int";
   7:         if (Attribute.AutoIncrement)
   8:             Constraints.Add(new AutoIncrement());
   9:     }
  10: } 
  11:  
  12: internal class List:IDataType
  13: {
  14:     public List(Attribute Attribute, Class Class, ClassManager Manager)
  15:         : base(Attribute)
  16:     {
  17:         _Class = Class;
  18:         Type = "List";
  19:         Name = Class.OriginalType.Name + "_" + Attribute.Name;
  20:         Type ObjectType = Attribute.Type.GetGenericArguments()[0];
  21:         Attribute TempAttribute=new Attribute();
  22:         TempAttribute.AttributeType=AttributeType.Reference;
  23:         TempAttribute.Length=Attribute.Length;
  24:         TempAttribute.Name=Attribute.Name;
  25:         TempAttribute.Type=ObjectType;
  26:         IDataType DataType = GlobalFunctions.GetSQLType(TempAttribute, Class, Manager);
  27:         if (DataType != null)
  28:         {
  29:             Columns.Add(DataType);
  30:         }
  31:         else
  32:         {
  33:             //Find matching class and ID
  34:         }
  35:         foreach (Property Property in Class.Properties)
  36:         {
  37:             if (Property.Attribute.AttributeType == AttributeType.ID)
  38:             {
  39:                 IDataType IDType = GlobalFunctions.GetSQLType(Property.Attribute, Class, Manager);
  40:                 IDName = IDType.Name;
  41:                 IDType.Constraints.Clear();
  42:                 if (IDType is NVarChar)
  43:                 {
  44:                     IDType.Constraints.Add(new Size(Property.Attribute.Length));
  45:                 }
  46:                 Columns.Add(IDType);
  47:             }
  48:         }
  49:     } 
  50:  
  51:     private string IDName = "";
  52:     private List<IDataType> Columns = new List<IDataType>();
  53:     private Class _Class; 
  54:  
  55:     public override string ToString()
  56:     {
  57:         StringBuilder Builder = new StringBuilder("CREATE TABLE " + Name + "("); 
  58:  
  59:         string Splitter = "";
  60:         string PrimaryKey="";
  61:         foreach (IDataType Column in Columns)
  62:         {
  63:             if (Column != null)
  64:             {
  65:                 PrimaryKey+=Splitter+Column.Name;
  66:                 Builder.Append(Splitter + Column.ToString());
  67:                 Splitter = ",";
  68:             }
  69:         }
  70:         Builder.Append(", PRIMARY KEY(" + PrimaryKey + ")");
  71:         Builder.Append(", FOREIGN KEY(" + IDName + ") REFERENCES " + _Class.OriginalType.Name + "(" + IDName + ")");
  72:         Builder.Append(")"); 
  73:  
  74:         return Builder.ToString();
  75:     }
  76: }

The top class is the Int class and as you can see, it's rather small. Most of the data type classes are this way but as you can see, the List class doesn't fall under that category. Whenever you have a list in a database, you can't exactly have it in the same table as the rest of the data without replecating all of the information for a row multiple times which isn't exactly desirable (or you can concat the data together, but it's usually more of a pain than it's worth).  As such you create a new table with the ID (or whatever information you need to join the two tables) and the list item.  As such the List class has to do this (create the secondary table).  In order to do this, it needs to figure out what the type within the list is, create the base type for that.  It then has to go back and find the ID for the class. This information is then used to create the table, set the primary/foreign keys, etc. You'll also notice that back in the CreateTable class that we also have to separate out the List items as they are separate commands from the main table. But that's it really. Everything else gets thrown into the main table. This in turn gets thrown back to the SQLBuilder class, which actually sends the commands to the database.

That's the basics at this point. We can create the tables and database for basic types. In the future I'll add in insertion, selection, and deletion. Once that's added, next come the more difficult types to deal with, namely other classes (ManyToMany, ManyToOne, and Map attribute types). But for now, take a look, leave feedback, and happy coding.



Comments