Creating an ORM in C# - Part 7

7/9/2009

Part 1 - Defining our classes
Part 2 - Reflection and class analysis
Part 3 - Creating our database and tables
Part 4 - Creating our stored procedures
Part 5 - Class mappings
Part 6 - Many to Many and Many to One mappings

If you've yet to read the previous portions of this series, this one is just going to be confusing. So read the previous ones. Anyway, this is the final entry in the series. Today I cover select statements and also the cache. In other words, this is a long one (potentially). Anyway, let's start with a small recap.

HaterAide was started to handle my ORM needs since I wasn't happy with most of the options out there. The system uses a base class, reflection, and generics to map a class to a set of tables in a database. Database and table where covered along with the creation/use of select, insert, delete, and update stored procedures. In other words our basic functionality is covered at this point with one exception. The only select statement that the code covers is when you know the ID of the object that you want. Since that's almost never the case, let's look at how to go about getting something a bit more complex.

If you look at the various ORMs out there, most of them out there require you to either write SQL or their own query language to get anything remotely complex out of them. And since I'm trying to hide as much of the back end as possible, I decided to ignore this route. Instead I came up with a set of objects to help me out with queries.

   1: public class And<T> : IClause<T>
   2: {
   3:     public And(IClause<T> WhereStatement1, IClause<T> WhereStatement2)
   4:         : base()
   5:     {
   6:         _WhereStatement1 = WhereStatement1;
   7:         _WhereStatement2 = WhereStatement2;
   8:     }
   9:  
  10:     protected IClause<T> _WhereStatement1 = null;
  11:     protected IClause<T> _WhereStatement2 = null;
  12:  
  13:     internal override void SetupSQL(SQLHelper Helper,Class Class)
  14:     {
  15:         _WhereStatement1.SetupSQL(Helper,Class);
  16:         _WhereStatement2.SetupSQL(Helper,Class);
  17:     }
  18:  
  19:     public override string ToString()
  20:     {
  21:         StringBuilder Builder = new StringBuilder(_ColumnName);
  22:         Builder.Append("(" + _WhereStatement1.ToString());
  23:         Builder.Append(" AND ");
  24:         Builder.Append(_WhereStatement2.ToString() + ")");
  25:         return Builder.ToString();
  26:     }
  27: } 
  28:  
  29: public class Or<T> : IClause<T>
  30: {
  31:     public Or(IClause<T> WhereStatement1, IClause<T> WhereStatement2)
  32:         : base()
  33:     {
  34:         _WhereStatement1 = WhereStatement1;
  35:         _WhereStatement2 = WhereStatement2;
  36:     }
  37:  
  38:     protected IClause<T> _WhereStatement1 = null;
  39:     protected IClause<T> _WhereStatement2 = null;
  40:  
  41:     internal override void SetupSQL(SQLHelper Helper,Class Class)
  42:     {
  43:         _WhereStatement1.SetupSQL(Helper,Class);
  44:         _WhereStatement2.SetupSQL(Helper,Class);
  45:     }
  46:  
  47:     public override string ToString()
  48:     {
  49:         StringBuilder Builder = new StringBuilder(_ColumnName);
  50:         Builder.Append("(" + _WhereStatement1.ToString());
  51:         Builder.Append(" OR ");
  52:         Builder.Append(_WhereStatement2.ToString() + ")");
  53:         return Builder.ToString();
  54:     }
  55: } 
  56:  
  57: public class Where<T> : IClause<T>
  58: {
  59:     public Where(Expression<Func<T, object>> Expression, HaterAide.SQL.Enums.Operators Constraint, object Value)
  60:         : base(Expression, Constraint, Value)
  61:     {
  62:  
  63:     }
  64:  
  65:     public Where(Expression<Func<T, object>> Expression, HaterAide.SQL.Enums.Operators Constraint, object Value1, object Value2)
  66:         : base(Expression, Constraint, Value1)
  67:     {
  68:         _Value2 = Value2;
  69:     }
  70:  
  71:     protected object _Value2 = null;
  72:  
  73:     internal override void SetupSQL(SQLHelper Helper,Class Class)
  74:     {
  75:         if (_ColumnType.FullName.Equals("System.String", StringComparison.CurrentCultureIgnoreCase))
  76:         {
  77:             if (_Constraint == Enums.Operators.Between)
  78:             {
  79:                 Helper.AddParameter("@" + _ColumnName + "1", (string)_Value, 255);
  80:                 Helper.AddParameter("@" + _ColumnName + "2", (string)_Value2, 255);
  81:             }
  82:             else
  83:             {
  84:                 Helper.AddParameter("@" + _ColumnName, (string)_Value, 255);
  85:             }
  86:         }
  87:         else
  88:         {
  89:             if (_Constraint == Enums.Operators.Between)
  90:             {
  91:                 Helper.AddParameter("@" + _ColumnName + "1", _Value, GlobalFunctions.GetSQLType(_ColumnType));
  92:                 Helper.AddParameter("@" + _ColumnName + "2", _Value2, GlobalFunctions.GetSQLType(_ColumnType));
  93:             }
  94:             else
  95:             {
  96:                 Helper.AddParameter("@" + _ColumnName, _Value, GlobalFunctions.GetSQLType(_ColumnType));
  97:             }
  98:         }
  99:     }
 100:  
 101:     public override string ToString()
 102:     {
 103:         StringBuilder Builder=new StringBuilder(_ColumnName);
 104:         if (_Constraint == Enums.Operators.Equals)
 105:         {
 106:             Builder.Append("=");
 107:         }
 108:         else if (_Constraint == Enums.Operators.GreaterThan)
 109:         {
 110:             Builder.Append(">");
 111:         }
 112:         else if (_Constraint == Enums.Operators.GreaterThanOrEqual)
 113:         {
 114:             Builder.Append(">=");
 115:         }
 116:         else if (_Constraint == Enums.Operators.LessThan)
 117:         {
 118:             Builder.Append("<");
 119:         }
 120:         else if (_Constraint == Enums.Operators.LessThanOrEqual)
 121:         {
 122:             Builder.Append("<=");
 123:         }
 124:         else if (_Constraint == Enums.Operators.Like)
 125:         {
 126:             Builder.Append(" LIKE ");
 127:         }
 128:         else if (_Constraint == Enums.Operators.NotEqual)
 129:         {
 130:             Builder.Append("<>");
 131:         }
 132:  
 133:         if (_Constraint == Enums.Operators.Between)
 134:         {
 135:             Builder.Append(" BETWEEN ");
 136:             Builder.Append("@" + _ColumnName + "1");
 137:             Builder.Append(" AND ");
 138:             Builder.Append("@" + _ColumnName + "2");
 139:         }
 140:         else
 141:         {
 142:             Builder.Append("@" + _ColumnName);
 143:         }
 144:         return Builder.ToString();
 145:     }
 146: }

