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

97

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

Классы, которые отображаются на базу данных SQL Server с использованием LINQ to SQL, называются сущностными классами (entity classes). Созданный экземпляр сущностного класса — это сущность данного типа, и это будет называться сущностным объектом (entity object).

Сущностные классы — это обычные классы C# с дополнительно заданными атрибутами LINQ to SQL. Вместо добавления атрибутов сущностные классы могут быть созданы с применением XML-файла отображения при создании экземпляра объекта DataContext. Эти атрибуты или вхождения файла отображения диктуют то, как сущностные классы могут отображаться на базу данных SQL Server с использованием LINQ to SQL.

Применяя сущностные классы, можно опрашивать и обновлять базу данных с использованием LINQ to SQL.

Создание сущностных классов

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

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

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

Генерация сущностных классов

В исходных кодах к примерам LINQ to SQL было показано, как сгенерировать сущностные классы из базы данных Northwind, чтобы можно было запускать примеры LINQ to SQL, приведенные в статьях по этой теме. Также детально описывалось, как генерировать сущностные классы с использованием либо инструмента командной строки по имени SQLMetal, либо инструмента с графическим интерфейсом пользователя под названием Object Relational Designer.

SQLMetal очень прост в использовании, но не предусматривает никаких опций для управления именованием сгенерированных сущностных классов помимо производства промежуточного XML-файла, который можно редактировать. Затем SQLMetal генерирует сущностные классы для каждой таблицы в указанной базе данных и для каждого поля в каждой из таблиц.

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

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

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

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

Частичные классы — замечательное приобретение C#, которое, как никогда ранее, облегчает разделение функциональности по разным модулям. Таким образом, если вам по какой-то причине придется повторно сгенерировать сущностный класс, вы не потеряете добавленные члены или методы.

Написание сущностных классов вручную

Написание сущностных классов вручную — более трудный подход. Он требует глубокого понимания атрибутов LINQ to SQL и/или внешней схемы отображения. Однако в то же время написание сущностных классов вручную — замечательный способ действительно изучить LINQ to SQL.

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

Благодаря гибкости LINQ to SQL нет необходимости, чтобы имена классов соответствовали именам таблиц, в которых они сохраняются, а имена свойств — именам столбцов этих таблиц. Это значит, что ранее реализованные классы могут быть модифицированы для хранения в базе данных SQL Server.

Чтобы создать сущностные классы вручную с использованием атрибутов, понадобится добавить соответствующие атрибуты к классам — будь то существующие бизнес-классы либо новые классы, — которые должны стать сущностными.

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

Дополнительная ответственность сущностных классов

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

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

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

Уведомления об изменениях

В свою очередь, отслеживание изменений не может быть реализовано элегантно и эффективно без участия самих сущностных классов. Если сущностные классы сгенерированы SQLMetal или Object Relational Design, можно расслабиться, поскольку эти инструменты позаботятся обо всем, реализуя код, участвующий в уведомлениях об изменении, при генерации сущностных классов. Но при написании собственных сущностных классов следует понимать механизм уведомления об изменении и самостоятельно реализовывать код, участвующий в этом процессе.

Можно выбрать, должны ли сущностные классы участвовать в уведомлениях об изменении. Если они не участвуют, то DataContext обеспечивает это, сохраняя две копии каждого сущностного объекта; одну для исходных значений и другую — для текущих. Он создает копии при первоначальном извлечении сущности из базы данных в начале отслеживания изменений. Этот механизм можно сделать более эффективным, если сущностные классы будут реализовывать два интерфейса уведомления об изменении — System.ComponentModel.INotifyPropertyChanging и System.ComponentModel.INotifyPropertyChanged.

Далее, мы часто будем ссылаться на код, сгенерированный SQLMetal, чтобы продемонстрировать суть способа обработки уведомления об изменении. Чтобы реализовать интерфейсы INotifyPropertyChanging и INotifyPropertyChanged, потребуется сделать четыре вещи.

Во-первых, необходимо определить, что сущностный класс реализует интерфейсы INotifyPropertyChanging и INotifyPropertyChanged:

// Из сгенерированного сущностного класса Customer
                  
    [Table(Name="dbo.Customers")]
	public partial class Customer : INotifyPropertyChanging, INotifyPropertyChanged
	{ ... }

Поскольку сущностный класс реализует эти два интерфейса, DataContext будет известно, что нужно зарегистрировать два обработчика для двух событий, о которых рассказывается ниже.

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

Во-вторых, нужно объявить приватную статическую переменную типа PropertyChangingEventArgs и передать ее конструктору значение String.Empty:

