Хранение данных приложений
14.3. Создание связей данных в LINQ
Чтобы добавить в программу возможность управления заказами товаров, нужно создать другие таблицы и связать их по ключевым полям. Класс для таблицы Products создается аналогично классу для таблицы Customers.
Класс для таблицы Orders будет иметь более сложную структуру, поскольку эта таблица содержит ссылки на записи двух других классов. В C# можно создать такое описание класса:
public class Order { public DateTime OrderDate; public int Quantity; public Customer OrderCustomer; public Product OrderProduct; }
Свойства OrderCustomer и OrderProduct связывают заказ с клиентом и заказанным им товаром. Соответствующие им поля таблицы базы данных являются внешними ключами.
Однако, в нашей базе данных отсутствуют ссылки как таковые. Вместо явных ссылок каждая запись таблицы Orders содержит значения идентификаторов клиента и товара, по которым можно установить соответствие заказа с клиентом и товаром. При этом, для таблиц базы данных можно задать отношения, которые связывают соответствующие записи двух таблиц, и в программе нужна возможность использовать эти отношения.
Ассоциации LINQ
В LINQ отношения между двумя таблицами называют ассоциацией. Она задается в классах базы данных с помощью класса EntityRef.
Связывание заказа с клиентом. В нашей программе в классе Order должна быть ссылка на класс Customer. Она создается в виде отношения между двумя таблицами. EntityRef — это специальный класс LINQ, который можно использовать для связи двух таблиц.
[Table] public class Order : INotifyPropertyChanged, INotifyPropertyChanging { ... private EntityRef<Customer> orderCustomer; [Association(IsForeignKey = true, Storage = "orderCustomer")] public Customer OrderCustomer { get { return orderCustomer.Entity; } set { NotifyPropertyChanging("OrderCustomer"); orderCustomer.Entity = value; NotifyPropertyChanged("OrderCustomer"); } } }
Класс EntityRef является своего рода ссылкой на таблицу. Теперь можно написать такой код:
Customer newCustomer = new Customer(); Order newOrder = new Order(); newOrder.OrderCustomer = newCustomer;
Этот код осуществляет связь заказа с клиентом, и запись в базе данных для этого заказа будет содержать идентификатор указанного клиента. Эту технику можно использовать каждый раз, когда необходимо создать отношение между записью одной таблицы и записью другой.
В атрибуте [Association] указывается, что ассоциация будет являться внешним ключом. Параметр Storage сообщает LINQ, какое закрытое свойство базы данных будет хранить информацию о свойстве.
Связывание клиента со всеми его заказами. Теперь программа может для заданного заказа получить информацию о клиенте, который создал этот заказ, поскольку запись в таблице Order содержит поле CustomerID, которое является идентификатором клиента. Однако, была бы очень полезна возможность найти для заданного клиента все его заказы.
В базе данных таблицы Customer и Order находятся в отношении "один ко многим". В классе C# Customer для хранения заказов клиента можно использовать одну из коллекций, например, массив или список. Однако, в LINQ для представления таких отношений нужно использовать класс EntitySet. Этот класс похож на EntityRef, но он позволяет управлять целым набором элементов. Можно связать клиента с его заказами, добавив в класс Customer свойство типа EntitySet:
[Table] public class Customer : INotifyPropertyChanged, INotifyPropertyChanging { [Column(IsPrimaryKey = true, IsDbGenerated = true, AutoSync = AutoSync.OnInsert)] public int CustomerID { get; set; } private EntitySet<Order> orders = new EntitySet<Order>(); [Association(Storage = "orders", ThisKey = "CustomerID", OtherKey = "OrderCustomerID")] public EntitySet<Order> Orders { get { return orders; } set { orders = value; } } }
Это свойство похоже на описанное ранее свойство класса EntityRef, за исключением того, что в его атрибуте Association указана дополнительная информация для описания отношения.
Параметр ThisKey задает имя свойства, которое будет использовать класс Order для поиска клиента, с которым связана ассоциация. Можно использовать свойство CustomerID, которое является первичным ключом для класса Customer.
Параметр OtherKey задает имя свойства класса Order, которое связано ассоциацией с другой стороны, то есть для заказа можно будет найти клиента, с которым связан этот заказ.
При изменении заказа не требуется генерировать события NotifyPropertyChanged, поскольку тип EntitySet является контейнером для заказов, а не непосредственно заказом. При добавлении заказов происходит добавление элементов в EntitySet, а не замена контейнера.
Чтобы ассоциацию работала должным образом, необходимо сделать некоторые изменения в ссылке в классе Order:
[Table] public class Order : INotifyPropertyChanged, INotifyPropertyChanging { ... [Column] public int OrderCustomerID; private EntityRef<Customer> orderCustomer = new EntityRef<Customer>(); [Association(IsForeignKey = true, Storage = "orderCustomer", ThisKey = "OrderCustomerID")] public Customer OrderCustomer { get { return orderCustomer.Entity; } set { NotifyPropertyChanging("OrderCustomer"); orderCustomer.Entity = value; NotifyPropertyChanged("OrderCustomer"); if (value != null) OrderCustomerID = value.CustomerID; } } }
Ассоциация использует внешний ключ (первичный ключ базы данных клиентов) и значение OrderCustomerID для хранения его значения.
В этой версии кода значение CustomerID копируется в столбец OrderCustomerID таблицы, когда клиенту назначается заказ. Теперь можно написать такой код:
Customer c = new Customer(); Order o = new Order(); o.OrderCustomer = c;
Код секции set свойства OrderCustomer копирует значение CustomerID в свойство OrderCustomerID для записи клиента текущего заказа.
Таким образом, можно легко добавить заказы клиента:
Customer c = new Customer(); Order o = new Order(); o.OrderCustomer = c; c.Orders.Add(o);
Класс EntitySet содержит метод Add, который позволяет добавлять заказы в базу данных. Для работы с содержимым набора объектов можно использовать одну из конструкций C# для перебора элементов коллекции.
Проектирование структуры баз данных для возможности заказа нескольких товаров. Теперь в структуре базы данных есть связь между клиентами и заказами. У клиента может быть несколько заказов, и каждый заказ связан с одним клиентом, который его создал.
Теперь необходимо связать с заказом товары, которые нужны клиенту. Однако, в нашей структуре базы данных каждый заказ может содержать только один тип товара, что является весьма существенным ограничением и недостатком.
Чтобы решить проблему, необходимо добавить новую таблицу OrderItem, в которой будет храниться информация обо всех товарах каждого заказа. Каждый элемент OrderItem будет связан с определенным заказом. Соответственно, каждый заказ будет содержать набор этих элементов. Таблица будет иметь структуру, представленную в таблице 5.4.
OrderItemID | OrderID | ProductID | Quantity |
---|---|---|---|
1 | 56 | 1001 | 1 |
2 | 56 | 1002 | 5 |
3 | 12343 | 1003 | 4 |
Соответственно, обновленную структуру базы данных можно представить в виде диаграммы классов, представленной на рис. 14.2. Каждый класс отображается на соответствующую таблицу в базе данных.
Таблицы связаны отношениями "один ко многим" и используют первичные ключи. В классах программы это отношения реализуются в виде ассоциаций по аналогии с рассмотренным в текущем разделе примером.
Выполнение запросов LINQ
Для получения необходимой информации из базы данных можно использовать мощные механизмы запросов LINQ. Например, можно найти все заказы, которые размещенные в указанную дату:
DateTime searchDate = new DateTime(2011, 8, 19); var orders = from Order order in activeDB.OrderTable where order.OrderDate == searchDate select order;
Этот запрос возвращает из таблицы заказов активной базы данных все заказы, созданные 19 августа 2011 г., и заносит их в переменную orders, тип которой трактуется как "запрос LINQ". При этом, эта строка программы только создает запрос, но еще не выполняет никаких операций для получения данных. Информация будет считана из базы данных при попытке обращения к результатам этого запроса:
int totalSales = 0; foreach (Order order in orders) { foreach (OrderItem item in order.OrderItems) { totalSales += item.OrderItemProduct.Price * item.Quantity; } }
Этот код получает информацию о заказах, которые будут получены запросом orders, и вычисляет полную стоимость всех заказанных товаров. Первый цикл foreach перебирает по одному заказу за каждый проход цикла, и в этот момент LINQ начинает получать информацию из базы данных.
Если выполнить в программе весь этот код еще раз, то LINQ снова начнет считывать информацию из базы данных, что может замедлить работу программы. Если необходимо обработать одни и те же данные несколько раз, лучше сохранить результат запроса в списке:
List<Order> DateOrders = orders.ToList<Order>();
Этот код создает в переменной DateOrders список, который содержит результат запроса. Этот список можно использовать в программе без повторного считывания одних и тех же записей из базы данных. При вызове метода ToList выполняется запрос LINQ и возвращается список результатов.
Получение данных из нескольких таблиц в одном запросе LINQ
LINQ позволяет создавать запросы для получения данных из нескольких таблиц с использованием операции соединения таблиц. Например, можно создать полный список всех заказов, сделанных всеми клиентами. Для этого нужно сопоставить записи о заказах с соответствующими записями о клиентах и сформировать список. Используя ключевое слово join, это можно сделать в одном запросе:
var allOrders = from Customer c in newDB.CustomerTable join Order o in newDB.OrderTable on c.CustomerID equals o.OrderCustomerID select new { c.Name, o.OrderDescription };
Этот запрос LINQ содержит ключевые слова from, join и select, которые встроены в язык C#. Первая часть запроса находит всех клиентов в таблице Customer. Затем к результату запроса присоединяются заказы из таблицы Order в соответствии с условием равенства значений идентификаторов клиента в этих таблицах. После этого создается новый тип данных, который содержит результаты, указанные в секции select. В нашем случае будет создан список объектов, содержащих информацию об имени клиента и описании заказа.
На основе результатов этого запроса можно сформировать список заказов клиентов:
List<String> OrderDescriptions = new List<String>(); foreach (var orderDesc in allOrders) { OrderDescriptions.Add(orderDesc.Name + " заказал: " + orderDesc.OrderDescription); }
Как и в предыдущем примере, результаты считываются из базы данных непосредственно перед их использованием в цикле. Ключевое слово var указывает на то, что тип переменной orderDesc будет определен при присвоении ей значения. Для этой переменной нельзя указать конкретный тип, поскольку он создается LINQ при выполнении запроса и будет известен только во время выполнения этого запроса.
Удаление элементов из базы данных
При необходимости можно удалить ненужные объекты из базы данных. Например, можно удалить ненужный заказ, используя метод DeleteOnSubmit:
ActiveDB.OrderItemTable.DeleteOnSubmit(item);
Также есть метод DeleteAllOnSubmit, который может удалить заданную коллекцию объектов.
Следует обратить внимание на удаление объектов, которые связаны отношениями с другими объектами. Если попытаться удалить объект, который содержит другие объекты, то база данных сгенерирует исключение. Сначала нужно удалить все дочерние объекты, и только после этого можно удалить родительский.
Краткие итоги
- Windows Phone предоставляет систему изолированного хранилища, в котором каждое приложение в телефоне может хранить свои данные.
- Программы могут создавать папки и файлы в своей изолированной области хранилища и использовать для взаимодействия с ними стандартные конструкции ввода/вывода, основанные на потоках.
- Альтернативным метод хранения данных является использование специализированного словаря для хранения в изолированном хранилище пар "имя—значение".
- Инструмент Isolated Storage Explorer позволяет программистам просматривать содержимое изолированного хранилища разрабатываемого приложения.
- Приложения Windows Phone могут использовать интегрированный язык запросов (LINQ) для взаимодействия с базами данных, которые могут храниться в изолированном хранилище в виде файлов. Этими базами данных управляет сервер баз данных SQL, но нельзя использовать SQL-запросы для взаимодействия с базой данных — для этого используется LINQ.
- База данных обычно состоит из нескольких таблиц. Таблицы состоят из столбцов (различные элементы данных, такие как имя, адрес, банковские реквизиты) и строк (вся информация об одном объекте — имя, адрес, банковские реквизиты одного клиента).
- Один из столбцов таблицы должен быть первичным ключом, значения которого уникальны для каждой строки таблицы. Каждое из этих значения однозначно идентифицирует запись таблицы. Можно указать базе данных, что нужно автоматически создавать первичный ключ для таблицы.
- В приложениях Windows Phone можно создавать классы, которые использует LINQ для взаимодействия с таблицами базы данных. Для этого к описанию класса добавляются атрибуты [Table] и [Column]. Также могут использоваться дополнительные параметры для определения первичных ключей.
- Свойства классов, которыми управляет LINQ, могут содержать код для создания уведомлений, чтобы LINQ автоматически обновлял записи базы данных при изменении значений этих свойств.
- База данных может содержать отношения, в которых столбец в одной таблице содержит значения первичного ключа другой таблицы. Такие отношения являются внешними ключами.
- В LINQ отношения реализуются с помощью класса EntityRef, который содержит ссылку на внешний ключ в другой базе данных. Класс EntitySet используется для того чтобы свойство одного объекта, связанного с таблицей базы данных, могло ссылаться на несколько записей другой таблицы. К этим элементам добавляется атрибут [Association], который описывает отношения между таблицами.
- Запрос LINQ можно использовать для получения структурированных данных из базы данных. Запрос считывает записи из базы данных непосредственно перед их использованием в программе. Запросы LINQ могут комбинировать результаты нескольких запросов.
- При удалении объектов из базы данных все дочерние объекты должны быть удалены перед удалением родительского объекта — иначе в таблицах базе данных могли бы существовать записи, содержащей значения первичного ключа для несуществующих записей из другой таблицы.
Вопросы
- Где приложение может сохранять данные?
- Каким образом программа может сохранять файлы?
- Как программа может сохранить настройки, которые можно представить в виде пар "имя—значение"?
- Для чего предназначена программа Isolated Storage Explorer?
- Как можно создать базу данных в приложении для Windows Phone?
- Как связать результаты запроса LINQ с визуальными элементами Silverlight?
- Как с помощью LINQ задать связи между таблицами?
- Как выполняются запросы LINQ?
- Как выполняется добавление и удаление записей таблиц в LINQ?
Упражнения
Упражнение 1. Использование изолированного хранилища
В предыдущем упражнении мы создавали простое приложение TimeTracker, которое сохраняло информацию о времени встреч с клиентами. Предыдущая версия программы при запуске генерировала набор тестовых данных. Необходимо использовать хранилище файлов, чтобы приложение сохраняло информацию о встречах.
Программист внес в программу изменения, однако, тестировщик обнаружил, что программа не сохраняет изменения данных. Необходимо выявить причину этой проблемы.
- Откройте в Visual Studio проект CustomerManager в папке Lab5 Customer Time Logger Storage. Этот проект содержит версию программы, в которой обнаружена ошибка.
- Убедитесь в том, что эмулятор Windows Phone не запущен. Укажите эмулятор Windows Phone в качестве целевой платформы.
- Запустите программу. При первом запуске программа создаст новый набор тестовых данных.
- Выберите клиента и измените информацию о нем.
- Нажмите кнопку сохранить. Обратите внимание на то, что содержимое экрана изменилось, поскольку в программе используется привязка данных для отображения обновленных значений.
- Остановите программу, нажав в эмуляторе Windows Phone кнопку Назад. Не останавливайте выполнение программы в Visual Studio.
- Запустите программу еще раз. При повторном запуске программа должна загрузить список клиентов из изолированного хранилища.
- Найдите клиента, информацию о котором вы изменили. Обратите внимание, что сделанные изменения не сохранились.
- Список клиентов загружается в файле App.xaml.cs. Откройте этот файл в обозревателе решений Visual Studio и найдите конструктор App. В конце этого метода находится код, который должен загружать данные из изолированного хранилища. Попробуйте определить причину возникающей проблемы.
- Конструктор содержит только код, который создает тестовый список при каждом запуске программы:
ActiveCustomerList = Customers.MakeTestCustomers();
- Необходимо изменить этот код, чтобы информация о клиентах загружалась из файла. Откройте в файле App.xaml.cs область Customer Manager Values и найдите методы LoadCustomers и SaveCustomers.
- Метод LoadCustomers возвращает список клиентов или значение null, если список не может быть считан. Если метод возвращает null, программа должна создать набор тестовых данных. Замените указанный в шаге 10 код на следующий:
// загрузить список клиентов из файла ActiveCustomerList = LoadCustomers(ListFilename); // если загрузить список не удалось, создать тестовые данные if (ActiveCustomerList == null) ActiveCustomerList = Customers.MakeTestCustomers();
- Запустите программу. Измените информацию о любом клиенте.
- Остановите программу, нажав в эмуляторе Windows Phone кнопку Назад. Не останавливайте выполнение программы в Visual Studio.
- Запустите программу еще раз. Убедитесь в том, что теперь сделанные изменения были сохранены.
Пропадание информации о встрече
Тестировщик нашел в программе еще одну проблему: программа некорректно сохраняет информацию о встрече. Необходимо исследовать эту проблему.
Программа создает объекты для работы с потоками StreamReader и StreamWriter для загрузки и сохранения элементов в изолированном хранилище. Каждый объект может сохранить свое состояние в поток и загрузить информацию из потока. Ниже приведен код методов для сохранения информации в поток и для загрузки информации из потока:
public void SaveToStream(StreamWriter output) { output.WriteLine(CustomerList.Count); foreach (Customer c in CustomerList) { c.SaveToStream(output); } } public Customers(StreamReader input) { CustomerList = new List<Customer>(); int noOfCustomers = int.Parse(input.ReadLine()); for (int i = 0; i < noOfCustomers; i++) { CustomerList.Add(new Customer(input)); } }
Этот код выглядит правильно. Необходимо проверить класс Customer, правильно ли он использует эти методы.
- Вернитесь в программу и откройте файл Customers.cs.
- Найдите класс Customers и просмотрите метод SaveToStream:
public void SaveToStream(StreamWriter output) { output.WriteLine(Name); output.WriteLine(Address); output.WriteLine(ID); }
- Этот метод не содержит код для сохранения информации о встрече. Аналогичная проблема возникает и в методе Customer:
public Customer(StreamReader input) { Name = input.ReadLine(); Address = input.ReadLine(); ID = int.Parse(input.ReadLine()); int noOfSessions = int.Parse(input.ReadLine()); }
- Необходимо использовать тот же шаблон для загрузки и сохранения списка встреч, который использует объект CustomerList для хранения информации о клиентах. Измените методы для сохранения и загрузки, чтобы можно было сохранять информацию о нескольких встречах:
public void SaveToStream(StreamWriter output) { output.WriteLine(Name); output.WriteLine(Address); output.WriteLine(ID); output.WriteLine(Sessions.Count); foreach (Session session in Sessions) { session.SaveToStream(output); } } public Customer(StreamReader input) { Name = input.ReadLine(); Address = input.ReadLine(); ID = int.Parse(input.ReadLine()); int noOfSessions = int.Parse(input.ReadLine()); for (int i = 0; i < noOfSessions; i++) { Sessions.Add(new Session(input)); } }
- Остановите эмулятор Windows Phone, чтобы при следующем запуске программы она создала новый набор тестовых данных. Запустите программу еще раз.
- Остановите программу, нажав в эмуляторе Windows Phone кнопку Назад. Не останавливайте выполнение программы в Visual Studio.
- Запустите программу еще раз.
- Выберите любого клиента и просмотрите информацию о встречах, которая должна была быть сохранена.
Упражнение 2. Использование базы данных
В программу, созданную в предыдущем упражнении, внесли изменения, чтобы она могла работать с базой данных. Однако, программа компилируется, но не запускается. Необходимо исправить ошибку.
- Откройте в Visual Studio проект CustomerManager в папке Lab5 Customer Time Logger Database. Этот проект содержит версию программы, в которой обнаружена ошибка.
- Убедитесь в том, что эмулятор Windows Phone не запущен.
-
Запустите программу. Программа сгенерирует тестовые данные и попытается их использовать. После этого будет сгенерировано исключение, в котором база данных сообщает, что проблема связана с внешним ключом. Это касается связей между классами Session и Customer. Сообщение об ошибке информирует о том, что невозможно вставить внешний ключ, так как не существует соответствующий первичный ключ.
При создании связи встречи с клиентом запись в таблице встреч должна содержать значение первичного ключа, которое идентифицирует клиента. Однако, если этот ключ еще не был задан, база данных не может добавить запись. При создании связи между этими двумя таблицами необходимо:
- добавить встречу к списку встреч клиента;
- установить значение customerID для встречи, чтобы идентифицировать клиента для этой встречи.
Код, который выполняет эти действия, выглядит следующим образом:
int sessionLength = sessionRand.Next(5,120); string sessionDesc = "Встреча " + i.ToString(); Session newSession = new Session(); newSession.Description = sessionDesc; newSession.LengthInMins = sessionLength; newSession.SessionCustomer = c; newDB.SessionTable.InsertOnSubmit(newSession); c.Sessions.Add(newSession);
- Добавьте этот код в программу и запустите ее повторно. Теперь программа работает правильно и позволяет управлять данными.
Тестирование хранения данных
В настоящее время при каждом запуске программа создает новую базу данных. Измените программу так, чтобы она использовала существующую базу данных и создавала новую, если база данных не существует. Обратите внимание, что новая база данных создается при выполнении следующей строки кода в файле App.xaml.cs:
CustomerDB.MakeTestDB("Data Source=isostore:/Sample.sdf");