Основное назначение DataContext

91

»» В ДАННОЙ СТАТЬЕ ИСПОЛЬЗУЕТСЯ ИСХОДНЫЙ КОД ДЛЯ ПРИМЕРОВ

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

Отслеживание идентичности

Одной из проблем, для преодоления которых предназначен LINQ to SQL, является проблема объектно-реляционной потери соответствия (object-relational Impedance mismatch). Этот термин указывает на неизбежные трудности, вызванные тем фактом, что большинство наиболее распространенных баз данных являются реляционными, в то время как большинство современных языков программирования — объектно-ориентированными. Из-за этого различия и возникают проблемы.

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

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

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

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

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

Несоответствие кэша результирующего набора

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

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

Возможно, станет понятнее, если рассмотреть конкретный пример.

Сначала запросим определенного заказчика, который, как известно, не соответствует критерию поиска, указанному в последующем запросе. Будем использовать заказчика LONEP.

Регионом этого заказчика является OR, поэтому производится поиск заказчиков из региона WA. Затем заказчики из региона WA выводятся на консоль. После этого регион заказчика LONEP заменяется значением WA, используя для этого ADO.NET — как если бы это сделал некоторый другой контекст, внешний по отношению к текущему процессу.

В этой точке LONEP будет иметь регион OR в сущностном объекте, но WA — в базе данных. Далее тот же самый запрос выполняется снова, чтобы извлечь всех заказчиков из региона WA. Взглянув на код, вы не увидите там нового определения запроса. Вы просто увидите, что производится перечисление на возвращенной последовательности с custs.

Помните, что из-за отложенного выполнения запросов нужно лишь перечислить результаты, чтобы снова запустить запрос. Поскольку в базе данных регион заказчика LONEP теперь WA, эта запись будет включена в результирующий набор. Но поскольку сущностный объект этой записи уже находится в кэше, будет возвращен именно этот кэшированный сущностный объект, а у этого объекта значение региона — по-прежнему OR. Затем регионы каждого возвращенного сущностного объекта выводятся на консоль. Когда очередь дойдет до заказчика LONEP, его регионом окажется OR, несмотря на тот факт, что в запросе указана необходимость в заказчиках из региона WA.

Ниже представлен код, демонстрирующий это несоответствие:

// Используйте свое подключение
            Northwind db = new Northwind(
                     @"Data Source=MICROSOF-1EA29E\SQLEXPRESS;Initial Catalog=C:\NORTHWIND.MDF;Integrated Security=True");

            // Получить заказчика для модификации, который находится 
            // вне запрашиваемого региона == 'WA'
            Customer cust = (from c in db.Customers
                             where c.CustomerID == "LONEP"
                             select c).Single<Customer>();

            Console.WriteLine("Заказчик {0} имеет регион = {1}.\n",
              cust.CustomerID, cust.Region);

            // Регионом LONEP является OR.

            // Теперь получить последовательность заказчиков из 'WA', 
            // которая не включает LONEP, поскольку его регион — OR. 
            IEnumerable<Customer> custs = (from c in db.Customers
                                           where c.Region == "WA"
                                           select c);

            Console.WriteLine("\nЗаказчики из WA перед изменением посредством AD0.NET - начало...");
            foreach (Customer c in custs)
            {
                // Отобразить Region каждого сущностного объекта.
                Console.WriteLine("Заказчик {0} имеет регион {1}.", c.CustomerID, c.Region);
            }
            Console.WriteLine("Заказчики из WA перед изменением посредством AD0.NET - конец\n");

            // Теперь изменить регион заказчика LONEP на WA, что приведет
            // к включению его в результаты предыдущего запроса.

            // Изменить регион заказчика через ADO.NET.
            Console.WriteLine("Обновления региона для LONEP на WA в ADO.NET...");
            ExecuteStatementInDb(
              "update Customers set Region = 'WA' where CustomerID = 'LONEP'");
            Console.WriteLine("Регион для LONEP обновлен.\n");

            Console.WriteLine("Таким образом регионом для LONEP является WA в базе данных, но ...");
            Console.WriteLine("Заказчик {0} имеет регион = {1} в сущностном объекте.\n",
              cust.CustomerID, cust.Region);

            // Теперь в базе данных регионом для LONEP является WA,
            // но в сущностном объекте - по-прежнему OR

            // Снова выполняем запрос
            Console.WriteLine("Запрос сущностных объекто после изменения посредством ADO.NET - начало ...");
            foreach (Customer c in custs)
            {
                // Отобразим регион для каждого сущностного объекта
                Console.WriteLine("Заказчик {0} имеет регион {1}.", c.CustomerID, c.Region);
            }
            Console.WriteLine("Запрос сущностных объекто после изменения посредством ADO.NET - конец\n");

            // Вернуть измененное значение в исходное состояние
            ExecuteStatementInDb(
              "update Customers set Region = 'OR' where CustomerID = 'LONEP'");
 Пример, демонстрирующий несоответствие кэша результирующего набора