There are a couple of enums as well but these classes are all that's really needed for a simple query. The where clause takes in an expression just like the mapping class, an Operator (equals, less than, like, etc.), and a value to compare it to (in the case of between, it takes two values). These can then be joined together with ANDs and ORs to form more complex queries. So you want to find the brown widget that has the blue handle? No problem. What about ordering the items? That's where this class comes in.

   1: public class OrderBy<T> : IClause<T>
   2: {
   3:     public OrderBy(Expression<Func<T, object>> Expression, Direction Direction, IClause<T> NestedOrderBy)
   4:         : base(Expression, Enums.Operators.Equals, null)
   5:     {
   6:         this._NestedOrderBy = NestedOrderBy;
   7:         this._Direction = Direction;
   8:     }
   9:  
  10:     protected IClause<T> _NestedOrderBy;
  11:     protected Direction _Direction;
  12:  
  13:     public override string ToString()
  14:     {
  15:         StringBuilder Builder = new StringBuilder();
  16:         Builder.Append(_ColumnName);
  17:         if (_Direction == Direction.Ascending)
  18:         {
  19:             Builder.Append(" ASC");
  20:         }
  21:         else
  22:         {
  23:             Builder.Append(" DESC");
  24:         }
  25:         if (_NestedOrderBy != null)
  26:         {
  27:             Builder.Append("," + _NestedOrderBy);
  28:         }
  29:         return Builder.ToString();
  30:     }
  31: }

Very simple, uses the same unary expression set up. It also allows for nested items, with the outside items having more weight than the nested OrderBy objects. And all of this gets inputted to a new select function, SelectWhere, that is viewable in the code below. And just like the previous functions in the Session class, it calls the provider, which calls the SQL builder, which calls a newly created class.

   1: internal class SelectWhere:IStatement
   2: {
   3:     public SelectWhere(Class Class, ClassManager Manager)
   4:         : base(Class, Manager)
   5:     {
   6:     }
   7:  
   8:     public List<T> Select<T>(SQLHelper Helper, IClause<T> WhereClause, IClause<T> OrderByClause)
   9:     {
  10:         Session TempSession = Factory.CreateSession();
  11:         List<T> ReturnList = new List<T>();
  12:         try
  13:         {
  14:             StringBuilder Builder=new StringBuilder("SELECT "+_Class.IDField.Name+" FROM "+_Class.OriginalType.Name);
  15:             if (WhereClause != null)
  16:             {
  17:                 Builder.Append(" WHERE " + WhereClause.ToString());
  18:             }
  19:             if (OrderByClause != null)
  20:             {
  21:                 Builder.Append(" ORDER BY " + OrderByClause.ToString());
  22:             }
  23:             Helper.Command = Builder.ToString();
  24:             Helper.CommandType = CommandType.Text;
  25:             Helper.ClearParameters();
  26:             Helper.Open();
  27:             WhereClause.SetupSQL(Helper, _Class);
  28:             Helper.ExecuteReader();
  29:             while (Helper.Read())
  30:             {
  31:                 object ID = Helper.GetParameter(_Class.IDField.Name, null);
  32:                 T TempObject = default(T);
  33:                 TempSession.Select<T>(ID, out TempObject);
  34:                 ReturnList.Add(TempObject);
  35:             }
  36:         }
  37:         catch { }
  38:         finally { Helper.Close(); }
  39:         return ReturnList;
  40:     }
  41: }

