При обучении обращению к базе данных есть две фундаментальные вещи, которые каждый программист должен делать: закрывать соединения и параметризировать запросы. Эти элементы отделены от фактического процесса выполнения оператора SQL и получения результатов, но они все еще абсолютно необходимы. По некоторым причинам, большинство учебных пособий, доступных в Интернете, просто замаскируют их или даже делают их просто неверными, возможно, потому что это настолько вторая натура для любого, кто достаточно продвинулся, чтобы написать учебник. Моя цель состоит в том, чтобы показать вам, как построить весь процесс, включая эти дополнительные основы, таким образом, чтобы упростить это понимание и делать это каждый раз правильно.
Первое, что нужно сделать, это осознать, что сокрытие кода доступа к данным в одном методе недостаточно: мы фактически хотим создать для этого отдельный класс (или даже библиотеку классов). Создав отдельный класс, мы можем сделать наш фактический метод соединения закрытым внутри этого класса, так что только другие методы в классе могут подключаться к базе данных. Таким образом, мы настроили привратник, который заставляет весь код базы данных в программе проходить через утвержденный канал. Сделайте правильный код привратника в отношении двух вопросов, о которых я говорил выше, и вся ваша программа также будет последовательно делать это правильно. Итак, вот наш старт:
public class DataLayer
{
private DbConnection GetConnection()
{
//This could also be a connection for OleDb, ODBC, Oracle, MySQL,
// or whatever kind of database you have.
//We could also use this place (or the constructor) to load the
// connection string from an external source, like a
// (possibly-encrypted) config file
return new SqlConnection("connection string here");
}
}
На данный момент мы не обратили внимания ни на один фундаментальный вопрос из введения. Все, что мы сделали до сих пор, - это настроили себя на написание кода, который позволит нам применять лучшие практики позже. Итак, начнем. Прежде всего, мы будем беспокоиться о том, как обеспечить закрытие ваших соединений. Мы делаем это, добавляя метод, который выполняет запрос, возвращает результаты и проверяет, закрыто ли соединение, когда мы закончим:
private DataTable Query(string sql)
{
var result = new DataTable();
using (var connection = GetConnection())
using (var command = new SqlCommand(sql, connection)
{
connection.Open();
result.Load(command.ExecuteReader(CommandBehavior.CloseConnection));
}
return result;
}
Вы можете добавить дополнительные похожие методы для возврата скалярных данных или вообще не возвращать данные (для обновлений / вставок / удалений). Пока не слишком привязывайтесь к этому коду, потому что он все еще не работает. Я объясню почему через минуту. А пока позвольте мне указать, что этот метод все еще закрыт. Мы еще не закончили, и поэтому мы не хотим, чтобы этот код был доступен для других частей вашей программы.
Еще одна вещь, которую я хочу выделить, это ключевое слово using
. Это ключевое слово является мощным способом объявления переменной в .Net и C #. Ключевое слово using
создает блок области видимости под объявлением переменной. В конце блока области видимости, ваша переменная расположена. Обратите внимание, что есть три важные части этого. Во-первых, это действительно относится только к неуправляемым ресурсам, таким как соединения с базой данных; память все еще собирается обычным способом. Во-вторых, переменная располагается , даже если выброшено исключение . Это делает ключевое слово подходящим для использования с чувствительными ко времени или жестко ограниченными ресурсами, такими как соединения с базой данных, без необходимости наличия отдельного блока try / catch поблизости. Последняя часть заключается в том, что ключевые слова используют шаблон IDisposable в .Net. Вам не нужно знать все об IDisposable прямо сейчас: просто знайте, что соединения с базой данных реализуют (думаю: наследуют) интерфейс IDisposable, и поэтому будут работать с блоком using.
Вам не нужно использовать ключевое слово using
в своем коде. Но если вы этого не сделаете, правильный способ обработки соединения выглядит так:
SqlConnection connection;
try
{
connection = new SqlConnection("connection string here");
SqlCommand command = new SqlCommand("sql query here", connetion);
connection.Open();
SqlDataReader reader = command.ExecuteReader();
//do something with the data reader here
}
finally
{
connection.Close();
}
Даже это все еще простая версия.Вам также нужна дополнительная проверка в блоке finally, чтобы убедиться, что ваша переменная соединения верна.Ключевое слово using
является гораздо более кратким способом выразить это, и оно гарантирует, что вы каждый раз получаете правильный шаблон.Здесь я хочу показать, что если вы просто позвоните по номеру connection.Close()
без защиты, чтобы убедиться, что программа действительно достигла этой линии, вы потерпели неудачу .Если ваш sql-код создает исключение без защиты try / finally или using, вы никогда не достигнете вызова .Close () и, следовательно, потенциально оставите соединение открытым.Делайте это достаточно часто, и вы можете заблокировать себя из своей базы данных!
Теперь давайте создадим что-то общедоступное: то, что вы действительно сможете использовать из другого кода.Как я уже упоминал ранее, каждый SQL-запрос, который вы пишете для приложения, будет проходить в своем собственном методе.Вот пример метода простого запроса для получения всех записей из вашей таблицы Employee:
public DataTable GetEmployeeData()
{
return Query("SELECT * FROM Employees");
}
Ого, это было просто ... однострочный вызов функции, и мы получили данные, возвращаемые избаза данных.Мы действительно куда-то добираемся.К сожалению, нам все еще не хватает одной части головоломки: вы видите, довольно редко хочется вернуть всю таблицу.Как правило, вы захотите каким-то образом отфильтровать эту таблицу и, возможно, объединить ее с другой таблицей.Давайте изменим этот запрос, чтобы он возвращал все данные для вымышленного сотрудника по имени «Фред»:
public DataTable GetFredsEmployeeData()
{
return Query("SELECT * FROM Employees WHERE Firstname='Fred'");
}
Все еще довольно легко, но это не соответствует духу того, что мы пытаемся выполнить.Вы не хотите создавать другой метод для каждого возможного имени сотрудника.Вы хотите что-то более похожее на это:
public DataTable GetEmployeeData(string FirstName)
{
return Query("SELECT * FROM Employees WHERE FirstName='" + FirstName + "'");
}
Э-э-э.Теперь у нас есть проблема.Есть такая надоедливая конкатенация строк, просто жду, когда кто-нибудь придет и введет текст ';Drop table employees;--
(или хуже) в поле FirstName в вашем приложении.Правильный способ справиться с этим - использовать параметры запроса, но здесь все становится сложнее, потому что несколько абзацев назад мы создали метод запроса, который принимает только готовую строку sql.
Многие люди хотят написать метод, подобный этому методу Query.Я думаю, что почти каждый программист базы данных испытывает соблазн этого паттерна в определенный момент своей карьеры, и, к сожалению, это просто неправильно, пока вы не добавите способ принимать данные параметров sql.К счастью, существует множество разных способов решения этой проблемы.Наиболее распространенным является добавление параметра к методу, который позволяет нам передавать данные sql для использования.Для этого мы могли бы передать массив объектов SqlParameter, коллекцию пар ключ / значение или даже просто массив объектов.Любого из них было бы достаточно, но я думаю, что мы можем добиться большего успеха.
Я провел много времени, работая над различными вариантами, и я сузил то, что я считаю самым простым, наиболее эффективными (что более важно) наиболее точный и поддерживаемый вариант для C #.К сожалению, для этого требуется, чтобы вы понимали синтаксис еще одной расширенной языковой функции в C #: анонимные методы / лямбды (на самом деле: делегаты, но я скоро покажу лямбду).Эта функция позволяет вам определять функции внутри другой функции, удерживать ее с помощью переменной, передавать ее другим функциям и вызывать ее на досуге.Это полезная возможность, которую я постараюсь продемонстрировать.Вот как мы изменим исходную функцию Query (), чтобы воспользоваться этой возможностью:
private DataTable Query(string sql, Action<SqlParameterCollection> addParameters)
{
var result = new DataTable();
using (var connection = GetConnection())
using (var command = new SqlCommand(sql, connection)
{
//addParameters is a function we can call that was as an argument
addParameters(command.Parameters);
connection.Open();
result.Load(command.ExecuteReader(CommandBehavior.CloseConnection));
}
return result;
}
Обратите внимание на новый параметр Action<SqlParameterCollection>
.Не против часть < >
.Если вы не знакомы с шаблонами, вы можете просто притвориться, что это часть имени класса.Важно то, что этот специальный тип Action позволяет нам передавать одну функцию (в данном случае такую, которая принимает SqlParameterCollection в качестве аргумента) в другую функцию.Вот как это выглядит при использовании нашей функции GetEmployeeData ():
public DataTable GetEmployeeData(string firstName)
{
return Query("SELECT * FROM Employees WHERE FirstName= @Firstname",
p =>
{
p.Add("@FirstName", SqlDbType.VarChar, 50).Value = firstName;
});
}
Ключом ко всему этому является то, что функция Query () теперь имеет возможность соединить аргумент firstName
, передаваемый ее родительской функции GetEmployeeData (), с выражением @FirstName в строке sql.Это делается с помощью функций, встроенных в ADO.Net и ядро вашей базы данных sql.Что наиболее важно, это происходит таким образом, что предотвращает любую возможность атак SQL инъекций.Опять же, этот странный синтаксис не единственный допустимый способ отправки данных параметров.Возможно, вам будет намного удобнее, просто отправляя коллекцию, которую вы повторяете.Но я думаю, что этот код хорошо справляется с сохранением кода параметра рядом с кодом запроса, избегая при этом лишнего рабочего построения и последующей итерации (перестройки) данных параметров.
Я закончу (наконец-то!) Двумякороткие предметы.Первый - это синтаксис для вызова нового метода запроса без параметров:
public DataTable GetAllEmployees()
{
return Query("SELECT * FROM Employees", p => {});
}
Хотя мы могли бы также предоставить это как перегрузку исходной функции Query (), в моем собственном коде я предпочитаю не делать этогочто, как я хочу сообщить другим разработчикам, что они должны стремиться параметризировать свой код, а не разбираться с конкатенацией строк.
Во-вторых, код, описанный в этом ответе, все еще не завершен.Есть некоторые важные недостатки, которые еще предстоит устранить.Например, использование таблицы данных, а не устройства чтения данных вынуждает вас загружать весь набор результатов из каждого запроса в память одновременно.Есть вещи, которые мы можем сделать, чтобы избежать этого.Мы также не обсуждали вставки, обновления, удаления или изменения, и мы не рассматривали, как комбинировать сложные ситуации с параметрами, где мы можем захотеть, скажем, добавить код, чтобы также фильтровать по фамилии, но только если данныедля фильтра фамилии был фактически доступен от пользователя.Хотя это может быть легко адаптировано для всех этих сценариев, я думаю, что на данный момент я выполнил первоначальную задачу, и поэтому я оставлю это читателю.
В заключение, запомните две вещи, которые вы must do: закрыть ваши соединения через блок finally и параметризировать ваши запросы.Надеюсь, этот пост поможет вам сделать это хорошо.