SQL
is a language for data retrieval and manipulation and other actions executed onto a database.
This article introduces different ways of organizing SQL
scripts in .NET
projects.
Executing queries onto a database would normally be done through ORM
, but if performance is an issue, it can be combined with raw SQL
.
Other techniques, like stored procedures and views were dismissed at one point, in favor of putting the SQL
files directly the .NET
projects, under a sub-folder named Sql
:
It is preferred to use embedded resources to include the SQL
files:
This deploys the SQL
together with your EXE
or DLL
, because compiles the SQL
file right into the assembly.
It might be an idea to let the SQL
file names begin with the entity type name, so they stay grouped together:
Then you can put an enum
in the Sql
folder in your .NET
project:
Add enum
members that correspond to the file names of the SQL
files:
namespace JJ.Demos.SqlExecutor.Sql
{
internal enum SqlEnum
{
Ingredient_UpdateName
}
}
The SQL
may look as follows:
update Ingredient set Name = @name where ID = @id;
The classic way of executing SQL
in .NET
would be to use System.Data.SqlClient
. But in this architecture the SqlExecutor API
is used instead.
With an API
like that, we can execute SQL
command in a strongly-typed way, often with only a single line of code.
An SqlExecutor
can be created as follows:
ISqlExecutor sqlExecutor = SqlExecutorFactory.CreateSqlExecutor(
SqlSourceTypeEnum.EmbeddedResource, connection, transaction);
We passed the SqlConnection
and SqlTransaction
to it.
Then you can call a method that executes the SQL
:
sqlExecutor.ExecuteNonQuery(SqlEnum.Ingredient_UpdateName, new { id, name });
The method names, like ExecuteNonQuery
, are similar to those of the SqlCommands
in the .NET Framework
.
SQL
parameters can be passed along as an anonymous type:
new { id, name }
The name and type of id
and name
correspond to the parameters of the SQL
. You do not need to use an anonymous type. You can use any object. As long as its properties correspond to the SQL
parameters:
var ingredient = new IngredientDto
{
ID = 10,
Name = "My ingredient"
};
sqlExecutor.ExecuteNonQuery(SqlEnum.Ingredient_UpdateName, ingredient);
The parameter names are not case sensitive.
You can also retrieve records as a collection of strongly typed objects:
IList<IngredientDto> records =
sqlExecutor.ExecuteReader<IngredientDto>(SqlEnum.Ingredient_GetAll).ToArray();
foreach (IngredientDto record in records)
{
// ...
}
The column names in the SQL
are case sensitive!
If you use SqlExecutor
in combination with NHibernate
you might want to
use the NHibernateSqlExecutorFactory
instead of the default SqlExecutorFactory
:
ISession session = ...;
ISqlExecutor sqlExecutor =
NHibernateSqlExecutorFactory.CreateSqlExecutor(
SqlSourceTypeEnum.EmbeddedResource, session);
This version uses an ISession
. In order for the SQL
to run in the same transaction as NHibernate
, we made it aware of its ISession
.
An implementation of NHibernateSqlExecutorFactory
can be found in JJ.Framework.Data.NHibernate
.
(This feature might not be available in the JJ.Framework
.)
It might be a good choice to include the SQL
as an embedded resource, but you can also use loose files:
Here is code to create the SqlExecutor
and execute the SQL
file:
ISqlExecutor sqlExecutor =
NHibernateSqlExecutorFactory.CreateSqlExecutor(
SqlSourceTypeEnum.FileName, session);
sqlExecutor.ExecuteNonQuery(@"Sql\Ingredient_Update.sql", new { id, name });
So the SqlEnum
cannot be used here. Youโd use a (relative) file path.
(This feature might not be available in the JJ.Framework
.)
It is not recommended to use SQL
strings in your code. But it is possible all the same using code like this:
ISqlExecutor sqlExecutor =
NHibernateSqlExecutorFactory.CreateSqlExecutor(
SqlSourceTypeEnum.String, session);
sqlExecutor.ExecuteNonQuery(
"update Ingredient set Name = @name where ID = @id",
new { id, name });
In that case no SQL
files have to be included in your project.
But it might make it harder to track down all the SQL
of your project and optimize it. Using SQL
strings may also circumvent another layer of protection against SQL
injection attacks.
SQL
string
concatenation is sort of a no-no, because it removes a layer of protection against SQL
injection attacks. SqlClient
has SqlParameters
from .NET
to prevent unwanted insertion of scripting. SqlExecutor
from JJ.Framework
uses SqlParameters
under the hood, to offer the same kind of protection. This encodes the parameters, so that they are recognized as simple types or string values rather than additional scripting.
Here is a trick to prevent the use of string
concatenation: When you want to filter something conditionally, depending on a parameter being filled in or not, then the following expression might be used in the SQL
scriptโs where
clause:
(@value is null or Value = @value)
But there might be exceptional cases where SQL
string concatenation would be favorable. Reasons to do so might include:
SQL
select
statement and wish to take the count
of it. String concatenation may prevent rewriting the SQL
statement twice, introducing a maintenance issue. Bugs would be awaiting as youโd have to change 2 SQL
scripts simultaneously, to make a change properly, which may easily be overlooked.string
concatenation might be helpful, is an SQL
script where you wish to include a database name or schema name.SQL
string concatenation might be used as an exception to the rule.One variation of SqlExecutor
included the ability to add placeholders to the SQL
files to insert additional scripting for this purpose. (This feature might not be available in the JJ.Framework
.)
The repository
pattern is used in this architecture.
The repository
pattern can be used together with JJ.Framework.Data
.
Using SQL
combined with repositories
can be simplified using the SqlExecutor
from JJ.Framework.Data.SqlClient
.
Here is some pseudo-code to demonstrate how it is put together:
FilterIDs.sql
select ID from MyEntity
where CategoryID = @categoryID
and MinStartDate >= @minStartDate
enum SqlEnum
{
FilterIDs
}
class MySqlExecutor
{
public var FilterIDs(int categoryID, DateTime minStartDate)
{
return SqlExecutor.ExecuteReader<int>(
SqlEnum.FilterIDs, new { categoryID, minStartDate });
}
}
class MyRepository : RepositoryBase
{
public var Filter(int categoryID, DateTime minStartDate)
{
var ids = MySqlExecutor.FilterIDs(categoryID, minStartDate);
var entities = ids.Select(x => Get(x));
return entities;
}
}
interface IMyRepository : IRepository
{
var Filter(int categoryID, DateTime minStartDate);
}
This would result in:
repository
.SQL
of all the entities behind an SqlExecutor
.repository interfaces
decoupling the persistence technology.It may seem overhead all the layers, but it might add up after adding more queries for more entities, that are either SQL
or ORM
queries. Of course you could skip layers, but this is how it is done in some of the JJ
projects.
You might also find a split up into separate assemblies:
MyProject.Data
MyProject.Data.EntityFramework
MyProject.Data.SqlClient
Separating the general things from the technology-specific things.
SQL
executed solely for database upgrading, might not be put in the main projects, but a project on the side. Suggestions of how to organize database upgrading might be found here.