LINQ to SQL - отношения «многие ко многим»
Установка
Мы будем работать с широко используемым отношением между пользователями и ролями. Пользователь может иметь множество ролей, и роли могут принадлежать множеству пользователей. Мы будем использовать примерную базу данных, содержащую три таблицы, которые составляют данное отношение. Вот как выглядит отношение Пользователь/Роль:
LINQ to SQL создаст по сущности для каждой из данных таблиц без прямой связи между Пользователем (User) и Ролями (Role), хотя отношение на самом деле состоит между ролями и пользователями. Для получения ролей пользователя, мы должны пройти через список UserRole и получить соответствующие роли.
// db является экземпляром DataContext
var user = db.Users.FirstOrDefault(u => u.Id = 1);
var roles = new Roles();
foreach(var userRole in db.Users.UserRoles)
roles.Add(userRole.Role);
Данную работу мы хотели бы передать на выполнение объектно-реляционной проекции. Все, что мы хотим, это найти роли определенного пользователя. Принудительное прохождение данного процесса для каждого отношения типа "многие ко многим" утомительно и разочаровывает, особенно притом что данные отношения довольно часто встречаются. Нам необходимо напрямую видеть связь между данными сущностями, поскольку нас волнует именно это отношение. Итак, нам необходимо придумать способ обработки такой связи.
Реализация
Используя дизайнер LINQ to SQL мы создадим сущности, которые соответствуют данным таблицам. Как только будет создан язык разметки базы данных, нам необходимо обновить его. Нужно добавить атрибут DeleteOnNull для каждой ассоциации в сущности UserRole. Поскольку дизайнер не поддерживает управление данными атрибутами, нам придется вручную обновить язык DBML таким образом, чтобы данное обновление было возможно. Для прямого обновления DBML вам стоит щелкнуть правой кнопкой мышки по DBML-файлу, выбрать опцию "Открыть с помощью..." (Open with) и выбрать редактор XML Editor. Как только вы откроете DBML-файл, найдите в нем сущность UserRole. Нам необходимо добавить атрибут DeleteOnNull каждой ассоциации и установить его в true.
<Table Name="dbo.UserRole" Member="UserRoles">
<Type Name="UserRole">
<Column Name="UserId" Type="System.Int32"
DbType="Int NOT NULL" IsPrimaryKey="true" CanBeNull="false">
</Column>
<Column Name="RoleId" Type="System.Int32"
DbType="Int NOT NULL" IsPrimaryKey="true" CanBeNull="false">
</Column>
<Association Name="Role_UserRole" Member="Role"
ThisKey="RoleId" OtherKey="Id" Type="Role"
IsForeignKey="true" DeleteOnNull="true" >
</Association>
<Association Name="User_UserRole" Member="User"
ThisKey="UserId" OtherKey="Id" Type="User"
IsForeignKey="true" DeleteOnNull="true" />
</Association>
</Type>
</Table>
Установка DeleteOnNull в true позволяет удалять объект тогда, когда ассоциация будет установлена в null. После обновления и сохранения DBML-файла щелкните правой кнопкой мышки по DBML-файлу в обозревателе решения (Solution Explorer) и выберите опцию запуска специализированного инструмента (Run Custom Tool). Это повторно создат сущность UserRole со всеми обновлениями.
Нам необходима возможность управления списком ролей (Roles), которые связаны с пользователями (User), но без необходимости в управлении перекрестной таблицы, которая представляет собой данное отношение. Итак, нам понадобится список ролей (Roles), а также возможность обрабатывать добавление и удаление ролей из отношения. Давайте начнем с составления списка, поэтому мы добавим следующее свойство Roles к объекту User:
private System.Data.Linq.EntitySet _roles;
public System.Data.Linq.EntitySet<role> Roles
{
get
{
if (_roles == null)
{
_roles = new System.Data.Linq.EntitySet<role>(OnRolesAdd, OnRolesRemove);
_roles.SetSource(UserRoles.Select(c => c.Role));
}
return _roles;
}
set
{
_roles.Assign(value);
}
}
Как вы могли уже удостовериться, мы используем данные из сущности UserRole на заднем плане для обработки отношения. Это отдаляет нас от работы с отношениями типа "многие ко многим". Свойство Roles является набором EntitySet , что позволяет упростить установку источника связи Roles , определенной в UserRole, и использовать свойство так же, как если бы вы использовали любое другое EntitySet в LINQ to SQL. Создание данного свойства позволит нам написать следующее:
user.Roles
Тем не менее, мы еще не закончили - нам необходима возможность добавлять и удалять роли и соответственно обновлять базу данных. Когда новый экземпляр EntitySet<role> будет создан, мы может предоставить обработчиков для данных действий. Для обработки нашего случая мы создали обработчики событий OnRolesAdd и OnRolesRemove.
[System.Diagnostics.DebuggerNonUserCode]
private void OnRolesAdd(Role entity)
{
this.UserRoles.Add(new UserRole { User = this, Role = entity });
SendPropertyChanged(null);
}
[System.Diagnostics.DebuggerNonUserCode]
private void OnRolesRemove(Role entity)
{
var userRole = this.UserRoles.FirstOrDefault(
c => c.UserId == Id
&& c.RoleId == entity.Id);
this.UserRoles.Remove(userRole);
SendPropertyChanged(null);
}
Сложные операции, необходимые для отношений "многие ко многим", будут абстрагированы потому, что мы управляем необходимыми действиями в обработчиках события. Когда к списку будет добавлена роль (Role), мы также добавим объект UserRole к списку UserRole , который содержит соответствующие записи объектов User и Role , составляющих данное отношение. Когда экземпляр роли будет удален (Role), мы найдем соответствующий объект в списке UserRole и удалим его. Теперь, когда у нас есть код, мы можем с легкостью обработать отношения "многие ко многим" и продемонстрируем это далее.
Мы создали два теста, которые добавляют и удаляют данную роль. Сперва мы рассмотрим добавление - все что нам необходимо, так это добавить Role в список Roles и выполнить SubmitChanges().
var role = db.Roles.First(r => r.Id == 1);
var user = db.Users.First(u => u.Id == 1);
user.Roles.Add(role);
db.SubmitChanges();
После теста добавления данные в базе данных будут выглядеть так:
Теперь нам необходимо удалить Role из User.
using (var db = new SampleDataContext())
{
var user = db.Users.First(u => u.Id == 1);
var role = user.Roles.First(r => r.Id == 1);
user.Roles.Remove(role);
db.SubmitChanges();
}
База данных теперь демонстрирует отсутствие связи.
Вывод
Может добавление поддержки отношения "многие ко многим" кажется не таким сложным, тем не менее свойство и обработчики событий, обсужденные здесь, должны быть добавлены к каждому отношению "многие ко многим". После того, как вы выполните это несколько раз вручную, вы, скорее всего, устанете от этого. PLINQO исследует каждое отношение в базе данных и выработает свойство и обработчики события, необходимые для обработки отношений типа "многие ко многим" таким образом, как вы ожидали выполнение этого от LINQ to SQL. Это означает, что вам не стоит беспокоиться о том, что необходимо для обработки данного типа отношений. PLINQO уже об этом позаботился.
При использовании PLINQO все что вам необходимо, так это генерация кода и способность наслаждаться использованием LINQ to SQL , который поддерживает отношения "многие ко многим".
Авторы: Eric J. Smith, Shannon Davidson