LINQ to SQL - отделение сущностей при помощи Detach

ОГЛАВЛЕНИЕ

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

Подготовка

Мы будем использовать базу данных Petshop для демонстрации отделения сущностей LINQ to SQL. Колонка RowVersion типа timestamp была добавлена в каждую таблицу в базе данных Petshop. LINQ to SQL использует колонку RowVersion для выполнения оптимистичной проверки совпадений и не позволит вам добавить сущности к DataContext без колонки RowVersion или установления UpdateCheck в Never (по умолчанию - значение равно Always) для каждого свойства сущности в DBML.

Отделение из DataContext

Чтобы обеспечить полное отделение сущности, нам необходимо изучить каждую дочернюю сущность, список дочерних сущностей и каждое свойство, которое загружается с опозданием (delay) или из отложенной списковой структуры (lazy). Мы пройдемся по процессу реализации отделения (detach) по сущности Product. Вот как выглядит сущность Product и все ее зависимости в дизайнере DBML.

 

Сущность Product содержит в себе все различные сценарии, с которыми мы встретимся при отделении сущностей. Category является дочерней сущностью , Item является списком дочерних сущностей и мы настроили Descn на загрузку типа delay или lazy.

Для отделения свойства Product, первым шагом является создание частичного класса Product. Чтобы создать частичный класс для сущности Product щелкните правой кнопкой мыши по сущности Product в дизайнере LINQ to SQL, выберите "View Code" (просмотреть код) и частичный класс будет создан для сущности Product. Мы добавим следующий метод в частичный класс Product:

partial class Product
{
    public override void Detach()
    {
        if (null == PropertyChanging)
            return;

        PropertyChanging = null;
        PropertyChanged = null;
    }
}

Во-первых совершается проверка на то, что сущность привязана к контексту. Это может показаться неким хакерством, но сущности LINQ to SQL участвуют в оповещениях об изменении DataContext посредством их обработчиков событий PropertyChanged и PropertyChanging. DataContext из LINQ to SQL отслеживает объекты используя интерфейсы INotifyPropertyChanging и INotifyPropertyChanged. Это означает что обработчики PropertyChanged и PropertyChanging управляют привязкой к DataContext. Проверка удостоверится в том, что событие обрабатывается и не оповестит о том, что сущность привязана к datacontext.

if (null == PropertyChanging)     return;

Если сущность не привязана к datacontext, то нам ничего не надо делать. Также данная проверка избавляет нас от возможности наличия циклической ссылки, вызывающей какие-либо проблемы с переполнением стека. Если сущность привязана к datacontext, то тогда обработчики для событий PropertyChanging и PropertyChanged будут удалены.

PropertyChanging = null;
PropertyChanged = null;

Теперь, когда обработчики событий были удалены, изменения сущностей не будут более отслеживаться. Тем не менее, мы еще не закончили с отделением сущности Product. Нам необходимо отделить все дочерние сущности, списки и свойства, которые загружаются с задержкой. Этим мы и займемся.

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

Поскольку мы реализуем некоторые основные методы для получения возможности повторного использования, где понадобится использовать метод Detach() из каждой сущности, необходим метод abstract detach в классе LinqEntityBase. Мы уже видели метод Detach() для сущности Product в Petshop. Каждая из других сущностей в Petshop.dbml должна обладать detach конкретно для данной сущности. Итак, Product теперь наследует LinqEntityBase.

partial class Product : LinqEntityBase

Теперь давайте рассмотрим, что нам необходимо сделать для того, чтобы отделить дочернюю сущность. Мы добавим следующий метод, который отделяет конкретно дочернюю сущность от нашего класса LinqEntityBase.

protected static System.Data.Linq.EntityRef<TEntity> Detach<TEntity>
    (System.Data.Linq.EntityRef<TEntity> entity)
        where TEntity : LinqEntityBase
{
    if (!entity.HasLoadedOrAssignedValue || entity.Entity == null)
        return new System.Data.Linq.EntityRef<TEntity>();
        entity.Entity.Detach();
    return new System.Data.Linq.EntityRef<TEntity>(entity.Entity);
}

Нам сначала необходимо определить, была ли загружена сущность. Задача заключается в том, что необходимо постараться не задействовать загрузку сущностей. Метод HasLoadedOrAssignedValue говорит нам о том, была ли  загружена сущность, и мы можем избежать любую задержанную загрузку сущностей. Как только мы поймем, что сущность была загружена, сущность будет отделена и возвращена в качестве целевого объекта нового экземпляра EntityRef. Если сущность не была загружена, то свойство устанавливается в новый пустой экземпляр EntityRef.

entity.Entity.Detach();

Данная строка вызывает реализацию Detach , которая является специфической для сущности Category в данном случае. Опять-таки, каждое свойство требует наличия своего собственного метода Detach , специфического для данной сущности . Мы реализовали метод Detach() для каждой сущности аналогично процессу, который мы использовали для сущности Product.

 partial class Category : LinqEntityBase
{
    public override void Detach()
    {
        if (null == PropertyChanging)
            return;
               
        PropertyChanging = null;
        PropertyChanged = null;
        this._Products = Detach(this._Products, attach_Products, detach_Products);
    }
}