// Из сгенерированного сущностного класса Customer
...
private static PropertyChangingEventArgs emptyChangingEventArgs = 
     new PropertyChangingEventArgs(String.Empty);

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

В-третьих, нужно добавить к сущностному классу два члена public event — один типа PropertyChangingEventHandler по имени PropertyChanging и еще один — типа PropertyChangedEventHandler по имени PropertyChanged:

// Из сгенерированного сущностного класса Customer
...

public event PropertyChangingEventHandler PropertyChanging;
public event PropertyChangedEventHandler PropertyChanged;

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

В-четвертых, каждый раз, когда свойство отображенного сущностного класса изменяется, необходимо сгенерировать событие PropertyChanging непосредственно перед изменением свойства и событие PropertyChanged — сразу после изменения свойства.

Хотя реализовывать генерацию событий подобным образом не обязательно, для согласованности SQLMetal генерирует методы SendPropertyChanging и SendPropertyChanged:

// Из сгенерированного сущностного класса Customer
...

protected virtual void SendPropertyChanging()
{
     if (this.PropertyChanging != null) {
         this.PropertyChanging(this, new PropertyChangedEventArgs); }
}

protected virtual void SendPropertyChanged(String propertyName) {
    if (this.PropertyChanged != null) {
         this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

Обратите внимание, что при возбуждении события PropertyChanged создается новый объект PropertyChangedEventArgs, получающий имя конкретного свойства, которое подвергается изменению. Это позволяет объекту DataContext знать точно, какое свойство изменилось. Поэтому когда метод SendPropertyChanging вызывается, он инициирует событие PropertyChanging, которое приводит к вызову зарегистрированного объектом DataContext обработчика. То же самое происходит с методом SendPropertyChanged и событием PropertyChanged.

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

Затем в методе set каждого свойства понадобится вызывать два метода SendPropertyChanging и SendPropertyChanged непосредственно перед изменением свойства и после него:

// Из сгенерированного сущностного класса Customer
...

[Column(Storage="_CustomerID", DbType="NChar(5) NOT NULL", CanBeNull=false, IsPrimaryKey=true)]
		public string CustomerID
		{
			get
			{
				return this._CustomerID;
			}
			set
			{
				if ((this._CustomerID != value))
				{
					this.OnCustomerIDChanging(value);
					this.SendPropertyChanging();
					this._CustomerID = value;
					this.SendPropertyChanged("CustomerID");
					this.OnCustomerIDChanged();
				}
			}
		}

Опять-таки, обратите внимание, что при вызове метода SendPropertyChanged передается имя свойства, в данном случае — CustomerID. Как только вызывается метод SendPropertyChanged, объект DataContext узнает, что свойство сущностного объекта CustomerID было изменено.

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

// Из сгенерированного сущностного класса Orders,
// поскольку Customer не имеет свойств EntityRef<T>
...

[Association(Name="FK_Orders_Customers", Storage="_Customer", ThisKey="CustomerID", IsForeignKey=true)]
		public Customer Customer
		{
			get
			{
				return this._Customer.Entity;
			}
			set
			{
				Customer previousValue = this._Customer.Entity;
				if (((previousValue != value) 
							|| (this._Customer.HasLoadedOrAssignedValue == false)))
				{
					this.SendPropertyChanging();
					if ((previousValue != null))
					{
						this._Customer.Entity = null;
						previousValue.Orders.Remove(this);
					}
					this._Customer.Entity = value;
					if ((value != null))
					{
						value.Orders.Add(this);
						this._CustomerID = value.CustomerID;
					}
					else
					{
						this._CustomerID = default(string);
					}
					this.SendPropertyChanged("Customer");
				}
			}
		}

А на стороне один ассоциации "один ко многим" понадобится следующий код, выделенный полужирным:

// Из сгенерированного сущностного класса Customer
...

public Customer()
{
			...
			this._Orders = new EntitySet<Order>(new Action<Order>(this.attach_Orders), 
               new Action<Order>(this.detach_Orders));
			...
}
...

private void attach_Orders(Order entity)
		{
			this.SendPropertyChanging();
			entity.Customer = this;
		}
		
private void detach_Orders(Order entity)
		{
			this.SendPropertyChanging();
			entity.Customer = null;
		}

Если вы не знакомы с обобщенным делегатом Action, использованным в приведенном коде, знайте, что он находится в пространстве имен System, и был добавлен в версии .NET Framework 2.0.

Предыдущий код создает экземпляр объекта Action для сущностного класса Order и передает ему делегат метода attachOrders. LINQ to SQL использует этот делегат позднее, чтобы присвоить объекту Order соответствующий объект Customer. Аналогично создается экземпляр другого объекта-делегата Action, которому передается делегат метода detachOrders. Этот делегат LINQ to SQL применяет позже для удаления присвоенного Customer из Order.

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

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

Конечно, если позволить SQLMetal или Object Relational Designer создавать сущностные классы, все эти сложности исчезают, потому что данные инструменты генерируют весь необходимый рутинный код. И только при написании сущностных классов вручную следует сосредоточиться на реализации уведомлений об изменении.

Согласованность графа

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

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

За обработку обновления другой стороны отношения отвечает сущностный класс. Если генерация сущностных классов поручается SQLMetal или Object Relational Designer, то ничего дополнительно делать не понадобится. Но в случае создания собственных сущностных классов придется реализовать весь необходимый код самостоятельно.

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

Взглянем на соответствующую реализацию, сгенерированную SQLMetal на базе данных Northwind:

// Из сгенерированного сущностного класса Customer
...

public Customer()
{
			...
			this._Orders = new EntitySet<Order>(new Action<Order>(this.attach_Orders), 
               new Action<Order>(this.detach_Orders));
			...
}
...

private void attach_Orders(Order entity)
		{
			this.SendPropertyChanging();
			entity.Customer = this;
		}
		
private void detach_Orders(Order entity)
		{
			this.SendPropertyChanging();
			entity.Customer = null;
		}

В данном примере класс Customer будет родительским, или стороной один в отношении "один ко многим". Класс Order будет дочерним, представляющим сторону многие того же отношения.

В приведенном коде можно увидеть, что при инициализации члена EntitySet<T> для коллекции дочерних объектов Orders конструктору передаются два объекта-делегата Action<T>.

Первый объект Action<T> получает делегат метода обратного вызова, который обработает присваивание текущего объекта Customer, на который ссылается ключевое слово this, как Customer объекта Order, переданный в метод обратного вызова. В приведенном выше коде таким методом обратного вызова является attach_Orders.

Второй параметр конструктора EntitySet<T> — это объект-делегат Action<T>, в котором передается делегат метода обратного вызова, обрабатывающий удаление присваивания Customer переданного объекта Order. В приведенном выше коде методом обратного вызова является detach_Orders.

Несмотря на то что приведенный выше код находится в родительском классе Customer, присваивание объекта дочернего класса Order на самом деле обрабатывается свойством Customer объекта Order.

Это можно видеть как в методе attach_Orders, так и в detach_Orders; все, что они в действительности делают — это изменяют свойство Customer объекта Order. Свойство entity.Customer устанавливается соответственно в this и null, чтобы присоединить текущий Customer и отсоединить подключенный в данный момент Customer. Вся работа по поддержке согласованности графа осуществляется в методах set и get дочернего класса Order. Таким образом, реальная работа делегируется дочернему классу. В родительском классе больше ничего делать не нужно.

Однако обратите внимание, что перед тем, как выполнить работу в методах attach__Orders и detach_Orders, вызовом методов SendPropertyChanging и SendPropertyChanged инициируются уведомления об изменении.

Теперь посмотрим, что нужно сделать в дочернем классе отношения "родительский-дочерний" для поддержки согласованности графа:

// Из сгенерированного сущностного класса Orders
...

[Association(Name="FK_Orders_Customers", Storage="_Customer", ThisKey="CustomerID", IsForeignKey=true)]
		public Customer Customer
		{
			get
			{
				return this._Customer.Entity;
			}
			set
			{
				Customer previousValue = this._Customer.Entity;
				if (((previousValue != value) 
							|| (this._Customer.HasLoadedOrAssignedValue == false)))
				{
					this.SendPropertyChanging();
					if ((previousValue != null))
					{
						this._Customer.Entity = null;
						previousValue.Orders.Remove(this);
					}
					this._Customer.Entity = value;
					if ((value != null))
					{
						value.Orders.Add(this);
						this._CustomerID = value.CustomerID;
					}
					else
					{
						this._CustomerID = default(string);
					}
					this.SendPropertyChanged("Customer");
				}
			}
		}

В приведенном коде рассматривается только метод set свойства Customer — именно потому, что родительская сторона отношения возлагает на него ответственность на поддержание согласованности графа. Поскольку этот метод довольно сложный, вместе с кодом предоставляется дополнительное описание.

В первой строке кода метода set производится сохранение исходного Customer, присвоенного Order, в переменную previousValue. Пусть вас не смущает тот факт, что код ссылается на this._Customer.Entity. Вспомните, что переменная-член _Customer — это на самом деле объект типа EntityRef<Customer>, а не Customer. Чтобы получить объект Customer, код должен обратиться к свойству Entity объекта EntityRef<T>. Поскольку здесь EntityRef<T> предназначен для хранения Customer, типом Entity будет Customer; никакого приведения не требуется.

if (((previousValue != value) 
							|| (this._Customer.HasLoadedOrAssignedValue == false)))
				{ ...

Далее приведенная строка кода проверяет, не присвоен ли уже этот Customer объекту Order, сравнивая переданный параметр value с имеющейся в Order ссылкой на Customer, поскольку в этом случае ничего делать не надо, если только этот Customer не был еще загружен или присвоен. Это не только логически оправдано, но, учитывая рекурсивную природу работы этого кода, эта строка становится чрезвычайно важной, поскольку позволяет остановить рекурсию.

this.SendPropertyChanging();

В этой строке кода вызывается метод SendPropertyChanging для генерации события уведомления об изменении.

Далее код определяет, не присвоен ли уже родительский объект Customer данному дочернему объекту Order, сравнивая previousValue с null. Вспомните, что в этой точке значение Customer объекта Order все еще совпадает со значением переменной previousValue.

Если Customer назначен Order, в том смысле, что переменная previousValue, представляющая присвоенный Customer, не равна null, то код должен установить свойство Entity объекта Customer EntityRef<T> в null в следующей строке:

this._Customer.Entity = null;

Свойство Entity устанавливается здесь в null, чтобы остановить рекурсию, которая запускается в следующей строке кода. Поскольку свойство Entity свойства Customer объекта Order теперь равно null и не ссылается на действительный объект Customer, в то время как свойство Orders объекта Customer все еще содержит этот Order в коллекции, в данный момент времени граф находится в несогласованном состоянии.

В следующей строке кода вызывается метод Remove на свойстве Orders объекта Customer, и текущий Order передается в качестве удаляемого Order:

...
       previousValue.Orders.Remove(this);
}

Вызов метода Remove инициирует вызов метода detach_Orders класса Customer и передает ему подлежащий удалению Order. В методе detach_Orders свойству Customer переданного объекта Order присваивается null.

Когда вызывается метод detach_Orders, свойство Customer переданного Order получает значение null. Это приводит к вызову метода set свойства Customer объекта Order, а этот метод вызывает код, обращающийся, в свою очередь, к методу detach_Orders, так что метод, запустивший этот процесс удаления Order, вызывается рекурсивно, и значение null передается в качестве value в метод set. Поток выполнения теперь находится в рекурсивном вызове метода set свойства Customer.

В четвертой строке метода set проверяется переданное значение value, и если оно эквивалентно свойству Entity текущего назначенного свойства Customer, этот рекурсивный вызов метода set возвращает управление, ничего не делая.

Поскольку в предыдущей строке кода первого не рекурсивного вызова метода set свойству Entity свойства Customer присваивается значение null, и поскольку null был передан как value в метод detach_Orders, они на самом деле эквивалентны: существует рекурсивный вызов метода set, который ничего не делает, и тогда поток управления возвращается обратно — к первому вызову метода set. Именно это имелось в виду в предыдущем абзаце, когда говорилось, что свойство Entity устанавливается в null, чтобы остановить рекурсию. Итак, как только рекурсивный вызов метода set вернул управление, поток возвращается к последней строке начального вызова метода set, который обсуждался ранее.

Как только метод Orders.Remove завершает работу, свойство Orders объекта Customer более не содержит ссылки на этот Order, и потому граф вновь находится в согласованном состоянии.

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

Затем в свойство Entity объекта Customer объекта Order устанавливается новый объект Customer, который был передан методу set в параметре value:

...
this._Customer.Entity = value;

В конце концов, это метод set свойства Customer. Мы пытались назначить Order новому Customer. И, опять-таки, в этой точке Order содержит ссылку на вновь назначенный Customer, а вновь назначенный Customer не имеет ссылки на Order, так что граф отныне не согласован.

Далее код проверяет, не равен ли null назначаемый Order объект Customer, потому что если нет, то вновь назначенный Customer следует назначить Order:

if ((value != null))
{
   ...

Если переданный в value объект Customer не равен null, нужно добавить текущий Order в коллекцию объектов Orders переданного Customer:

value.Orders.Add(this);

Когда в этой строке Order добавляется в коллекцию Orders переданного объекта Customer, то вызывается делегат, переданный методу обратного вызова в конструкторе EntitySet<T> объекта Customer. Поэтому результатом присваивания является вызов метода attach__Orders объекта Customer.

Это, в свою очередь, установит свойство Customer текущего объекта Order в переданный Customer, в результате чего опять вызывается метод set свойства Customer объекта Order. Код рекурсивно входит в метод set, как делал это ранее. Однако всего двумя операторами кода перед предыдущим оператором и перед началом рекурсии свойство Entity свойства Customer объекта Order было установлено в новый Customer, который передан методу set методом attach_Orders. Опять-таки, код метода set вызывается рекурсивно, и в конечном итоге вызывается следующая строка кода:

if (((previousValue != value)
   || (this._Customer.HasLoadedOrAssignedValue == false)))

Поскольку текущий объект Customer объекта Order, который теперь хранится в previousValue, и параметр value — одно и то же, метод set возвращает управление, ничего более не делая, в результате чего рекурсия прекращается.

В следующей строке кода член CustomerlID текущего объекта Order устанавливается в CustomerID нового объекта Customer:

this._CustomerID = value.CustomerID;

Если вновь назначаемый Customer равен null, то этот код просто устанавливает член CustomerID текущего объекта Order в значение по умолчанию для его типа данных — в этом случае типа string:

...
else
{
		this._CustomerID = default(string);
}

Если бы член CustomerID был типа int, код установил бы его в default(int).

В самой последней строке кода вызывается метод SendPropertyChanged и ему передается имя изменяемого свойства, чтобы инициировать событие уведомления об изменении:

this.SendPropertyChanged("Customer");

Этот шаблон действителен для отношения "один ко многим". Для отношения "один к одному" каждая сторона должны была быть реализована, как была реализована дочерняя в этом примере, но с парой изменений. Поскольку в отношении "один к одному" нет логического родительского или дочернего объекта, предположим, что отношение между заказчиками и заказами строится по схеме "один к одному". Это даст имя для использования в качестве ссылки на каждый класс, поскольку родительский и дочерний объекты более не применимы.

Если вы пишете сущностный класс вручную, и отношение между классом Customer и классом Order — "один к одному", тогда каждый из этих классов будет содержать свойство типа EntityRef<T>, где T — тип противоположного класса. Класс Customer будет содержать EntityRef<Order>, а класс Order — EntityRef<Customer>. Поскольку ни один из классов не содержит EntitySet<T>, нет вызовов методов Add и Remove, которые присутствуют в ситуации с отношениями "один ко многим", описанной ранее.

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

Таким образом, вместо такой строки кода:

previousValue.Orders.Remove(this);

будет следующая строка кода:

previousValue.Order = null;

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

Поэтому вместо следующей строки кода:

value.Orders.Add(this);

будет такая строка:

value.Order = this;

Как видите, обработка согласованности графа нетривиальна, и в ней легко запутаться. К счастью, существуют два инструмента, которые обо всем позаботятся — SQLMetal и Object Relational Designer. Они адекватно поддерживают согласованность графа и правильную реализацию уведомлений об изменении.

Вызов частичных методов

Когда в Microsoft добавили частичные методы для облегчения расширения сгенерированного кода, такого как код сущностных классов, то тем самым возложили на вас немного больше ответственности, если вы собираетесь самостоятельно реализовывать сущностные классы.

Существуют несколько частичных методов, которые должны быть объявлены во вручную написанных сущностных классах:

partial void OnLoaded();
partial void OnValidate(ChangeAction action); 
partial void OnCreated();
partial void On[Property]Changing(int value); 
partial void On[Property]Changed();

Для каждого свойства сущностного класса должна существовать пара методов On[Property]Changing и On[Property]Changed.

Что касается методов OnLoaded и OnValidate, то не нужно добавлять их вызовы где-либо в коде сущностных классов; их будет вызывать DataContext.

Внутрь конструктора сущностного класса потребуется добавить код вызова метода OnCreate, как показано ниже:

public Customer()
		{
			...
			OnCreated();
		}

Затем для каждого свойства сущностного класса должны быть добавлены вызовы методов On[Property]Changing и On[Property]Changed непосредственно перед и после изменения этого свойства, как показано ниже:

// Из сгенерированного сущностного класса Customer
...

[Column(Storage="_CustomerID", DbType="NChar(5) NOT NULL", CanBeNull=false, IsPrimaryKey=true)]
		public string CustomerID
		{
			get
			{
				return this._CustomerID;
			}
			set
			{
				if ((this._CustomerID != value))
				{
					this.OnCustomerIDChanging(value);
					this.SendPropertyChanging();
					this._CustomerID = value;
					this.SendPropertyChanged("CustomerID");
					this.OnCustomerIDChanged();
				}
			}
		}

Обратите внимание, что метод On[Property]Changing вызывается перед вызовом метода SendPropertyChanging, а метод On[Property]Changed — после метода SendPropertyChanged.

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

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