Как улучшить метод выбора уровня доступа к данным
В последнее время я нахожу, что я пишу методы выбора уровня доступа к данным, где весь код принимает эту общую форму:
public static DataTable GetSomeData( ... arguments)
{
string sql = " ... sql string here: often it just a stored procedure name ... ";
DataTable result = new DataTable();
// GetOpenConnection() is a private method in the class:
// it manages the connection string and returns an open and ready connection
using (SqlConnection cn = GetOpenConnection())
using (SqlCommand cmd = new SqlCommand(sql, cn))
{
// could be any number of parameters, each with a different type
cmd.Parameters.Add("@Param1", SqlDbType.VarChar, 50).Value = param1; //argument passed to function
using (SqlDataReader rdr = cmd.ExecuteReader())
{
result.Load(rdr);
}
}
return result;
}
Или вот так:
public static DataRow GetSomeSingleRecord( ... arguments)
{
string sql = " ... sql string here: often it just a stored procedure name ... ";
DataTable dt = new DataTable();
// GetOpenConnection() is a private method in the class:
// it manages the connection string and returns an open and ready connection
using (SqlConnection cn = GetOpenConnection())
using (SqlCommand cmd = new SqlCommand(sql, cn))
{
// could be any number of parameters, each with a different type
cmd.Parameters.Add("@Param1", SqlDbType.VarChar, 50).Value = param1; //argument passed to function
using (SqlDataReader rdr = cmd.ExecuteReader(CommandBehavior.SingleRow))
{
dt.Load(rdr);
}
}
if (dt.Rows.Count > 0)
return dt.Rows[0];
return null;
}
Эти методы будут вызываться кодом бизнес-уровня, который затем преобразует базовый DataTable или DataRecord в строго типизированные бизнес-объекты, которые может использовать уровень представления.
Так как я использую аналогичный код несколько раз, я хочу убедиться, что этот код лучше всего. Итак, как его можно улучшить? И стоит ли пытаться переместить общий код из этого метода в свой собственный. Если да, то каким будет выглядеть этот метод (в частности, для передачи коллекции SqlParameter)?
Ответы
Ответ 1
Пришлось добавить свои собственные:
Возвращает DataReader из DataLayer в инструкции using
Новый шаблон позволяет мне иметь только одну запись в памяти за раз, но все равно заключает в себя соединение в симпатичном "использовании":
public IEnumerable<T> GetSomeData(string filter, Func<IDataRecord, T> factory)
{
string sql = "SELECT * FROM [SomeTable] WHERE SomeColumn= @Filter";
using (SqlConnection cn = new SqlConnection(GetConnectionString()))
using (SqlCommand cmd = new SqlCommand(sql, cn))
{
cmd.Parameters.Add("@Filter", SqlDbType.NVarChar, 255).Value = filter;
cn.Open();
using (IDataReader rdr = cmd.ExecuteReader())
{
while (rdr.Read())
{
yield return factory(rdr);
}
rdr.Close();
}
}
}
Ответ 2
Один образец, который мне понравился, выглядит так, как клиентский код:
DataTable data = null;
using (StoredProcedure proc = new StoredProcedure("MyProcName","[Connection]"))
{
proc.AddParameter("@LoginName", loginName);
data = proc.ExecuteDataTable();
}
Я обычно делаю подключение необязательным, и я буду кодировать, чтобы вытащить его из раздела конфигурации ConnectStrings или рассматривать его как фактическую строку соединения. Это позволяет мне повторно использовать dal в одном сценарии и частично является привычкой из дней COM +, когда я сохранил строку соединения, используя свойство построения объекта.
Мне это нравится, потому что он легко читается и скрывает от меня весь код ADO.
Ответ 3
Единственное, что я делаю по-другому, - это переключение с моих собственных внутренних вспомогательных методов базы данных на фактический блок приложений доступа к данным http://msdn.microsoft.com/en-us/library/cc309504.aspx
Делает его более стандартизированным/единообразным для других разработчиков, которые знают, что корпоративная библиотека наращивает код.
Ответ 4
Существует так много способов реализовать DBAL, на мой взгляд, вы на правильном пути. Что нужно учитывать при реализации:
- Вы используете метод factory-like для создания вашего SqlConnection, это незначительная точка, но вы можете сделать то же самое для своей SqlCommand.
- Длина параметра не обязательна, поэтому вы действительно можете оставить ее вне вызова Parameter.Add.
- Создайте методы для добавления параметров, пример кода ниже.
Добавьте параметры с помощью DbUtil.AddParameter(cmd, "@Id", SqlDbType.UniqueIdentifier, Id);
internal class DbUtil {
internal static SqlParameter CreateSqlParameter(
string parameterName,
SqlDbType dbType,
ParameterDirection direction,
object value
) {
SqlParameter parameter = new SqlParameter(parameterName, dbType);
if (value == null) {
value = DBNull.Value;
}
parameter.Value = value;
parameter.Direction = direction;
return parameter;
}
internal static SqlParameter AddParameter(
SqlCommand sqlCommand,
string parameterName,
SqlDbType dbType
) {
return AddParameter(sqlCommand, parameterName, dbType, null);
}
internal static SqlParameter AddParameter(
SqlCommand sqlCommand,
string parameterName,
SqlDbType dbType,
object value
) {
return AddParameter(sqlCommand, parameterName, dbType, ParameterDirection.Input, value);
}
internal static SqlParameter AddParameter(
SqlCommand sqlCommand,
string parameterName,
SqlDbType dbType,
ParameterDirection direction,
object value
) {
SqlParameter parameter = CreateSqlParameter(parameterName, dbType, direction, value);
sqlCommand.Parameters.Add(parameter);
return parameter;
}
}
Ответ 5
Во-первых, я думаю, вы уже рассмотрели использование ORM против вашего собственного. Я не буду вдаваться в это.
Мои мысли о сворачивании вашего собственного кода доступа к данным:
- С течением времени мне было проще не иметь отдельные объекты DAL/BL, а скорее объединить их в один объект (через некоторое время после достижения этого вывода я узнал его довольно хорошо известный шаблон, а именно ActiveRecord). Это может выглядеть красиво и развязано, чтобы иметь отдельные сборки DAL, но накладные расходы на обслуживание будут складываться. Каждый раз, когда вы добавляете новую функцию, вам нужно будет создать больше кода/изменить другие классы. По моему опыту, команда, которая поддерживает приложение, часто намного меньше, чем первоначальная команда разработчиков, которая ее построила, и они будут ненавидеть дополнительную работу.
- Для крупных команд может иметь смысл отделить DAL (и пусть группа работает над ней, а остальные), но это создает хороший стимул для раздувания кода.
- Возвращаясь к вашему конкретному образцу: как вы используете результирующий DataTable? Итерировать строки, создавать типизированные объекты и получать данные из строки? Если да, подумайте о дополнительном DataTable, который вы создали, только для перемещения данных между DAL и BL. Почему бы не взять его непосредственно из DataReader?
- Также о примере: если вы вернете нетипизированный DataTable, то, я думаю, вы должны использовать имена столбцов (из набора результатов, который возвращает вызов SP) в вызывающем коде. Это означает, что если мне нужно что-то изменить в базе данных, это может повлиять на оба уровня.
Мое предложение (я пробовал оба метода - предложение - это новейший рабочий подход, с которым я столкнулся - со временем он эволюционировал).
- Создайте базовый класс для типизированных бизнес-объектов.
- Сохранять состояние объекта в базовом классе (новый, измененный и т.д.)
- Поместите основные методы доступа к данным в этом классе, как статические методы. С небольшим усилием (подсказка: общие методы + Activator.CreateInstance) вы можете создать один бизнес-объект для каждой строки, возвращенной в считыватель.
- сделать абстрактный метод в бизнес-объекте для синтаксического анализа данных строки (непосредственно из DataReader!) и заполнить объект.
- создавать статические методы в производных бизнес-объектах, которые готовят хранимые параметры процесса (в зависимости от различных критериев фильтра) и вызывают общие методы доступа к данным из базового класса.
Цель состоит в том, чтобы закончить использование, например:
List<MyObject> objects = MyObject.FindMyObject(string someParam);
Преимущество для меня состояло в том, что мне нужно только изменить один файл, чтобы справиться с изменениями в именах столбцов базы данных, типах и т.д. (небольшие изменения вообще). С некоторыми хорошо продуманными областями вы можете организовать код так, чтобы они были отдельными "слоями" в одном и том же объекте:). Другое преимущество заключается в том, что базовый класс действительно многократно используется из одного проекта в другой. И разбухание кода минимально (ну, по сравнению с преимуществами. Вы также можете заполнить наборы данных и привязать их к элементам управления пользовательского интерфейса: D
Ограничения - вы получаете один класс для каждого объекта домена (обычно для каждой основной таблицы базы данных). И вы не можете загружать объекты в существующие транзакции (хотя вы могли подумать о передаче транзакции, если у вас есть).
Сообщите мне, если вас интересует более подробная информация. Я мог бы немного расширить ответ.
Ответ 6
Подобно тому, что я разместил здесь
public IEnumerable<S> Get<S>(string query, Action<IDbCommand> parameterizer,
Func<IDataRecord, S> selector)
{
using (var conn = new T()) //your connection object
{
using (var cmd = conn.CreateCommand())
{
if (parameterizer != null)
parameterizer(cmd);
cmd.CommandText = query;
cmd.Connection.ConnectionString = _connectionString;
cmd.Connection.Open();
using (var r = cmd.ExecuteReader())
while (r.Read())
yield return selector(r);
}
}
}
У меня есть эти простые методы расширения, чтобы облегчить вызов:
public static void Parameterize(this IDbCommand command, string name, object value)
{
var parameter = command.CreateParameter();
parameter.ParameterName = name;
parameter.Value = value;
command.Parameters.Add(parameter);
}
public static T To<T>(this IDataRecord dr, int index, T defaultValue = default(T),
Func<object, T> converter = null)
{
return dr[index].To<T>(defaultValue, converter);
}
static T To<T>(this object obj, T defaultValue, Func<object, T> converter)
{
if (obj.IsNull())
return defaultValue;
return converter == null ? (T)obj : converter(obj);
}
public static bool IsNull<T>(this T obj) where T : class
{
return (object)obj == null || obj == DBNull.Value;
}
Итак, теперь я могу позвонить:
var query = Get(sql, cmd =>
{
cmd.Parameterize("saved", 1);
cmd.Parameterize("name", "abel");
}, r => new User(r.To<int>(0), r.To<string>(1), r.To<DateTime?>(2), r.To<bool>(3)));
foreach (var user in query)
{
}
Это полностью общее, подходит для любой модели, соответствующей интерфейсам ado.net. Объект подключения и считыватель расположены только после перечисления коллекции один раз.
Ответ 7
Простейшее решение:
var dt=new DataTable();
dt.Load(myDataReader);
list<DataRow> dr=dt.AsEnumerable().ToList();