Build Your Own Data Access Layer: Out Of The Box CRUD Functionality

14 commentsWritten on August 24th, 2009 by
Categories: Build Your Own DAL

Note: This post is part of a series. Be sure to read the introduction here.

One thing that i consider an absolute must-have in any data access layer is the ability to perform CRUD operations out-of-the-box without having to write any code to enable these operations. Once your data access layer knows about your classes and your tables, CRUD operations should 'just work'.

As you've seen in the previous post of this series, the TableInfo class offers a couple of methods to automatically build the required SQL statements for CRUD actions. With these statements, we can easily create SqlCommand instances for all CRUD operations.

First of all, i use the following helper method to easily add a SqlParameter to a SqlCommand:

        public static void CreateAndAddInputParameter(this SqlCommand command, DbType type, string name, object value)
        {
            var parameter = command.CreateParameter();
            parameter.Direction = ParameterDirection.Input;
            parameter.DbType = type;
            parameter.ParameterName = name;
 
            if (value == null)
            {
                parameter.IsNullable = true;
                parameter.Value = DBNull.Value;
            }
            else
            {
                parameter.Value = value;
            }
 
            command.Parameters.Add(parameter);
        }

I also have the following abstract DatabaseAction class which has a few properties that are used by most of the CRUD actions:

    public abstract class DatabaseAction
    {
        private readonly SqlConnection connection;
        private readonly SqlTransaction transaction;
        protected MetaDataStore MetaDataStore { get; private set; }
        protected EntityHydrater Hydrater { get; private set; }
        protected SessionLevelCache SessionLevelCache { get; private set; }
 
        protected DatabaseAction(SqlConnection connection, SqlTransaction transaction, MetaDataStore metaDataStore,
                                 EntityHydrater hydrater, SessionLevelCache sessionLevelCache)
        {
            this.connection = connection;
            this.transaction = transaction;
            MetaDataStore = metaDataStore;
            Hydrater = hydrater;
            SessionLevelCache = sessionLevelCache;
        }
 
        protected SqlCommand CreateCommand()
        {
            var command = connection.CreateCommand();
            command.Transaction = transaction;
            return command;
        }
    }

Did you notice the EntityHydrater and SessionLevelCache? I'm going to ignore those as much as possible for now, since they will be covered in depth in the following two posts in these series. The important thing to note is that each derived DatabaseAction will have a reference to the MetaDataStore.

And now we can easily start implementing our CRUD actions. Let's start with the GetByIdAction:

    public class GetByIdAction : DatabaseAction
    {
        public GetByIdAction(SqlConnection connection, SqlTransaction transaction, MetaDataStore metaDataStore,
            EntityHydrater hydrater, SessionLevelCache sessionLevelCache)
            : base(connection, transaction, metaDataStore, hydrater, sessionLevelCache)
        {
        }
 
        public TEntity Get<TEntity>(object id)
        {
            var cachedEntity = SessionLevelCache.TryToFind(typeof(TEntity), id);
            if (cachedEntity != null) return (TEntity)cachedEntity;
 
            using (var command = CreateCommand())
            {
                var tableInfo = MetaDataStore.GetTableInfoFor<TEntity>();
 
                var query = tableInfo.GetSelectStatementForAllFields();
                tableInfo.AddWhereByIdClause(query);
 
                command.CommandText = query.ToString();
                command.CreateAndAddInputParameter(tableInfo.PrimaryKey.DbType, tableInfo.GetPrimaryKeyParameterName(), id);
                return Hydrater.HydrateEntity<TEntity>(command);
            }
        }
    }

Pretty simple stuff, right? This will first check the session level cache to see if this instance has already been retrieved in the current session (i'll discuss the session in a later post) and if so, it will return that instance. If it's not in the cache, it will create a SqlCommand and fill its CommandText property with a SQL string that is provided by the relevant TableInfo class. After that, it passes the SqlCommand to the EntityHydrater so it can return an actual entity instance.

The details of EntityHydration will be fully explored in the next post of this series, so for now you only need to know that it can transform the results from the SqlCommand to an instance of TEntity.

It's always useful to get a collection of all instances of a certain entity class, so we also have this very simple FindAllAction:

    public class FindAllAction : DatabaseAction
    {
        public FindAllAction(SqlConnection connection, SqlTransaction transaction, MetaDataStore metaDataStore,
            EntityHydrater hydrater, SessionLevelCache sessionLevelCache)
            : base(connection, transaction, metaDataStore, hydrater, sessionLevelCache)
        {
        }
 
        public IEnumerable<TEntity> FindAll<TEntity>()
        {
            using (var command = CreateCommand())
            {
                command.CommandText = MetaDataStore.GetTableInfoFor<TEntity>().GetSelectStatementForAllFields().ToString();
                return Hydrater.HydrateEntities<TEntity>(command);
            }
        }
    }

We also need an InsertAction:

    public class InsertAction : DatabaseAction
    {
        public InsertAction(SqlConnection connection, SqlTransaction transaction, MetaDataStore metaDataStore,
            EntityHydrater hydrater, SessionLevelCache sessionLevelCache)
            : base(connection, transaction, metaDataStore, hydrater, sessionLevelCache)
        {
        }
 
        public TEntity Insert<TEntity>(TEntity entity)
        {
            using (var command = CreateCommand())
            {
                var tableInfo = MetaDataStore.GetTableInfoFor<TEntity>();
 
                command.CommandText = tableInfo.GetInsertStatement();
 
                foreach (var parameterInfo in tableInfo.GetParametersForInsert(entity))
                {
                    command.CreateAndAddInputParameter(parameterInfo.DbType, parameterInfo.Name, parameterInfo.Value);
                }
 
                object id = Convert.ChangeType(command.ExecuteScalar(), tableInfo.PrimaryKey.DotNetType);
                tableInfo.PrimaryKey.PropertyInfo.SetValue(entity, id, null);
                SessionLevelCache.Store(typeof(TEntity), id, entity);
                return entity;
            }
        }
    }

There's not a lot to this one either... The actual insert statement is once again retrieved through the TableInfo class, as are the parameter values (including their values for this specific entity). You can go back to the previous post to look at the implementation of TableInfo's GetParametersForInsert method :)

