Обзор LINQ to SQL

78

К этому моменту уже рассказывалось об использовании LINQ с находящимися в памяти коллекциями данных и массивами, XML и DataSet. Теперь перейдем к тому, что многим кажется наиболее веской причиной для использования LINQ — LINQ to SQL.

LINQ to SQL — это API-интерфейс для работы с базами данных SQL Server.

В современном мире, где господствуют объектно-ориентированные языки программирования, существует несоответствие между языком программирования и реляционной базой данных. При написании приложения мы моделируем классы как представления объектов реального мира, таких как заказчики, счета, политики и полеты. Нам нужен способ сохранения этих объектов, чтобы при перезапуске приложения все эти объекты с их данными не были потеряны. Однако большинство баз данных промышленного масштаба остаются реляционными и хранят свою информацию в виде записей в таблицах, а не в виде объектов. Пользовательский класс может содержать несколько адресов и номеров телефонов, хранящихся в коллекциях, которые являются дочерними свойствами класса заказчика; при сохранении такая информация, скорее всего, распределится по множеству таблиц, например, в таблицу заказчиков, таблицу адресов и таблицу телефонов.

Вдобавок типы данных, поддерживаемые языком приложения, отличаются от типов базы данных. Разработчикам приходится писать собственный код, который загружает и сохраняет объекты заказчиков из соответствующих таблиц, обрабатывая необходимые преобразования данных между языком приложения и базой данных. Это утомительный, и часто подверженный ошибкам процесс. Наличие упомянутых проблем объектно-реляционного отображения (object-relational mapping — ORM), часто называемых объектно-реляционной потерей соответствия (object-relational impedance mismatch), в течение ряда лет привело к появлению широкого разнообразия готовых программных ORM-решений. LINQ to SQL — это реализация ORM начального уровня от Microsoft на основе LINQ, предназначенная для работы с SQL Server.

Обратите внимание — для работы с SQL Server. API-интерфейс LINQ to SQL предназначен исключительно для SQL Server! Однако существуют реализации LINQ от независимых поставщиков, поддерживающие наиболее распространенные базы данных, включая Oracle, DB2, MySQL, SqlLite и другие.

Также учтите, что LINQ to SQL — реализация ORM начального уровня. Если вы найдете ее недостаточно мощной или гибкой, чтобы отвечать имеющимся требованиям, попробуйте обратиться к API-интерфейсу LINQ to Entities.

Большинство инструментов ORM пытаются абстрагировать физическую базу данных в виде бизнес-объектов. С такой абстракцией иногда теряется возможность выполнения запросов SQL, составляющих значительную часть абстракции реляционных баз данных. Именно это отличает LINQ to SQL от большинства его аналогов. Мы не только получаем удобство в виде бизнес-объектов, которые отображаются на базу данных, но также получаем полноценный язык запросов, подобный хорошо знакомому SQL.

В общем, LINQ to SQL - инструмент ORM начального уровня, позволяющий выполнять мощные SQL-запросы.

В дополнение к предоставлению возможностей запросов LINQ, до тех пор, пока запрос возвращает сущностные объекты LINQ to SQL, в противоположность одиночным полям, именованным не сущностным классам или анонимным классам, LINQ to SQL также предлагает средства отслеживания изменений и обновлений базы данных, построенных на основе оптимистической модели обнаружения и разрешения конфликтов параллельного доступа, а также транзакционной целостности.

LINQ to SQL — сложная тема, и любые примеры требуют участия многих элементов LINQ to SQL. Более подробно все эти концепции будут рассматриваться в последующих статьях, посвященных LINQ to SQL.

DataContext

DataContext — это класс, устанавливающий соединение с базой данных. Он также предоставляет несколько служб, обеспечивающих отслеживание идентичности, отслеживание изменений и обработку этих изменений. DataContext — это класс, соединяющий с базой данных, отслеживающий то, что мы изменяем, и обновляющий базу данных при вызове метода SubmitChanges.

В LINQ to SQL принято использовать класс, производный от DataContext. Имя производного класса обычно совпадает с именем базы данных, на которую он отображается. В примерах LINQ to SQL на этот производный класс мы часто будем ссылаться как на [Your]DataContext, поскольку его имя зависит от базы данных, для которой он создан.

В приводимых примерах производный от DataContext класс будет называться Northwind. Для его построения применялся инструмент SqlMetal, входящий в состав Visual Studio 2010, который генерирует классы отображения на основе базы данных SQL Server.

Этот производный от DataContext класс, [Your]DataContext, обычно будет иметь общедоступное свойство Таblе<Т> для каждой таблицы базы данных, которая отображается на базу данных, где T — тип сущностного класса, экземпляр которого создается для каждой извлеченной записи из конкретной таблицы базы данных.

Тип данных Table<T> представляет собой специализированную коллекцию. Например, поскольку в базе Northwind присутствует таблица Customers, класс Northwind, унаследованный от класса DataContext, будет иметь Table<Customers> по имени Customers. Это значит, что обращаться к записям в таблице Customers базы данных можно, непосредственно обращаясь к свойству Customers типа Table<Customers> в классе Northwind.