Как видите, даже несмотря на то, что запрашивались заказчики из региона WA, заказчик LONEP был включен в результат, при том, что его регионом является OR. Конечно, в базе данных у LONEP в качестве региона указано WA, но у объекта, на который имеется ссылка в коде, это не так.

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

// Используйте свое подключение
            Northwind db = new Northwind(
                     @"Data Source=MICROSOF-1EA29E\SQLEXPRESS;Initial Catalog=C:\NORTHWIND.MDF;Integrated Security=True");

            Console.WriteLine("Сначала добавляется заказчик LAWN...");
            db.Customers.InsertOnSubmit(
              new Customer
              {
                  CustomerID = "LAWN",
                  CompanyName = "Lawn Wranglers",
                  ContactName = "Mr. Abe Henry",
                  ContactTitle = "Owner",
                  Address = "1017 Maple Leaf Way",
                  City = "Ft. Worth",
                  Region = "TX",
                  PostalCode = "76104",
                  Country = "USA",
                  Phone = "(800) MOW-LAWN",
                  Fax = "(800) MOW-LAWO"
              });

            Console.WriteLine("Далее заказчик LAWN запрашивается...");
            Customer cust = (from c in db.Customers
                             where c.CustomerID == "LAWN"
                             select c).SingleOrDefault<Customer>();
            Console.WriteLine("\nЗаказчик LAWN {0}.\n",
              cust == null ? "не существует" : "существует");

            Console.WriteLine("Теперь удаляется заказчик LONEP");
            cust = (from c in db.Customers
                    where c.CustomerID == "LONEP"
                    select c).SingleOrDefault<Customer>();
            db.Customers.DeleteOnSubmit(cust);

            Console.WriteLine("Далее заказчик LONEP запрашивается.");
            cust = (from c in db.Customers
                    where c.CustomerID == "LONEP"
                    select c).SingleOrDefault<Customer>();
            Console.WriteLine("\nЗаказчик LONEP {0}.\n",
              cust == null ? "не существует" : "существует");

В предыдущем коде сначала вставляется заказчик LAWN, который затем запрашивается, чтобы проверить его существование. После этого удаляется другой заказчик — LONEP — и выполняется запрос, чтобы проверить его наличие. Все это делается без вызова метода SubmitChanges, так что кэшированные сущностные объекты не сохраняются в базе данных. Вот результат выполнения этого кода:

Другой пример, демонстрирующий несоответствие кэша результирующего набора

Разработчики из Microsoft утверждают, что задуманное поведение состоит в том, что данные, извлеченные запросом, считаются устаревшими на момент извлечения, а данные, кэшированные в DataContext, не предназначены для долговременного хранения в кэше. Если нужна лучшая изоляция и согласованность, они рекомендуют помещать все в транзакцию.

Отслеживание изменений

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

Все это работает хорошо до тех пор, пока сущностные объекты извлекаются из базы данных. Однако простое создание нового экземпляра сущностного объекта не вызывает никакого отслеживания идентичности или изменений до тех пор, пока DataContext не узнает об его существовании. Чтобы сообщить объекту DataContext о существовании сущностного объекта, просто вставьте сущностный объект в одно из свойств Table<Т>.