Keep in mind that there is a limitation here that i only support SQL Server's Identity-style generators. Again, if you want to support multiple identifier strategies like NHibernate does, you'll have to deal with a lot more complexity in the InsertAction class.

The UpdateAction is very similar:

    public class UpdateAction : DatabaseAction
    {
        public UpdateAction(SqlConnection connection, SqlTransaction transaction, MetaDataStore metaDataStore,
            EntityHydrater hydrater, SessionLevelCache sessionLevelCache)
            : base(connection, transaction, metaDataStore, hydrater, sessionLevelCache)
        {
        }
 
        public TEntity Update<TEntity>(TEntity entity)
        {
            using (var command = CreateCommand())
            {
                var tableInfo = MetaDataStore.GetTableInfoFor<TEntity>();
 
                command.CommandText = tableInfo.GetUpdateStatement();
 
                foreach (var parameterInfo in tableInfo.GetParametersForUpdate(entity))
                {
                    command.CreateAndAddInputParameter(parameterInfo.DbType, parameterInfo.Name, parameterInfo.Value);
                }
 
                command.ExecuteNonQuery();
                return entity;
            }
        }
    }

And finally, we have the DeleteAction:

    public class DeleteAction : DatabaseAction
    {
        public DeleteAction(SqlConnection connection, SqlTransaction transaction, MetaDataStore metaDataStore,
            EntityHydrater hydrater, SessionLevelCache sessionLevelCache)
            : base(connection, transaction, metaDataStore, hydrater, sessionLevelCache)
        {
        }
 
        public void Delete<TEntity>(TEntity entity)
        {
            using (var command = CreateCommand())
            {
                var tableInfo = MetaDataStore.GetTableInfoFor<TEntity>();
                command.CommandText = tableInfo.GetDeleteStatement();
                object id = tableInfo.PrimaryKey.PropertyInfo.GetValue(entity, null);
                command.CreateAndAddInputParameter(tableInfo.PrimaryKey.DbType, tableInfo.GetPrimaryKeyParameterName(), id);
                command.ExecuteNonQuery();
                SessionLevelCache.Remove(entity);
            }
        }
    }

And that's all there is to it. We now have some classes that will give us out-of-the-box CRUD functionality for all of the mapped entity classes. Obviously, you will still need some way of actually accessing this functionality from your application code and you certainly don't want to instantiate and use these DatabaseAction classes directly. All of that will be covered in the "Bringing It All Together" post, so stay tuned ;)

  • Pingback: Reflective Perspective - Chris Alcock » The Morning Brew #419

  • suhair

    This series is awesome, thanks

  • Dan

    What Suhair said, simply awesome.

  • Pingback: New and Notable 362 : Sam Gentile's Blog (if (DeveloperTask == Communication && OS == Windows)

  • Angel Cheong

    Awesome post! But l have a question here. l can’t see where is the usage of this method CreateAndAddInputParameter?. From your codes l can see some places did use the command.CreateAndAddInputParameter(tableInfo.PrimaryKey.DbType, tableInfo.GetPrimaryKeyParameterName(), id), but l don’t think there are referring to public static void CreateAndAddInputParameter rite? Because l can see the number of parameters pass to the method CreateAndAddInputParameter are different from the actual parameter in this method. Correct me if l am wrong.

    Thanks.

  • http://davybrion.com Davy Brion

    @Angel

    the CreateAndAddInputParameter method is an extension method… that’s why the number of parameters differs from the definition

  • Pmdevers

    Any Ideas to implement Many To Many Database action??

    • http://davybrion.com Davy Brion

      not immediately no… collections in general were deliberately kept out of scope for this because i wanted to keep this relatively simple

      any reason why you can’t go with an existing solution?

      • Pmdevers

        I was wondering if it was easly done because I have an application that has Many Users and Roles
        and offcourse User_role mapping table.

        And writing code like

        IQuery query = session.CreateQuery(“WHERE RoleID in (SELECT RoleID FROM UserRoles where UserID = @USERID)”);
        query.AddParameter(“@USERID”, user.ID, System.Data.DbType.Guid);
        query.GetResults();

        feels like there is to much SQL or is there an easier way to accomplish this or is this the right approach?

        • http://davybrion.com Davy Brion

          well, in the context of this data layer it is indeed the right approach

          though i agree that it feels like there’s too much SQL… but that’s a limitation that won’t be easy to avoid if you’re using this

          • Pmdevers

            thanks for your quick replies and this is a great article by the way ;)

  • Pingback: Build Your Own Data Access Layer: Hydrating Entities

  • Feddedo21

    How can i use this DAL layer , i cannot find an entry point for that , can you provide us with example ?

    • http://davybrion.com Davy Brion

      it’s mentioned here: http://davybrion.com/blog/2009/08/build-your-own-data-access-layer-bringing-it-all-together/

      basically, create the SessionFactory and use that to create sessions