Все дочерние списки для данной сущности должны быть также отделены. Свойство ItemList в сущности Product является EntitySet. Каждый список ItemList в EntitySet должен быть отделен и следующий метод как раз выполняет это.

 protected static System.Data.Linq.EntitySet<TEntity> Detach<TEntity>
    (System.Data.Linq.EntitySet<TEntity> set, Action<TEntity> onAdd,
    Action<TEntity> onRemove)
        where TEntity : LinqEntityBase
{
    if (set == null || !set.HasLoadedOrAssignedValues)
        return new System.Data.Linq.EntitySet<TEntity>(onAdd, onRemove);
            
    // копирование списка и отделение всех сущностей
    var list = set.ToList();
    list.ForEach(t => t.Detach());
        
    var newSet = new System.Data.Linq.EntitySet<TEntity>(onAdd, onRemove);
    newSet.Assign(list);
    return newSet;
}

Как мы уже упоминали ранее, HasLoadedOrAssignedValue используется для определения того, загружен ли список, предостерегает от поздних загрузок списка. Каждый элемент в списке ItemList должен быть отделен и скопирован в новый EntitySet , который не привязан к datacontext.

Наконец, любые свойства, загружаемые с задержкой (delay loaded) также должны быть отделены. Обновив DBML, мы сконфигурировали свойство Descn сущности Product таким образом, чтобы оно загружалось с задержкой (delay loaded).

 

Любые загружаемые с задержкой свойства, которые также связаны с datacontext , которые должны быть отделены и используют третий и последний метод Detach, необходимы нам в базовом классе.

protected static System.Data.Linq.Link<T> Detach<T>(System.Data.Linq.Link<T> value)
{
    if (!value.HasLoadedOrAssignedValue)
        return default(System.Data.Linq.Link<T>);
           
    return new System.Data.Linq.Link<T>(value.Value);
}

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

Теперь, когда мы добавили необходимые базовые методы для отделения дочерних сущностей, наборы дочерних сущностей и свойств с поздней загрузкой, мы можем завершить метод Product Detach путем добавления вызова Detach для свойств Category, ItemList и Desc. Далее следует полноценный метод Product Detach():

partial class Product : LinqEntityBase
{
    public override void Detach()
    {
        if (null == PropertyChanging)
            return;
           
        PropertyChanging = null;
        PropertyChanged = null;
           
        this._Category = Detach(this._Category);
        this._Items = Detach(this._Items, attach_Items, detach_Items);
        this._Descn = Detach(this._Descn);
    }
}


Использование функции Detach

Одним из способов применения отделения и соединения сущностей Linq to SQL является использование модели хранилища. Мы установили простой OrderRepository для получения и сохранения заказов (Orders).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace Detach.Repositories
{
    public class OrderRepository
    {
        public static Order Get(int orderId)
        {
            Order order = null;
            using (var context = new PetshopDataContext())
            {
                order = context.Orders.FirstOrDefault(o => o.OrderId == orderId);
                order.Detach();
            }
            return order;
        }
        
        public static Order Save(Order order)
        {
            using (var context = new PetshopDataContext())
            {
                if (order.OrderId > 0)
                    context.Orders.Attach(order, true);
                else
                    context.Orders.InsertOnSubmit(order);
                   
                context.SubmitChanges();
                order.Detach();
            }
            return order;
        }
    }
}

Как вы могли уже заметить, каждый из данных методов имеет свой контекст данных (datacontext). Нет необходимости в передаче одного datacontext по всем в качестве параметра либо содержать его в переменной модульного уровня. Сущности полностью отделены, что означает полную свободу использования их где вам захочется и вам не придется беспокоиться об datacontext. Возможность отделения позволяет создать модель репозитория при помощи LINQ to SQL. Указанный далее код взаимодействует с хранилищем и не заботится о datacontext или об поддержании соединения с базой данных.

Order order = new Order();
order.BillAddr1 = "0001 Cemetery Lane";
order.BillCity = "Westfield";
order.BillState = "NJ";
order.BillZip = "07090";
order.BillCountry = "US";
order.Courier = "DHL";
order.OrderDate = System.DateTime.Now;
order.TotalPrice = 0;
order.AuthorizationNumber = 1;
order.BillToFirstName = "Gomez";
order.BillToLastName = "Adams";
order.Locale = "blah";
order.ShipToFirstName = "Gomez";
order.ShipToLastName = "Adams";
order.ShipAddr1 = "0001 Cemetery Lane";
order.ShipCity = "Westfield";
order.ShipState = "NJ";
order.ShipZip = "07090";
order.ShipCountry = "NJ";
order.UserId = "gadams";
    
order = OrderRepository.Save(order);
    
order.UserId = "gadams2";
order = OrderRepository.Save(order);
    
order = OrderRepository.Get(2);
order.TotalPrice = 150;
order = OrderRepository.Save(order);

Вывод

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

PLINQO генерирует методы отделения для каждой сущности, которые похожи на тот, который только что создали для сущности Product. PLINQO находит все дочерние сущности, списки и свойства, которые также должны быть отделены и обеспечивает выполнение соответствующих отделяющих методов для этих сущностей тогда, когда отделяется сущность от datacontext. Это означает, что вы не должны волноваться о том, что необходимо для отделения сущностей. PLINQO уже позаботился об этом.

Итак, как же отделить сущности используя PLINQO? Вызовите метод Detach и все!

Eric J. Smith, Shannon Davidson

Загрузить исходный код - 220.6 KB