Например, в классе Northwind есть свойство типа Table<Customer> по имени Customers. На этом свойстве можно вызвать метод InsertOnSubmit и вставить сущностный объект Customer в Table<Customer>. Когда это сделано, DataContext начнет отслеживать идентичность и изменения этого сущностного объекта. Ниже приведен пример кода, вставляющего нового заказчика:

db.Customers.InsertOnSubmit(
              new Customer
              {
                  CustomerID = "LAWN",
                  CompanyName = "Lawn Wranglers",
                  ContactName = "Mr. Abe Henry",
                  ContactTitle = "Owner",
                  Address = "1017 Maple Leaf Way",
                  City = "Ft. Worth",
                  Region = "TX",
                  PostalCode = "76104",
                  Country = "USA",
                  Phone = "(800) MOW-LAWN",
                  Fax = "(800) MOW-LAWO"
              });

Как только вызывается метод InsertOnSubmit, начинается отслеживание идентичности и изменений заказчика LAWN.

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

Обработка изменений

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

Когда вызывается метод SubmitChanges, процессор изменений объекта DataContext управляет обновлением базы данных. Сначала процессор изменений вставит все вновь вставленные сущностные объекты в свой список отслеживаемых объектов. Затем он упорядочит все измененные сущностные объекты на основе их зависимостей, обусловленных внешними ключами и ограничениями уникальности. Затем, если в области определения нет никаких транзакций, он создаст транзакцию, так что все команды SQL, выполняемые во время вызова метода SubmitChanges, будут обладать транзакционной целостностью.

Он использует уровень изоляции SQL Server по умолчанию — ReadCommited, который означает, что прочитанные данные не будут физически повреждены и только зафиксированные (commited) данные будут прочитаны, но поскольку блокировка является разделяемой (shared}, ничто не помешает данным измениться до окончания транзакции. И, наконец, он выполняет перечисление упорядоченного списка измененных сущностных объектов, создает необходимые операторы SQL и выполняет их.

Если в процессе перечисления измененных сущностных объектов происходят любые ошибки и если метод SubmitChanges использует для ConflictMode вариант FailOnFirstConflict, то процесс перечисления прерывается, транзакция откатывается, отменяя все изменения в базе данных, и генерируется исключение.

Если для ConflictMode указан вариант ContinueOnConflict, то все измененные сущностные объекты будут перечислены и обработаны, несмотря на любые возникающие конфликты, пока DataContext строит список конфликтов. Но, опять-таки, транзакция откатывается, отменяя все изменения в базе данных, и генерируется исключение. Однако пока данные не сохранены в базе данных, все изменения сущностных объектов присутствуют в этих объектах. Это дает возможность разработчику попробовать решить проблему и снова вызвать метод SubmitChanges.

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

Время жизни контекста данных

Одним из часто задаваемых вопросов является вопрос о том, насколько долго объект DataContext должен сохраняться актуальным и использоваться. Как упоминалось в разделе "Несоответствие кэша результирующего набора", данные, извлеченные и кэшированные объектом DataContext, считаются устаревшими на момент извлечения!

Это означает, что чем дольше сохраняется актуальным объект DataContext, тем сильнее устаревают находящиеся в памяти данные. И потому это означает не только дополнительные накладные расходы, но и повышает вероятность несоответствия данных кэша результирующего набора реальным данным. Следовательно, настоятельно рекомендуется делать объекты DataContext как можно более кратковременными.

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

Следует придерживаться эмпирического правила, что объект DataContext должен существовать несколько минут, а не часов.

У некоторых разработчиков возникает соблазн сохранять объект DataContext в течение более длительного времени, полагаясь на метод Refresh, чтобы предотвратить несоответствие кэша результирующего набора. Это нельзя считать удачным подходом, потому что при этом приходится принимать решение о частоте и времени вызова метода Refresh.

Будете ли вы вызывать его во время каждого использования объекта DataContext? Излишние вызовы метода Refresh приводят полному обновлению всего кэша из базы данных. Если DataContext существует достаточно долго, это может вызвать проблемы с производительностью. Слишком дорогая цена за то, чтобы избежать затрат на создание объекта DataContext.

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