|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionThis article presents a data abstraction layer (DAL) designed to:
Background
My initial goal in designing and implementing this data abstraction layer (DAL) was to make my job - helping design and code a large website from the ground up - easier. Designed to be a simple and light-weight library with a limited feature set, the end product turned out to be a much larger beast than originally planned, both in terms of size and functional scope. I attribute the difference between the project's original and current design to a continuous stream of feature requests (not only from the client, but also from the other developer on the project) and to constantly-changing requirements during development. Still, creating this data abstraction layer solo was a great learning experience, and quite exciting at times ("testing" a new version involved uploading it to the heavily-trafficked (production) website for immediate testing - trial-by-fire-style). In other words, this was one of those rare "no program managers, screw the unit tests, specs-be-damned, design documents are a waste of time" kind of projects. Ahh, fun times. Data Abstraction Layer - Basic DefinitionA data abstraction layer (DAL) is a library used to facilitate communication between code and database. In code, the developer issues all query, insert, update, and delete requests to the DAL - not directly to the database. By providing a standard interface to expose database-related functionality, the DAL hides the complexity associated with code-database communication, and at the same time, eliminates the need for developers to write database-engine specific data access code (e.g., SQL Server uses How it Works (and Implementation Options)The data abstraction layer is responsible for generating Query, Update, Insert, and Delete statements as needed, and populating class property values using data returned from the database. Therefore, the data abstraction layer needs to be able to translate between table names and class types (i.e., table person stores data representing instances of class In the Beginning...
In the beginning, there were two custom attributes: the class attribute After creating the two attributes, I finished the project by writing methods that used Reflection to read the attribute values and auto-generated Update, Insert, and Delete statements. Piece of cake... Piece of cake, except - the developer I was working with (also the database designer) was accustomed to embedding hard-coded SQL statements directly into code wherever and whenever data access was required. Frequently, he felt restricted by our use of the data abstraction layer (I must admit that during the initial stages of development, I also was tempted to go back to plunking massive, hardcoded SQL statements directly into web page code-behinds). When he felt this way, he would say something to the effect of, "Trying to use the DAL to do X is really frustrating, and it is easy to do with a SQL statement... so what's up with your library?" These bug reports / complaints were almost always cases where he was attempting to use the DAL well outside of its original design scope. In hindsight, these complaints were the feature requests which continuously drove development forward. (Thus began the first era of feature creep...) Data Abstraction Layer - Example UseBefore getting into the nitty-gritty details of implementation, I'd like to take a minute to:
To use the data abstraction layer, existing C# or VB.NET classes are decorated with instructive attributes, and must inherit from the DAL-enabling generic base class,
// Find a specific person by id
Person p = Person.Find[19387];
// Retrieve all People, sorting/grouping by last name,
// then (within identical last names) by first name:
OrderBy<Person> orderByLastFirstName =
new OrderBy<Person>("last_name").AddOrderBy("first_name");
List<Person> orderedPeople = Person.DALQuery(orderByLastFirstName);
// View all people with first name of "Fred"
// whose information has been updated in the past 30 days.
// Note: with caching enabled, this query
// will not hit the database - results will be retrieved
// from cache (which was conveniently populated
// during execution of the above statement)
List<Person> people = WorkOrder.DALQuery_List(
(Col_FirstName == "Fred") &
(Col_LastUpdated > DateTime.Now.AddDays(-30)));
// Create and insert a new Person entity - the autoincrement
// value is automatically populated after insert
Person p = new Person() { FirstName = "Owen", LastName = "Emlen" };
Person.Insert(p);
Console.WriteLine("The identity of the newly inserted Person is " +
p.person_id.ToString());
Accessing sensitive data via code:
The same data, viewed in the database:
Implementation DetailsAs mentioned in the Background section, the data abstraction layer relies on custom attributes attached to class definitions and properties to translate between references to code objects (classes, properties) and references to database objects (tables, columns). Translating property names into column names allows the data abstraction layer to transparently generate Query, Update, Insert, and Delete statements. Translating column names into property names allows the DAL to populate the appropriate class properties using values returned from a database query.
Encapsulating Search Logic: Class DBWhere
The For example, in code, a There are several benefits associated with using public partial class UserInfo : DBLayer<UserInfo>,
IDBLayer<UserInfo>, INotifyPropertyChanged{
.....
// Active User Definition/Logic:
// 1. Admins are always considered active
// 2. If not an admin, user must be verified, not banned,
// and must have logged in once within the past 90 days
public static readonly DBWhere ActiveUserDefinition =
(Col_security == SecurityLevel.Admin) |
(Col_verified == true & Col_banned == false &
Col_last_login > DateTime.Now.AddDays(-90));
public static List<UserInfo> ReadAllActiveUsers()
{
return DALQuery_List(ActiveUserDefinition);
}...
}
Defining Search Logic Using DBWhereThe
// Explicit creation of DBWhere logic using the constructor and class methods
DBWhere where = new DBWhere("FirstName", "Fred").AND("LastName", "Flintstone");
List<Person> results = DALQuery(where);
// Construction of a 'friendly-looking' DBWhere logic statement
List<Person> results = DALQuery(Col_FirstName == "Fred" &
Col_LastName == "Flintstone");
DBWhere InternalsInternally, each instance of
Class diagram for
DBWhere.Translating DBWhere Logic into a SQL WHERE StatementIn order to effectively express search logic in a database query, a SQL /// <summary>
/// Takes basic AND pair requirements
/// (PropertyName (logic) Value) and creates a SQL AND statement
/// </summary>
/// <param name="parametersOut">
/// This parameter is used to output SqlParameters if the query is parameterized.
/// For instance, if a pair contains parameter
/// @CustID with value 42, then parametersOut will contain
/// the parameter value 42 and associate it with the token @CustID
/// </param>
/// <returns>
/// A StringBuilder containing a SQL logic statement
/// appended to the existing contents of sb
/// </returns>
protected static StringBuilder BuildAndFromPairs(
StringBuilder sb, PropertyNameAndValueCollection pairs,
ref List<SqlParameter> parametersOut)
{
bool wroteAnything = false;
foreach (PropNameAndVal pair in pairs._propertyNameAndValuePairs)
{
PropertyDescriptor propDescriptor =
CachedAttributes.GetPropertyDescriptor(pair._propertyName);
if (propDescriptor == null)
{
throw new Exception(
String.Format("Property name {0} associated with class {1} not found!",
pair._propertyName, ClassType.Name));
}
string columnName =
CachedAttributes.GetColumnNameForProperty(propDescriptor);
// only generate SQL for properties
// that have corresponding database column names
if (columnName != null)
{
SqlDbType sqlType = CachedAttributes.GetSqlDataType(propDescriptor);
// Some SQL types we cannot use in queries
if (sqlType == SqlDbType.Text || sqlType == SqlDbType.NText ||
sqlType == SqlDbType.Binary || sqlType == SqlDbType.Image) continue;
if (!wroteAnything)
{
// first time open parens
wroteAnything = true;
sb.Append("(");
}
else
{
// subsequent times AND together statements
sb.Append(" AND ");
}
if (pair._nonStandard)
{
// For queries that currently cannot be handled using SQL, insert a placeholder
// that always evaluates to true: <large_unique_number> = <large_unique_number>
int u = (pair._propertyName + pair._value.ToString()).GetHashCode();
sb.Append("(" + u.ToString() + "=" + u.ToString() + ")");
continue;
}
object comparisonValue = pair._value;
string sqlComparison;
if (pair._isParameterized)
{
// Handle parameterized queries
if (parametersOut == null) parametersOut = new List<SqlParameter>(4);
Type propType = CachedAttributes.GetPropertyType(propDescriptor);
// Handle string mangling or encryption, if specified
if (propType == typeof(string))
{
if (comparisonValue != null)
{
string formattedValue =
PadAndTrim(propDescriptor, comparisonValue.ToString());
bool wantMangle = WantManglePropertyValue(propDescriptor);
bool wantRijindael = WantUseRijindaelProperty(propDescriptor);
// Possibly Mangle/Encrypt the string value before writing out
if (wantMangle)
formattedValue = ManglingProvider.Encrypt(formattedValue);
if (wantRijindael)
formattedValue = EncryptionProvider.Encrypt(formattedValue);
comparisonValue = formattedValue;
}
}
if (_dataTransport == DataTransport.SqlDirectConnection)
{
// Use Sql named parameters
SqlParameter sqlparam =
new SqlParameter(pair._parameterName, comparisonValue);
parametersOut.Add(sqlparam);
// Assume parameter names are passed in as @name
sqlComparison = pair._comparisonOperator + pair._parameterName;
}
else
{
// OleDb does not support named parameters...
// we must use the column names and ?'s instead
SqlParameter sqlparam =
new SqlParameter(pair._propertyName, comparisonValue);
parametersOut.Add(sqlparam);
sqlComparison = " " + pair._comparisonOperator + " ?";
}
}
else
{
// No parameterization
// Determine the comparison operator to use
string comparison = (pair._logicNOT) ?
pair._comparisonOperator.ReverseSQLComparisonOperator() :
pair._comparisonOperator;
sqlComparison = GetSQLComparison(
propDescriptor, pair._value, comparison);
}
sb.Append(columnName);
sb.Append(sqlComparison);
}
}
// Close parens if we wrote anything to sb
if (wroteAnything)
sb.Append(")");
return sb;
}
Querying an In-Memory Collection using DBWhereThe Populating DAL-Enabled Classes using Database Query ResultsThe
At runtime, DAL instantiates a concrete implementation of the The solution includes two implementations of this interface: The use of the Currently, the DAL expects that In the future, I'd like to determine if task #2 can be generalized and extracted from the database-engine-specific Avoiding Repetitive Use of ReflectionIn the code I've presented so far, you may have noticed statements like // Using reflection every time we want
// to retrieve the value of a MappedToColumn attribute
// attached to a property would be inefficient and slow...
object[] objs = propInfo.GetCustomAttributes(typeof(MappedToColumn), true);
The AttributeLookup<string, LimitTextLength, T> _maxFieldLength =
new AttributeLookup<string, LimitTextLength, T>();
// Initially uses reflection to locate the LimitTextLength
// attribute associated with the property FirstName
LimitTextLength attrib = _maxFieldLength.Find("FirstName");
...
// The request for the LimitTextLength attribute
// associated with property FirstName is now handled
// via dictionary lookup
LimitTextLength attrib = _maxFieldLength.Find("FirstName");
The implementation of the /// <summary>
/// Provides common code for fast dictionary lookups of an attribute by key,
/// with dictionary lookup populated on demand
/// </summary>
public class AttributeLookup<TKey, TAttribute, TClass>
where TAttribute : Attribute
where TClass : DBLayer<TClass>, IDBLayer<TClass>, new()
{
// Dictionary used to quickly find a custom attribute
// associated with a property by lookup key
private Dictionary<TKey, TAttribute> _lookup;
public AttributeLookup()
{
_lookup = new Dictionary<TKey, TAttribute>();
}
public TAttribute Find(TKey key)
{
TAttribute val;
// Attempt to retrieve the attribute using a fast dictionary lookup
if (_lookup.TryGetValue(key, out val) == true) { return val; }
else
{
// Find the property associated
// with the specified key (usually property name)
PropertyInfo propInfo =
DBLayer<TClass>.ClassType.GetProperty(key.ToString());
return Find(key, propInfo);
}
}
public TAttribute Find(TKey key, PropertyInfo propInfo)
{
TAttribute val;
// Attempt to retrieve the attribute using a fast dictionary lookup
if (_lookup.TryGetValue(key, out val) == true) { return val; }
else
{
// Not found, get custom attributes
// of type TAttribute associated with the property
val = (propInfo == null) ? null :
propInfo.GetCustomAttribute<TAttribute>();
// Add the attribute to the dictionary.
// Use try/catch to handle race conditions, avoiding lock()
try { _lookup.Add(key, val); }
catch { }
return val;
}
}
}
Solution Contents and DescriptionThe solution file contains the following projects:
The Demo Application
The demo application implements several DAL-enabled classes -
The fully decorated, DAL-enabled [TableName, CollectionCacheable, PrepopulateCache(false)]
[IDALDatabaseClassName("DALSqlServer"),
ReadConnectionStringFromMethod("ReadConnectionString")]
public partial class Person : DBLayer<Person>,
IDBLayer<Person>, INotifyPropertyChanged
{
private long _person_id;
private long _user_id;
private string _first_name = String.Empty;
private string _last_name = String.Empty;
/// <summary>
/// Demonstrates another way to supply DAL with a connection string (at runtime)
/// </summary>
public static string ReadConnectionString()
{
return DALSampleApplication.Properties.Settings.Default.ConnectionStringToUse;
}
[MappedToColumn, Identity, PrimaryKey, QuickLookup]
[OneToMany(typeof(Address), "person_id"),
OneToMany(typeof(SecretInfo), "person_id")]
public long person_id
{
get { return _person_id; }
set { Set<long>(ref _person_id, "person_id", value); }
}
[MappedToColumn, JoinsTo(typeof(UserInfo))]
public long user_id
{
get { return _user_id; }
set { Set<long>(ref _user_id, "user_id", value); }
}
[MappedToColumn, NameOfObject, LimitTextLength(100)]
public string first_name
{
get { return _first_name; }
set { SetString(ref _first_name, "first_name", value); }
}
[MappedToColumn, LimitTextLength(100)]
public string last_name
{
get { return _last_name; }
set { SetString(ref _last_name, "last_name", value); }
}
/// <summary>
/// Returns the associated UserInfo for this Person.
/// Using JoinNonNull guarantees that
/// if there is no UserInfo associated with this Person,
/// a new (blank) UserInfo object will be returned.
/// </summary>
public UserInfo UserInfo { get { return JoinNonNull<UserInfo>(); } }
/// <summary>
/// These are passthrough properties that reference
/// properties from the Person's associated UserInfo record.
/// </summary>
public string LoginName { get { return UserInfo.login_name; } }
public string Password { get { return UserInfo.password; } }
/// <summary>
/// Gets all addresses associated with this Person
/// </summary>
public List<Address> Addresses { get { return JoinMultiple<Address>(); } }
/// <summary>
/// Gets the 'secret information' records associated with this Person
/// </summary>
public List<SecretInfo> SecretInformation { get { return JoinMultiple<SecretInfo>(); } }
/// <summary>
/// FirstName + LastName
/// </summary>
public string FullName { get { return String.Format("{0} {1}",
first_name.Trim(), last_name.Trim()).Trim(); } }
/// <summary>
/// Returns a comma-delimited list of all other people
/// who have a primary address near this person (same zipcode)
/// </summary>
public string NamesOfOtherPeopleWithPrimaryAddressInSameZipcode()
{
List<Person> otherPeopleInZip = OtherPeopleWithPrimaryAddressInSameZipcode;
return otherPeopleInZip.BuildCommaDelimited<Person>(delegate(Person p)
{ return p.FullName; });
}
/// <summary>
/// Returns the Person's primary address,
/// or null if there is no primary address for the Person
/// </summary>
public Address PrimaryAddress
{ get { return Address.FindPrimaryAddressFor(person_id); } }
/// <summary>
/// Find other people with a primary address near this person (same zipcode)
/// </summary>
public List<Person> OtherPeopleWithPrimaryAddressInSameZipcode
{
get
{
string primaryZipcode =
(PrimaryAddress == null) ? string.Empty : PrimaryAddress.zip;
return Address.PeopleWithPrimaryAddressInZipcode(primaryZipcode,
person_id);
}
}
/// <summary>
/// Overridden Delete() - cascading delete:
/// 1. Cleans up all addresses associated with the Person
/// 2. Cleans up all secreted information associated with the Person
/// 3. Calls the base DBLayer.Delete to delete the Person entity
/// </summary>
/// <returns></returns>
public override string Delete()
{
Addresses.Delete();
SecretInformation.Delete();
return DBLayer<Person>.Delete(this);
}
}
Development ChallengesI encountered many challenges during the development; if you've written a data abstraction layer yourself, you may be very familiar with some of these issues (suggestions for alternate solutions are welcome!):
The database was (appropriately) expecting two characters, but I sure as heck didn't want to have to remember to write I created a /// <summary>
/// Returns a padded and/or trimmed string based
/// on the padding/maxlength attributes attached to a property
/// </summary>
internal static string PadAndTrim(PropertyDescriptor property, string s)
{
int padLength = CachedAttributes.GetTextPadding(property);
int trimLength = CachedAttributes.GetMaxTextLength(property);
// If no trimming or padding required, return the original string
if (trimLength == Int32.MaxValue && padLength == 0)
return s;
int stringLength = s.Length;
if (stringLength > trimLength)
{
// Return a trimmed string
return s.Substring(0, trimLength);
}
else if (stringLength < padLength)
{
// Return a padded string
return s.PadRight(padLength);
}
return s;
}
Using the DAL made it simple to retrieve information from the database. Naturally, we began to retrieve, use, and display additional (and arguably non-essential) information. This practice resulted in a significant increase in the number of database queries issued. In addition, the way we were using the DAL to perform multiple smaller (on-demand) lookups resulted in 'chatty' communications between the DAL and the database engine. To solve this problem, a specialized Unless otherwise specified, SQL uses case insensitive comparison when performing string comparison with For some reason (...), we had several tables with no defined primary key (be nice - I had little say in the database design). When there was no primary key to identify a row to delete, Step-by-step Instructions for Generating DAL-Enabled Classes from Database Tables
Specifying the Connection String to UseIn order to enable database connectivity, a connection string needs to be specified. If you do not do so, a "No connection string specified for class (YourClassNameHere)" exception will be thrown. There are several ways to supply DAL-enabled classes with a connection string:
[ConnectionString("workstation id=localhost;" +
"packet size=4096;Connect Timeout = 45;" +
"data source=OWEN-PC;uid=DALUser;" +
"pwd=codeproject;persist security info=True;" +
"initial catalog=DALSample;")]
public partial class SecretInfo : DBLayer<SecretInfo>,
IDBLayer<SecretInfo>, INotifyPropertyChanged
...
[ReadConnectionStringFromMethod("ReadConnectionString")]
public partial class Person : DBLayer<Person>,
IDBLayer<Person>, INotifyPropertyChanged
{
...
/// <summary>
/// Demonstrates another way to supply DAL
/// with a connection string (at runtime)
/// </summary>
public static string ReadConnectionString()
{
switch (RuntimeEnvironment)
{
case RuntimeEnvironment.Local:
return Properties.Settings.Default.LocalConnectionString;
case RuntimeEnvironment.Test:
return Properties.Settings.Default.TestConnectionString;
case RuntimeEnvironment.Production:
return Properties.Settings.Default.ProductionConnectionString;
}
}
...
}
Specifying Database & Database-Specific Options
Testing Your DAL-Enabled ClassesYour DAL-enabled classes are now ready for use. :)
Console.WriteLine(UserInfo.DAL_GetRowCount().ToString());
Cache Limits and other DAL-Related SettingsEach DAL-enabled class type has an associated collection of settings that define certain aspects of the DAL-specific behavior for that class (examples include limiting cache size and using of dynamic indices when querying in-memory cache).
Advanced Features: Join and JoinMultiple
Note the Once you have added the Person addressBelongsTo = address.Join<Person>();
//... or ...
Person addressBelongsTo = address.JoinNonNull<Person>();
The public partial class Person :
DBLayer<Person>, IDBLayer<Person>, INotifyPropertyChanged
{
...
[MappedToColumn, Identity, PrimaryKey, QuickLookup]
[OneToMany(typeof(Address), "person_id")]
public long person_id
{
get { return _person_id; }
set { Set<long>(ref _person_id, "person_id", value); }
}
...
}
After marking the property with the List<Address> addressesForPerson = person.JoinMultiple<Address>();
Other Useful Methods / Additional FunctionalityThe There are also several somewhat-tangent features included in the solution that are worth mentioning. These are:
OrderBy<Log> orderBy =
new OrderBy<Log>("LogDate", true).AddOrderBy("Severity", true);
logEntries = logEntries.SortByProperties(orderBy);
List<Person> peopleList1 = ...
List<Person> peopleWithNoDupes = peopleList1.RemoveDuplicates();
List<Person> peopleList2 = ...
List<Person> personWithUniqueFullNames =
peopleList2.RemoveDuplicates("FullName");
List<Person> uniquePeople =
peopleList1.CombineAndRemoveDuplicates(peopleList2);
Appendix: DAL-Related Class Attributes
DAL-Related Property Attributes
DisclaimerAlthough the DAL presented in this article handles the ASP.NET-database interaction for several websites and is being used in Windows Forms applications, I make no guarantees that there aren't still nasty bugs lurking around every corner. Use at your own risk. Points of InterestI hope you've enjoyed the article (and?)/or have found it useful. Comments/suggestions are welcome, and I'd be happy to publish future versions to CodeProject. owen@binarynorthwest.com. Member BrainTech, LLC. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||