Сущностные классы

LINQ to SQL подразумевает использование сущностных классов, причем каждый из них обычно отображается на единственную таблицу базы данных. Однако, используя наследуемое отображение сущностных классов, при определенных условиях можно отобразить целую иерархию классов на единственную таблицу. Таким образом, мы имеем сущностные классы, отображенные на таблицы базы данных, и свойства сущностных классов, отображенные на столбцы этих таблиц. Это отображение "класс-таблица" и "свойство-столбец" являются сутью LINQ to SQL.

Квинтэссенция LINQ to SQL заключается в отображении сущностных классов на таблицы базы данных и свойств сущностных классов на столбцы таблиц базы данных.

Такое отображение может возникать непосредственно в исходных файлах класса за счет оснащения их соответствующими атрибутами или же может быть задано во внешнем XML-файле отображения. При использовании такого внешнего файла специфичная для LINQ to SQL информация может храниться отдельно от исходного кода. Это может быть очень удобно, если нет исходного кода либо если нужно хранить код отдельно от LINQ to SQL.

В большинстве примеров, посвященных LINQ to SQL, используются сущностные классы, сгенерированные инструментом командной строки SQLMetal. Этот инструмент генерирует сущностные классы с информацией об отображении LINQ to SQL, встроенной непосредственно в генерируемые исходные модули. Эта информация представлена в форме атрибутов и их свойств.

Сущностные классы имеют имя в форме единственного числа от имени таблицы базы данных Northwind. Например, класс по имени Customer. Поскольку Customer— форма единственного числа от Customers, а в базе данных Northwind есть таблица по имени Customers, это указывает на то, что Customer — сущностный класс для таблицы Customers из базы данных Northwind.

Инструмент командной строки SQLMetal поддерживает опцию /pluralize, которая вызывает именование сущностного класса в форме единственного числа имени таблицы базы данных. Если при генерации сущностных классов эта опция не указана, то класс получил бы имя Customers, а не Customer, потому что таково имя таблицы — Customers. Не забывайте, что сущностный класс может иметь имя как во множественном, так и в единственном числе.

Ассоциации

Ассоциация (association) — это термин, используемый для назначения первичного ключа для отношений внешнего ключа между двумя сущностными классами. В отношении "один ко многим" результатом ассоциации является то, что родительский класс — тот, что содержит первичный ключ — включает коллекцию дочерних классов, т.е. классов, имеющих внешний ключ. Эта коллекция сохраняется в приватной переменной-члене типа EntitySet<T>, где T будет типом дочернего сущностного класса.

Например, в сущностном классе Customer, сгенерированном инструментом командной строки SQLMetal для базы данных Northwind, есть приватный член типа EntitySet<Order>, названный _Orders, который содержит все объекты Order для определенного объекта Customer:

private EntitySet<Order> _Orders;

SQLMetal также сгенерировал общедоступное свойство по имени Orders, используемое для доступа к приватной коллекции Orders.

На другом конце отношения — в классе, содержащем внешний ключ, — находится ссылка на родительский класс, поскольку это отношение "многие к одному". Эта ссылка сохраняется в приватной переменной-члене типа EntityRef<T>, где T — родительский класс (здесь термин "родительский" относится к ассоциации, а не к наследованию).

В сгенерированных сущностных классах Northwind сущностный класс Order содержит приватную переменную-член типа EntityRef<Customer> по имени _Customer:

private EntityRef<Customer> _Customer;

И снова SQLMetal сгенерировал общедоступное свойство по имени Customer для доступа к ссылке на родителя.

Ассоциация, первичные и внешние ключи, а также направление отношения задаются атрибутами и свойствами атрибутов в исходном модуле сгенерированных сущностных классов.

Преимущество ассоциации состоит в возможности доступа к дочерним объектам родительских и, таким образом, к записям базы данных, с той же легкостью, как к любому свойству родительского объекта. Аналогично доступ к родительскому объекту из дочернего заключается в обращении к соответствующему свойству дочернего объекта.

Обнаружение конфликтов параллельного доступа

Одной из ценных служб, предоставляемых вам DataContext, является обработка изменений. Когда вы пытаетесь обновить базу данных вызовом метода SubmitChanges класса DataContext, он автоматически выполняет оптимистическое обнаружение конфликтов параллельного доступа.

Если конфликт обнаружен, генерируется исключение ChangeConflictException. Всякий раз, когда вы вызываете метод SubmitChanges, то должны помещать его в блок try/catch и перехватывать исключение ChangeConflictException. Это — правильный способ обнаружения конфликтов параллельного доступа.

Разрешение конфликтов параллельного доступа

Как только конфликт параллелизма обнаружен, следующий шаг состоит в его разрешении. Это можно сделать несколькими способами.

Например, за счет вызова метода ResolveAll коллекции ChangeConflicts унаследованного класса DataContext, когда перехвачено исключение ChangeConflictException.

Опять-таки, во многих примерах по LINQ to SQL, не включен код для обнаружения и разрешения конфликтов, но вы всегда должны его предусматривать в реальном коде.

Пройди тесты
Лучший чат для C# программистов