And as you can see, all the class does is runs a query to find a list of objects that fulfill the query. So that's it for basic queries. For anything more complex, I realized that I needed to be able to create my own stored procedures and call them. That's even simpler.

   1: internal class SelectStoredProcedure : IStatement
   2: {
   3:     public SelectStoredProcedure(Class Class, ClassManager Manager)
   4:         : base(Class, Manager)
   5:     {
   6:     }
   7:  
   8:     public List<T> Select<T>(SQLHelper Helper, List<IVariable> Variables, string StoredProcedure)
   9:     {
  10:         Session TempSession = Factory.CreateSession();
  11:         List<T> ReturnList = new List<T>();
  12:         try
  13:         {
  14:             Helper.Command = StoredProcedure;
  15:             Helper.CommandType = CommandType.StoredProcedure;
  16:             Helper.ClearParameters();
  17:             Helper.Open();
  18:             foreach(IVariable Variable in Variables)
  19:             {
  20:                 Variable.SetupSQL(Helper);
  21:             }
  22:             Helper.ExecuteReader();
  23:             while (Helper.Read())
  24:             {
  25:                 object ID = Helper.GetParameter(_Class.IDField.Name, null);
  26:                 T TempObject = default(T);
  27:                 TempSession.Select<T>(ID, out TempObject);
  28:                 ReturnList.Add(TempObject);
  29:             }
  30:         }
  31:         catch { }
  32:         finally { Helper.Close(); }
  33:         return ReturnList;
  34:     }
  35: }

The function just needs to know the name of the stored procedure and a list of variables.

   1: public class Variable:IVariable
   2: {
   3:     public Variable(string Name, object Value,SqlDbType DataType)
   4:         : base(Name, Value,DataType)
   5:     {
   6:     }
   7:  
   8:     internal override void SetupSQL(SQLHelper Helper)
   9:     {
  10:         Helper.AddParameter(_Name, _Value, _DataType);
  11:     }
  12: }

The variable class is just a holder for the name of the field, the value, and the SQL type. That's it. Now if you paid attention, you'll notice that each of these approaches only query the ID fields for the objects. Once it has the ID field, it calls the select function that already exists. The main reason for this is the simple fact that I'm lazy and didn't want to rewrite all that code again. The second reason is how I've set up the cache.

The caching system that I've set up uses the same provider model that the SQL uses (so I'm going to skip that portion). Instead I'm just going to show you the actual class in charge of caching.

   1: internal class ASPNetCache:ICache
   2: {
   3:     public override bool Exists(string Name)
   4:     {
   5:         try
   6:         {
   7:             if (HttpContext.Current.Cache[Name] != null)
   8:             {
   9:                 return true;
  10:             }
  11:         }
  12:         catch { }
  13:         return false;
  14:     }
  15:  
  16:     public override void Save(string Name, object Value)
  17:     {
  18:         if (Exists(Name))
  19:         {
  20:             HttpContext.Current.Cache[Name] = Value;
  21:         }
  22:         else
  23:         {
  24:             HttpContext.Current.Cache.Insert(Name, Value, null,
  25:                 System.Web.Caching.Cache.NoAbsoluteExpiration,
  26:                 TimeSpan.FromHours(1.0));
  27:         }
  28:     }
  29:  
  30:     public override void Delete(string Name)
  31:     {
  32:         if (Exists(Name))
  33:         {
  34:             HttpContext.Current.Cache.Remove(Name);
  35:         }
  36:     }
  37:  
  38:     public override object this[string Name]
  39:     {
  40:         get { return HttpContext.Current.Cache[Name]; }
  41:     }
  42: }

As you can see there are only three functions and one property. Save, Delete, and a retrieve. I like to keep code simple when possible and this was about as simple as I could make it. So what does this have to do with everything going through the Select function? Well let's look at a scenario.

Let's say that you have a table that holds three items. I make a query against their IDs, which thanks to our cache, places them in there without too much of an issue. Now let's say that I make a query that asks the system for two of those items. The cache is rather dumb so unless we know before hand that the query is going to return something already in memory, we're left with making the query and storing that in memory. So now we have 5 objects in memory. Now let's say we update one of the objects and the object is stored twice in the system. When we save, it's easy to tell that the item is there the first time because we queried against the ID, but what about the other instance? So either we search all of the objects in all of the stored queries, or I have to make that cache a lot smarter, OR I simply force everything through that select statement and simply store the information once. I chose the easy route.

So we delete the item from the cache when we delete from the database, update it when we save, and store it in the database when we call select. That's it, we're done. After the cache is in place, we have a library that will work as a simple ORM. I have no freakin' clue how well it will do in the real world but, hey, I just wanted to see what went into making an ORM. Although I do have a project lined up where I plan to use it and improve upon it. I even see some areas where I could improve the system (and I'm sure everyone else out there sees the giant logic holes and issues that I've set up for myself as well). Anyway, download the code, take a look, leave feedback, and happy coding.



Comments