Hungry Mind , Blog about everything in IT - C#, Java, C++, .NET, Windows, WinAPI, ...

Показаны сообщения с ярлыком C#. Показать все сообщения
Показаны сообщения с ярлыком C#. Показать все сообщения

ReaderWriterLockSlim fails on dual-socket environments

This is yet another story of orphaned ReaderWriterLockSlim.

Another dump, the same problem - ReaderWriterLockSlim object state is corrupted:

0:173> !do 0x0000000001c679f8
Name:        System.Threading.ReaderWriterLockSlim
MethodTable: 000007f87ec7c1d8
EEClass:     000007f87e999448
Size:        96(0x60) bytes
File:        C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Core\v4.0_4.0.0.0__b77a5c561934e089\System.Core.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007f880dbcbf0  4000755       50       System.Boolean  1 instance                1 fIsReentrant
000007f880dbe0c8  4000756       30         System.Int32  1 instance                0 myLock
000007f880db2308  4000757       34        System.UInt32  1 instance                1 numWriteWaiters
000007f880db2308  4000758       38        System.UInt32  1 instance               28 numReadWaiters
000007f880db2308  4000759       3c        System.UInt32  1 instance                0 numWriteUpgradeWaiters
000007f880db2308  400075a       40        System.UInt32  1 instance                0 numUpgradeWaiters
000007f880dbcbf0  400075b       51       System.Boolean  1 instance                0 fNoWaiters
000007f880dbe0c8  400075c       44         System.Int32  1 instance               -1 upgradeLockOwnerId
000007f880dbe0c8  400075d       48         System.Int32  1 instance               -1 writeLockOwnerId
000007f880db9138  400075e        8 ...g.EventWaitHandle  0 instance 000000000381eb38 writeEvent
000007f880db9138  400075f       10 ...g.EventWaitHandle  0 instance 00000000035a32e0 readEvent
000007f880db9138  4000760       18 ...g.EventWaitHandle  0 instance 0000000000000000 upgradeEvent
000007f880db9138  4000761       20 ...g.EventWaitHandle  0 instance 0000000000000000 waitUpgradeEvent
000007f880dd0398  4000763       28         System.Int64  1 instance 9 lockID
000007f880dbcbf0  4000765       52       System.Boolean  1 instance                0 fUpgradeThreadHoldingRead
000007f880db2308  4000766       4c        System.UInt32  1 instance       1073741824 owners
000007f880dbcbf0  4000767       53       System.Boolean  1 instance                0 fDisposed
000007f880dd0398  4000762      408         System.Int64  1   static 14118 s_nextLockID
000007f87ec99a20  4000764        8 ...ReaderWriterCount  0 TLstatic  t_rwc
0:173> .formats 0n1073741824 
Evaluate expression:
  Hex:     00000000`40000000

EnterReadLock, EnterWriteLock and other Enter operations waiting for an event which never goes off. Deadlock.

I must say that I checked possibilities of thread aborts in this code and found no signs of such scenarios happening. This made me desperately searching for another root cause of the problem.

So started searching ReaderWriterLockSlim.cs file for potential problems. I immediately became suspicious when I realyzed there is lack of synchronization when, for example, TryEnterUpgradeableReadLockCore method modified one of object fields:

uint owners;
...
private bool TryEnterReadLockCore(TimeoutTracker timeout)
{ 
...
       owners++;
}

Fields are not declared volatile, nor are they modified via interlocked operations. The only exception is the myLock field, which is used as a spin lock ang modified via Interlocked.CompareExchange:

[MethodImpl(MethodImplOptions.AggressiveInlining)] 
private void EnterMyLock()
{
   if (Interlocked.CompareExchange(ref myLock, 1, 0) != 0)
      EnterMyLockSpin(); 
}

Note, however, spin lock release method doesn't use Interlocked operation:

private void ExitMyLock()
{ 
   Debug.Assert(myLock != 0, "Exiting spin lock that is not held");
   myLock = 0; 
} 

This looks to be a mistake, possibly the root cause on one of root causes.

OK, lets go back to the problem - ReaderWriterLockSlim gets locked forever on 24-core dual socket Intel hardware. Threads are not aborted, the code is perfect. So what the hell is going on?

Well, the problem looks to be bad software (ReaderWriterLockSlim) on expensive hardware. Dell PowerEdge R720 has two psysical CPUs - 2x Intel Xeon E5-2620, 1200 MHz (12 x 100), 6 cores and 12 threads each. 24 logical cores total. And the problem is experienced only on such configurations.

I made a program that creates 24 (= Environment.ProcessorCount) threads with highest priority acquiring and releasing the lock in a tight loop:

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
namespace RWLSTest
{
   internal class Program
   {
      private static readonly ReaderWriterLockSlim slim = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
      private static readonly List<object> objects = new List<object>();
      private static readonly Int32 processorCount = Environment.ProcessorCount;
      private static Int32 threadsCount;
      private static Int64 reads;
      private static Int64 writes;
      private static volatile Object[] threads = new Object[processorCount];
      private static Action loopAction;
      static Program()
      {
         // Let it JIT those methods
         using (var temp = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion)) {
            Thread.Yield();
            temp.EnterReadLock();
            temp.ExitReadLock();
         }
         var thread = new Thread(() =>
         {
            try {
               Thread.Sleep(Timeout.Infinite);
            }
            catch {
               return;
            }
            throw new InvalidOperationException();
         });
         thread.Start();
         try {
            thread.Abort();
         }
         catch (Exception e) {
            Console.WriteLine(e.Message);
         }
      }
      private static void LoopWithEmptryTryBlocks()
      {
         var random = new Random(Environment.TickCount);
         for (;;) {
            if (random.Next(processorCount) <= (processorCount / 4)) {
               Interlocked.Increment(ref writes);
               try {}
               finally {
                  slim.EnterWriteLock();
               }
               try {
                  ExclusiveLoop(random);
               }
               finally {
                  slim.ExitWriteLock();
               }
            }
            else {
               Interlocked.Increment(ref reads);
               try {}
               finally {
                  slim.EnterReadLock();
               }
               try {
                  SharedLoop(random);
               }
               finally {
                  slim.ExitReadLock();
               }
            }
         }
      }
      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      private static void SharedLoop(Random random)
      {
         foreach (var o in objects) {
            var i = (Int32)o;
            if ((i % processorCount) == (random.Next() % processorCount) && random.Next(37) == 3) {
               break;
            }
         }
      }
      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      private static void ExclusiveLoop(Random random)
      {
         if (objects.Count < 10240) {
            for (var i = 0; i < 19; ++i) {
               if (random.Next(13) == 7) {
                  objects.Add(random.Next());
               }
            }
         }
         for (var i = 0; i < 13; ++i) {
            if (objects.Count > 0 && random.Next(19) == 13) {
               objects.Remove(random.Next() % objects.Count);
            }
         }
      }
      private static void Loop()
      {
         var random = new Random(Environment.TickCount);
         for (;;) {
            if (random.Next(processorCount) <= (processorCount / 4)) {
               slim.EnterWriteLock();
               try {
                  ExclusiveLoop(random);
               }
               finally {
                  slim.ExitWriteLock();
               }
            }
            else {
               slim.EnterReadLock();
               try {
                  SharedLoop(random);
               }
               finally {
                  slim.ExitReadLock();
               }
            }
         }
      }
      private static void StartOneThread(Object state)
      {
         var thread = new Thread(() =>
         {
            try {
               Interlocked.Increment(ref threadsCount);
               loopAction();
            }
            catch (ThreadAbortException) {}
            finally {
               Interlocked.Decrement(ref threadsCount);
               ThreadPool.UnsafeQueueUserWorkItem(StartOneThread, state);
            }
         }) { Priority = ThreadPriority.Highest };
         thread.Start();
         Thread.VolatileWrite(ref threads[(Int32)state], thread);
      }
      private static void Main(string[] args)
      {
         var random = new Random(Environment.TickCount);
         var abortCycle = 0;
         if (args.Length > 0) {
            abortCycle = Int32.Parse(args[0]);
            loopAction = LoopWithEmptryTryBlocks;
         }
         else {
            loopAction = Loop;
         }
         for (var i = 0; i < processorCount; ++i) {
            StartOneThread(i);
         }
         for (var i = 0U;; ++i) {
            Thread.Sleep(1);
            if (abortCycle > 0 && i % abortCycle == 0) {
               var ti = random.Next(111) % processorCount;
               var thread = (Thread)Thread.VolatileRead(ref threads[ti]);
               if (thread != null) {
                  Console.WriteLine("Aborting thread #" + ti);
                  try {
                     thread.Abort();
                  }
                  catch (Exception e) {
                     Console.WriteLine(e.Message);
                  }
               }
            }
         }
      }
   }
}

I ran it several times and after about 1 hour all threads ended up waiting for lock event to fire. Voila! Have a look at the state of ReaderWriterLockSlim object:

0:000> !do 000000b343bd2860 
Name:        System.Threading.ReaderWriterLockSlim
MethodTable: 000007fbf887c1a8
EEClass:     000007fbf8599448
Size:        96(0x60) bytes
File:        C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Core\v4.0_4.0.0.0__b77a5c561934e089\System.Core.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007fbfe6ac7b8  4000755       50       System.Boolean  1 instance                1 fIsReentrant
000007fbfe6adc90  4000756       30         System.Int32  1 instance                0 myLock
000007fbfe6a1ed0  4000757       34        System.UInt32  1 instance                1 numWriteWaiters
000007fbfe6a1ed0  4000758       38        System.UInt32  1 instance               23 numReadWaiters
000007fbfe6a1ed0  4000759       3c        System.UInt32  1 instance                0 numWriteUpgradeWaiters
000007fbfe6a1ed0  400075a       40        System.UInt32  1 instance                0 numUpgradeWaiters
000007fbfe6ac7b8  400075b       51       System.Boolean  1 instance                0 fNoWaiters
000007fbfe6adc90  400075c       44         System.Int32  1 instance               -1 upgradeLockOwnerId
000007fbfe6adc90  400075d       48         System.Int32  1 instance               -1 writeLockOwnerId
000007fbfe6a8d00  400075e        8 ...g.EventWaitHandle  0 instance 000000b343beb448 writeEvent
000007fbfe6a8d00  400075f       10 ...g.EventWaitHandle  0 instance 000000b343be2fd0 readEvent
000007fbfe6a8d00  4000760       18 ...g.EventWaitHandle  0 instance 0000000000000000 upgradeEvent
000007fbfe6a8d00  4000761       20 ...g.EventWaitHandle  0 instance 0000000000000000 waitUpgradeEvent
000007fbfe6bff60  4000763       28         System.Int64  1 instance 1 lockID
000007fbfe6ac7b8  4000765       52       System.Boolean  1 instance                0 fUpgradeThreadHoldingRead
000007fbfe6a1ed0  4000766       4c        System.UInt32  1 instance       1073741824 owners
000007fbfe6ac7b8  4000767       53       System.Boolean  1 instance                0 fDisposed
000007fbfe6bff60  4000762      408         System.Int64  1   static 2 s_nextLockID
000007fbf88999f0  4000764        8 ...ReaderWriterCount  0 TLstatic  t_rwc

There are 23 reader waiters, 1 writer waiter and owners field is 0x40000000 once again. All of 24 threads look like the following:

0:000> ~22e !CLRStack
OS Thread Id: 0xf28 (22)
        Child SP               IP Call Site
000000b361a1df78 000007fc137b315b [HelperMethodFrame_1OBJ: 000000b361a1df78] System.Threading.WaitHandle.WaitOneNative(System.Runtime.InteropServices.SafeHandle, UInt32, Boolean, Boolean)
000000b361a1e0a0 000007fbfe5195c4 System.Threading.WaitHandle.InternalWaitOne(System.Runtime.InteropServices.SafeHandle, Int64, Boolean, Boolean)
000000b361a1e0e0 000007fbf8af4c25 System.Threading.ReaderWriterLockSlim.WaitOnEvent(System.Threading.EventWaitHandle, UInt32 ByRef, TimeoutTracker)
000000b361a1e150 000007fbf8dd4c48 System.Threading.ReaderWriterLockSlim.TryEnterReadLockCore(TimeoutTracker)
000000b361a1e1b0 000007fbf8804d4a System.Threading.ReaderWriterLockSlim.TryEnterReadLock(TimeoutTracker)
000000b361a1e200 000007fbf8af55ad System.Threading.ReaderWriterLockSlim.TryEnterReadLock(Int32)
000000b361a1e250 000007fba0010a45 RWLSTest.Program.Loop()
000000b361a1e2c0 000007fba00106f7 RWLSTest.Program+<>c__DisplayClass4.<startonethread>b__3()
000000b361a1e330 000007fbfe4ff8a5 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
000000b361a1e490 000007fbfe4ff609 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
000000b361a1e4c0 000007fbfe4ff5c7 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
000000b361a1e510 000007fbfe512d21 System.Threading.ThreadHelper.ThreadStart()
000000b361a1e828 000007fbff6bf713 [GCFrame: 000000b361a1e828] 
000000b361a1eb58 000007fbff6bf713 [DebuggerU2MCatchHandlerFrame: 000000b361a1eb58] 

They all WaitOnEvent, but who and when will fire the event? This never happens. Deadlock.

Now lets get back to ExitMyLock. ReaderWriterLockSlim contains many fields with at least 88 bytes storage space required (without extra aligning, if needed). Modern Intel CPUs have cache lines of 64 bytes which is too small to entirely hold ReaderWriterLockSlim object instance. So each one requires at least two cache lines to hold its data. Since the distance between myLock and owners fields is more than 64 bytes (both x86 and x64), releasing myLock without a memory barrier (or interlocked instruction) causes only a portion of object's storage invalidated on demand between CPU cores and/or CPUs. Invalidation is forced by EnterMyLock's interlocked instruction. But only 64 bytes of aligned memory where myLock resides. Other cache line's changes might not be visible at that point. So the core acquiring the lock may see inconsistent object state.

Very important note: ReaderWriterLockSlim.cs is a part of 4.5Update1 reference source. Vanilla .NET 4.5 and probably several updates following it has this code, for example 4.0.30319.17929, 4.0.30319.18408. Recent versions, for example 4.0.30319.33440, has fixed this:

private void ExitMyLock()
{ 
   Volatile.Write(ref myLock, 0);
}

Volatile write inserts explicit memory barriers and makes any changes visible to other cores and CPUs.

Conclusion: do not use ReaderWriterLockSlim class without .NET Framework updated to at least 4.0.30319.33440. Its will eventually fail, at least on dual-socket Intel system.

Windows 8.1 and Windows Server 2012 R2 have this issue fixed. Windows Server 2012 (nor R2) seems to stuck with buggy implementation of ReaderWriterLockSlim class. After installing all available updates, ExitMyLock looks the same (no volatile write operation).

Arithmetic overflows in C#/C/C++

Иногда самая банальная вещь может быть невероятно полезной. К примеру, как бы Вы решили проблему переполнения в результате арифметических операций, скажем, на языке C/C++? Уверен, пришлось бы анализировать операнды перед операций в поиске возможного переполнения, что есть не очень тривиальная задача. Еще один недостаток такого решения - производительность, анализ будет дороже самой операции. Конечно, можно использовать ассемблерные вставки типа _asm jo OverflowedLabel после каждой операции, но это скорее маразм. Я хочу заметить, что язык ассемблера позволяет постфактум проверять флаг переполнения последней арифметической операции.

Язык C#, IL код и CLR позволяют легко выявлять переполнения с помощью т.н. checked секций кода. Одноименное ключевое слово языка C# определяет блок кода в котором переполнения приводят к исключению OverflowException. Реализация проста до безобразия - JIT вставляет операцию jo SomeAddr после каждой операции внутри блока checked, а по адресу SomeAddr находится одна единственная инструкция - call clr!JIT_Overflow.

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

Label with vertical auto sizing

В проекте потребовалась лейба с автоподгоном высоты и фиксированной в пределах формы шириной, чтобы работал AutoEllipsis. Проблема в том, что AutoSize меняет также ширину лейбы. Сначала в голову пришла мысль считать высоту текста через Graphics или TextRenderer, но покопавшись в исходниках класса Label я выяснил, что его метод GetPreferredSize возвращает требуемый размер. Приятно то как! В итоге получился такой вот кусочек класса-наследника Label:

/// <summary>
/// See <see cref="Label.OnFontChanged"/>.
/// </summary>
protected override void OnFontChanged(EventArgs e)
{
    base.OnFontChanged(e);

    adjustHeight();
}

/// <summary>
/// See <see cref="Label.OnTextChanged"/>.
/// </summary>
protected override void OnTextChanged(EventArgs e)
{
    base.OnTextChanged(e);

    adjustHeight();
}

#endregion

#region Private methods

/// <summary>
/// Adjust <see cref="Control.Height"/> so that the text is fully visible vertically.
/// </summary>
private void adjustHeight()
{
    if (AutoSize)
    {
        return;
    }
    Height = PreferredSize.Height;
}

Funny code

Надо придумать специальное название для таких вот смешных кусков кода: Control.ModifierKeys == Keys.Control.

Too much exception detalization

В коде нашел:
catch (Exception ex)
{
   ...
   ex.Data.Add(ex, ex.Message);
   ...
}

Entity Framework Horizontal Entity splitting

Продолжая тему The splendour and misery of Entity Framework, хочу рассказать подробнее о схеме Horizontal Entity splitting, которая не поддерживается текущей версией дизайнера EF Visual Studio 2008 SP1.

Подготовка EDM дизайнером

Начем с простой концептуальной модели.
Sample Entity diagram
Схема базы данных описывается таким скриптом:

CREATE TABLE [HorizontalEntitySplitting].[Part1](
[ID] INT IDENTITY(1,2) PRIMARY KEY NOT NULL,
[Data] NVARCHAR(max) NULL,
)

CREATE TABLE [HorizontalEntitySplitting].[Part2](
[ID] INT IDENTITY(2,2) PRIMARY KEY NOT NULL,
[Data] NVARCHAR(max) NULL,
)

Обратите внимание на IDENTITY(1,2) и IDENTITY(2,2). Автогенераторы гарантируют уникальность ID в пределах двух таблиц вместе.

Следующий шаг - привязка модели к БД, импорт таблиц Part1 и Part2. Желательно выполнить отображение сущности на одну из таблиц, чтобы потом было проще править msl-файл вручную.

Ручное редактирование EDM

При ручном редактировании edmx-файла возможны проблемы с валидатором. Дело в том, что msbuild выполняет проверку средствами дизайнера и завершается с ошибкой, если модель содержит непонятные элементы. С нашим сценарием именно этот случай.

Полученный edmx-файл нужно разделить на 3. Сделать это можно вручную, но лучше воспользоваться готовой утилитой EdmGen2. Полученные csdl, ssdl и msl-файлы необходимо включить в проект, а в качестве значения Build Action поставить Embedded Resource (чтобы в строке подключения указать путь к этим файлам в виде ссылки на ресурсы сборки). Затем, воспользовавшись утилитой EDM Generator (EdmGen.exe), получить файл с кодом и тоже включить его в состав проекта.

Савый важный шаг - изменение msl-файла. Необходимо отобразить сущность на две таблицы по некоторому условию. К сожалению, текущая версия Entity Framework позволяет накладывать условие лишь на булевое свойство сущности, тем самым ограничивая количество таблиц числом 2. Добавленные фрагменты выделены комментариями:

<Mapping xmlns="urn:schemas-microsoft-com:windows:storage:mapping:CS" Space="C-S">
    <Alias Key="Model" Value="HorizontalSplitting" />
    <Alias Key="Target" Value="HorizontalSplittingContainer.Store" />
    <EntityContainerMapping CdmEntityContainer="HorizontalSplittingEntities" StorageEntityContainer="HorizontalSplittingContainerStoreContainer">
        <EntitySetMapping Name="Sample">
            <EntityTypeMapping TypeName="IsTypeOf(HorizontalSplittingContainer.Sample)">
                <MappingFragment StoreEntitySet="Part1">
                    <ScalarProperty Name="Data" ColumnName="Data" />
                    <ScalarProperty Name="ID" ColumnName="ID" />
                    <!-- BEGIN MODIFICATION -->
                    <Condition Name="Selector" Value="true" />
                    <!-- END MODIFICATION -->
                </MappingFragment>
                <!-- BEGIN MODIFICATION -->
                <MappingFragment StoreEntitySet="Part2">
                    <ScalarProperty Name="Data" ColumnName="Data" />
                    <ScalarProperty Name="ID" ColumnName="ID" />
                    <Condition Name="Selector" Value="false" />
                </MappingFragment>
                <!-- END MODIFICATION -->
            </EntityTypeMapping>
        </EntitySetMapping>
    </EntityContainerMapping>
</Mapping>

Пример использования

Первое, что необходимо сделать - проверить адекватность строки подключения в app.config. Может возникнуть проблема с именами ресурсов т.к. имя ресурса формируется по шаблону {DefaultProjectNamespace}.{Folder}.{Folder}...{FileName}. В моем случае строка подключения выглядит вот так:

<add name="HorizontalSplittingEntities" connectionString="metadata=res://*/Part2_MappingSchemes.HorizontalSplitting.HorizontalSplitting.csdl|res://*/Part2_MappingSchemes.HorizontalSplitting.HorizontalSplitting.ssdl|res://*/Part2_MappingSchemes.HorizontalSplitting.HorizontalSplitting.msl;provider=System.Data.SqlClient;provider connection string=&quot;Data Source=veastduo;Initial Catalog=Test;Integrated Security=True;MultipleActiveResultSets=True&quot;" providerName="System.Data.EntityClient" />

Расмотрим небольшой отрывок кода:

using (var horizontalSplittingEntities = new HorizontalSplittingEntities()) {
    foreach (var sample in horizontalSplittingEntities.Sample) {}
    if (horizontalSplittingEntities.Sample.Count() < 10000) {
        for (Int32 i = 0; i < 1000; ++i) {
            horizontalSplittingEntities.AddToSample(new Sample {Data = DateTime.Today.ToString(), Selector = i%2 == 1});
        }
    }
    horizontalSplittingEntities.SaveChanges();
}

Код выполняет две операции - чтение данных из БД и запись новых объектов в БД.

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

SELECT
[UnionAll1].[ID] AS [C1],
[UnionAll1].[Data] AS [C2],
CASE WHEN ([UnionAll1].[C1] = 1) THEN cast(1 as bit) ELSE cast(0 as bit) END AS [C3]
FROM  (SELECT
    [Extent1].[ID] AS [ID],
    [Extent1].[Data] AS [Data],
    cast(0 as bit) AS [C1]
    FROM [HorizontalEntitySplitting].[Part2] AS [Extent1]
UNION ALL
    SELECT
    [Extent2].[ID] AS [ID],
    [Extent2].[Data] AS [Data],
    cast(1 as bit) AS [C1]
    FROM [HorizontalEntitySplitting].[Part1] AS [Extent2]) AS [UnionAll1]

А колонка [C1], кстати, и есть наш булевый переключатель.

С сохранением тоже нетрудно догадаться - в зависимости от значения свойства Selector используется одна из двух таблиц:

exec sp_executesql N'insert [HorizontalEntitySplitting].[Part1]([Data])
values (@0)
select [ID]
from [HorizontalEntitySplitting].[Part1]
where @@ROWCOUNT > 0 and [ID] = scope_identity()',N'@0 nvarchar(18)',@0=N'04.09.2008 0:00:00'
exec sp_executesql N'insert [HorizontalEntitySplitting].[Part2]([Data])
values (@0)
select [ID]
from [HorizontalEntitySplitting].[Part1]
where @@ROWCOUNT > 0 and [ID] = scope_identity()',N'@0 nvarchar(18)',@0=N'04.09.2008 0:00:00'

Выводы

Поддержка разделения сущности горизонтально - прикольная фишка. Но в текущем варианте, когда можно использовать лишь две таблицы и булевый переключатель, ее полезность существенно падает. Кстати, многие коммерческие базы данных (Oracle, MSSQL) поддерживают горизонтальное деление таблиц на физическом уровне. Поэтому такая схема возможно понадобится малоимущим.

ReSharper 4.1 is available

Download it here.

The splendour and misery of Entity Framework designer, architecture and runtime

Entity Framework уже больше года блуждает призраком по просторам интернета. Несколько CTP-шек, несколько Beta-версий, абстрактные разговоры о его безграничных возможностях и куча примеров в стиле Hello World, которыми автора, наверное, хотят продемонстрировать нам всю мощь LINQ to Entities или удивить своими обширными познаниями. Я поигрался несколько дней и далее вы прочитаете мои открытия и впечатления.

Что такое Entity Framework

Маркетологи Microsoft уверяют нас, простых смертных программистов, что Entity Framework - начало нового века, что it's the first step in a much larger vision of an entity-aware data platform. А люди, которые на этом собаку съели, не верят. И я не верю. Entity Framework в текущем виде представляет собой не что иное, как OR/Mapper, причем слабенький. С другой стороны, учитывая политику Microsoft, Entity Framework будет развиваться и обрастать. Уже заявлено начало работ над 2-й версией. Вероятнее всего, дальнейшая архитектура data-driven приложений и фреймворков будет строиться именно на Entity Framework.

Бороздя просторы интернета, я наткнулся на ADO.NET Entity Framework Vote of No Confidence - петиция к Microsoft по поводу проблем Entity Framework. Советую почитать, если есть время. Забавно просто очень - целая петиция.

Состав Entity Framework

Entity Framework состоит из таких основных компонентов:

  1. Дизайнер Visual Studio 2008 SP1
  2. Кодогенератор, входит в .NET Framework 3.5 SP1
  3. Библиотеки поддержки, входит в .NET Framework 3.5 SP1

Рекомендую сразу взлгянуть на раздел (я этого не делал и, кстати, жалею - там много полезного написано для начального изучения) MSDN --> MSDN Library --> Development Tools and Languages --> Visual Studio 2008 --> Visual Studio --> .NET Framework Programming in Visual Studio --> Accessing Data --> ADO.NET --> Entity Data Model Tools.

Visual Studio 2008 Entity Framework Visual Designer

Visual Studio 2008 SP1 позволяет визуально редактировать модель Entity Framework, которая состоит из 3-х частей:

  1. Conceptual Schema - CSDL-файл, описывающий сущности и отношения между ними.
  2. Storage Metadata Schema - SSDL-файл, описывающий схему базы данных.
  3. Mapping Specification - MSL-файл, связывающий сущности и способ их хранения в реляционной БД.

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

Информация, заложенная в этих файлах, называется Entity Data Model, или сокращенно - EDM.

Я сейчас добавлю в проект пустую модель из шаблона ADO.NET Entity Data Model и назову ее TestModel:
Add new ADO.NET Entity Data Model

Мастер Entity Data Model предлагает нам создать пустую модель или сгенерировать ее по существующей базе данных:
Entity Data Model Wizard
Мы выберем Empty model и я уточню, что никакой генерации, фактически, не происходит. Дизайнер просто сразу связывает модель со схемой конкретной базы данных. Это действие можно выполнить в любой момент после. В результате работы мастера в проекте появляется файл TestModel.edmx. Это обычный XML, в котором хранятся CSDL, SSDL, MSL и вспомогательный контент самого дизайнера Entity Framework.

Элементы дизайнера Entity Framework

  1. Model Browser - показывает нам списки сущностей, ассоциаций между ними, наборы сущностей и схему БД в упрощенном виде. Окошко не позволяет производить никаких изменяющих схему манипуляций с объектами.

    Entity Framework Model Browser

  2. Mapping Details - позволяет связать сущность с физической схемой БД.

    Entity Framework Mapping Details

    Окошко имеет две закладки (названия, полагаю, говорят сами за себя):

    1. Map Entity to Tables / Views

      Связывание сущности с одной или несколькими таблицами БД.

    2. Map Entity to Functions

      Связывание операций создания, изменения и удаления сущности с процедурным кодом БД.

    Mapping Details используется не только для сущностей, но и для ассоциаций между ними.

  3. Полотно концептуального дизайнера.

    Entity Framework edmx designer surface

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

  4. Окна Toolbox и Properties.

Начинаем работать с дизайнером - концептуальная модель (CSDL)

Уверен, читатель сможет без труда создать подобие схемки на картинке выше и убедиться (или просто поверить), что

  • Есть возможность указать сущность, как абстрактную.
  • Есть поддержка композитных первичных ключей.
  • Есть возможность документирования - Long Description, Summary для всех элементов концептуальной модели.
  • Есть поддержка оптимистического параллелизма (optimistic concurrency) - Concurrency Mode = Fixed (единственный режим).
  • Поддерживаются только простые типы данных - Binary, Boolean, Byte, DateTime, DateTimeOffset, Decimal, Double, Guid, Int16, Int32, Int64, SByte, Single, String, Time и их Nullable вариации.

    CSDL и MSL, кстати, поддерживают ComplexType. Можно, например, сгруппировать несколько атрибутов сущности в отдельный тип. Но текущая версия дизайнера это не поддерживает. Она, вообще, мало чего поддерживает.

  • Есть поддержка ассоциаций с кардинаностями [0..1 - *], [1 - 0..1], [1 - 1], [1 - *], [* - *].
  • Невозможно скрыть (не генерировать в коде) одну из сторон ассоциации (например, у справочника).
  • Нет поддержки отложенной загрузки (Lazy Loading).
  • Нет поддержки свойств-перечислений.
  • Нельзя менять порядок свойств в сущностях.
  • Алгоритм расположения сущностей на полотне единственный, случайный и в результате неудобный.
  • Валидация модели - это проверка edmx-файла на корректность. Поэтому сообщения об ошибке не говорят почти ничего о ситуации на экране.
  • Дизайнер не отображает абстрактность класса.
  • Возможности Mapping Details по фильтрации очень ограниченные - поддерживаются только операторы =, IS NULL, IS NOT NULL, на колонку можно наложить лишь одно условие, все условия соединяются логическим оператором AND.
  • Visual Studio 2008 SP1 иногда банально перестает открывать edmx-файл дизайнером.

Далее выдержка из MSDN. The following are Entity Framework features that are not currently supported by the Entity Designer:

  • Multiple entity sets per type.
  • Creating entity sets for non-root types.
  • Table-per-concrete class mapping.

    Не поддерживает, но и не ругается - покажу, как подкрутить.

  • Using EntityType properties in mapping conditions.
  • Editing storage model elements.
  • Unmapped abstract types. When you create an abstract entity type with the Entity Designer, the type must be mapped to a table or view.
  • Creating conditions on association mappings.
  • Mapping associations directly to stored procedures. Mapping many-to-many associations is not supported. You can indirectly map other associations to stored procedures along with entity types by mapping the appropriate navigation properties to stored procedure parameters.
  • Creating conditions on Function Import mappings.
  • Complex types.
  • Annotations.
  • QueryViews.
  • Specifying a parameter on an update function to return the number of rows affected. The Entity Designer does not expose a user interface for specifying this output parameter. However, you can manually edit the .edmx file so that the update function will handle this output parameter.
  • Models that contain references to other models.

Спускаемся с небес на землю - физическая модель (SSDL)

Первое, что необходимо сделать - подключить базу данных к модели Entity Framework - пункт меню Update Model from Database... вызывает Update Model Wizard:
Update Model from Database wizard
Далее нам предлагают внести изменения в нашу модель:
Choose your Database Objects
Обращаю внимание, что основная задача этого мастера - синхронизация физической модели БД со схемой SSDL. При добавлении таблицы мастер создаст одноименную сущность, наполнив ее нужными атрибутами, и автоматически выполнит связывание с таблицей (mapping), но этот эффект единичный - после удаления сущности из концептуальной модели вернуть ее таким образом будет невозможно, придется создавать вручную! Я сначала думал, что это дефект дизайнера, но потом понял, что синхронизируется лишь SSDL схема. Кстати, удалить из SSDL ненужные объекты БД можно только тогда, когда они перестали существовать в самой БД.

Связывание концептуальной и физической моделей (MSL) - отображение (mapping)

Завершающая стадия проектирования модели Entity Framework - отображение концептуальной модели на физическую. Здесь нужно рассмотреть различные сценарии, которые поддерживаются текущей версией платформы и/или дизайнером.

Отображение сущности на таблицу

Эта процедура выполняется с помощью окна Mapping Details. Следующая картинка демонстрирует сущность ComplexObject и ее отображение на одноименную таблицу БД:
Simple Entity mapping to one database table
Обратите внимание, что дизайнер позволяет наложить условия на поля таблицы, а также добавить еще одну или несколько таблиц. Это потребуется для следующих двух сценариев.

Vertical Entity splitting

Entity Framework позволяет вертикально отобразить несколько таблиц на одну сущность. Таблицы для такого сценария должны быть физически связаны отношением [1 - 1], т.е. первичный ключ дочерней таблицы должен быть одновременно внешним ключом к основной. Entity Framework требует, чтобы соединение таблиц происходило только через первичные ключи. Следующая картинка демонстрирует сущность ComplexObject и ее отображение на две таблицы БД - ComplexObject и ComplexObjectEx:
Vertical Splitting Entity mapping to two database tables

Horizontal Entity splitting

Entity Framework позволяет горизонтально отобразить несколько таблиц на одну сущность. Таблицы для такого сценария должны обладать одинаковым набором колонок, которые будут отображены на сущность. Насколько я понимаю, дизайнер такую схему не поддерживает, да и сам Entity Framework не в ажуре с ней - сущность должна обладать булевым свойством, по которому она будет отнесена к одной из двух физических таблиц. В будущем эта функциональность может быть расширена. А зачем, если современные БД поддерживают это на уровне физической модели?

Вариант настройки вручную подробно описан в статье Entity Framework Horizontal Entity splitting.

Отображение ассоциаций [0..1 - *], [1 - 0..1], [1 - 1], [1 - *], [* - *], self association

Рассмотрим три основных типа ассоциаций между сущностями - [1 - 1], [1 - *] и [* - *]. Варианты [0..1 - ?] концептуально ничем не отличаются от своих собратьев, просто внешние ключи допускают значения NULL, а свойства сущностей, соответственно, - null. Self association - тот же [0..1 - *].

Ниже представлены ER-диаграмма и концептуальная схема, которые я буду использовать:
Associations database ER model
Associations conceptual model

Чтобы связать две сущности, необходимо выполнить следующие действия:

  1. Создать в концептуальной модели ассоциацию между сущностями.
  2. В свойствах этой ассоциации выставить кардинальность сторон.
  3. Убедиться, что никакие внешние ключи (foreign key), используемые для соединения таблиц, не отображены на атрибуты сущностей.
  4. Выбрать ассоциацию в дизайнере и перейти в окно Mapping Details.

    Association Mapping Details

  5. Нажать Add a Table or View и выбрать из списка таблицу, которая содержит внешний ключ, используемый для соединения таблиц.

    Это, как правило, будет таблица с кардинальностью выше, чем у таблицы другой стороны ассоциации.

  6. Сопоставить ключи (первичные и вторичные) с атрибутами сущностей.

    С этим внимательно - дизайнер сам сопоставляет одинаковые имена. В результате отображение сконфигурировано некорректно:
    Wrong association mapping
    А правильно вот так:
    Correct association mapping

Association One To One [1 - 1]

Association One To One

Association One To Many [1 - *]

Association One To Many

Association Many To Many [* - *]

Здесь немного интереснее. Я писал, что для отображения ассоциации нужно выбрать из списка таблицу, которая содержит внешний ключ, используемый для соединения таблиц на концах ассоциации. Но ни одна из этих таблиц не содержит такого ключа! Что же делать? Да выбрать таблицу, которая связывает одно с другим - промежуточную таблицу!
Association Many To Many

Здесь есть один нюанс. Дизайнер генерирует ассоциацию [* - *] только в случае, когда связующая таблица содержит две колонки - два внешних ключа, одновременно образующих первичный ключ этой же таблицы.

Наследование, абстрактные классы, иерархии

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

  1. Table per Hierarchy (TPH)
  2. Table per Type (TPT)
  3. Table per Concrete Type (TPC)

Отображение абстрактных классов

Дизайнер Entity Framework позволяет пометить сущность, как абстрактную. Соответственно, в коде генерируется абстрактный класс. Схемы, перечисленные выше, влияют на отображение абстрактного класса на таблицы БД (об этом будет указано в описании каждой их схем). Далее я использую термин класс вместо сущность.

В случае, когда абстрактный класс не имеет неабстрактных наследников, - библиотека времени выполнения выбросит исключение.

Table per Hierarchy (TPH)

В TPH все классы иерархии отображены на одну таблицу БД, которая должна содержать все атрибуты всех классов иерархии. Экземпляр класса сопоставляется с одной строчкой таблицы, которая содержит значение NULL в колонках, для которых нет соответствующих атрибутов в классе. Также необходимо иметь колонку-идентификатор, значения которой будут идентифицировать конечный тип иерархии. При отображении каждой сущности иерархии необходимо накладывать на эту колонку условие. Условия должны удовлетворять таким требованиям:

  1. Они должно быть взаимоисключающим.
  2. Условия должны покрывать все строки таблицы.
  3. Колонка-идентификатор не должна быть отображена на свойство сущности.
Пример концептуальной модели для схемы Table per Hierarchy (TPH)

Table per Hierarchy (TPH) conceptual model

Модель нуждается в некоторых пояснениях, поскольку кое-что визуально определить нельзя. Классы Animal, Mammal и Reptile - абстрактные.

Отображение концептуальной модели для схемы Table per Hierarchy (TPH)

Для создания таблицы я использую следующий SQL скрипт:

CREATE TABLE [TPHInheritance].[Animal]
(
-- Animal
[ID] INT PRIMARY KEY NOT NULL IDENTITY,
[ClassType] NVARCHAR(128) NOT NULL
    CHECK ([ClassType] IN (N'Horse', N'Dog', N'Snake', N'Crocodile')),
[Weight] DECIMAL(4,2) NOT NULL,
[FoodClassification] NVARCHAR(128) NOT NULL
    CHECK ([FoodClassification] IN (N'Omnivorous', N'Herbivorous', N'Carnivorous')),
    -- Mammal
    [BirthDate] DATETIME /*NOT*/ NULL,
        -- Horse
        [MaximumSpeed] DECIMAL(4,2) NULL,
        -- Dog
        [Breed] NVARCHAR(128) /*NOT*/ NULL,
    -- Reptile
    [Length] DECIMAL(5,2) /*NOT*/ NULL,
        -- Snake
        [IsAdder] BIT /*NOT*/ NULL,
        -- Crocodile
        [Family] NVARCHAR(128) NULL,
        [Genus] NVARCHAR(128) NULL
)

Связав модель с БД, я отображаю все сущности иерархии на эту таблицу, попутно добавляя условия на колонку ClassType всем неабстрактным классам:
Table per Hierarchy (TPH) mapping non-abstract entity
Абстрактные классы по логике вещей в условии не нуждаются.

Итак, все условия соблюдены, но валидация модели проходит с ошибками:

Error 2078: The EntityType 'TPHInreritanceContainer.Mammal' is Abstract and can be mapped only using IsTypeOf.
Error 2078: The EntityType 'TPHInreritanceContainer.Reptile' is Abstract and can be mapped only using IsTypeOf.

Я сбит с толку - текущей информации вполне хватает для определения типа!

Выход есть - создать по дискриминирующей колонке для каждого разветвления наследования:

CREATE TABLE [TPHInheritance].[Animal]
(
-- Animal
[ID] INT PRIMARY KEY NOT NULL IDENTITY,
[ClassType] NVARCHAR(128) NULL
    CHECK ([ClassType] IN (N'Mammal', N'Reptile')),
[MammalType] NVARCHAR(128) NULL
    CHECK ([MammalType] IN (N'Horse', N'Dog')),
[ReptileType] NVARCHAR(128) NULL
    CHECK ([ReptileType] IN (N'Snake', N'Crocodile')),
[Weight] DECIMAL(4,2) NOT NULL,
[FoodClassification] NVARCHAR(128) NOT NULL
    CHECK ([FoodClassification] IN (N'Omnivorous', N'Herbivorous', N'Carnivorous')),
    -- Mammal
    [BirthDate] DATETIME /*NOT*/ NULL,
        -- Horse
        [MaximumSpeed] DECIMAL(4,2) NULL,
        -- Dog
        [Breed] NVARCHAR(128) /*NOT*/ NULL,
    -- Reptile
    [Length] DECIMAL(5,2) /*NOT*/ NULL,
        -- Snake
        [IsAdder] BIT /*NOT*/ NULL,
        -- Crocodile
        [Family] NVARCHAR(128) NULL,
        [Genus] NVARCHAR(128) NULL
)

Условия на дискриминирующую колонку накладываются при отображении всех сущностей, кроме базовой:
Table per Hierarchy (TPH) mapping abstract entity
Table per Hierarchy (TPH) mapping non-abstract entity

Преимущества и недостатки схемы Table per Hierarchy (TPH)
Преимущества (или когда использовать TPH)
  • Полиморфные запросы выполняются быстро.
Недостатки (или когда не использовать TPH)
  • Много колонок.
  • Сильная разреженность (преимущество значений NULL). Как следствие - чрезмерное использование памяти и дискового пространства.
  • Все колонки небазовых классов должны быть объявлены, как позволяющие значение NULL. Тоесть страдает целостность данных.

Table per Type (TPT)

Здесь все просто - каждому типу сопоставляется одна таблица, а каждому неунаследованному свойству - одна колонка в этой таблице. Концептуальная модель - та же. Таблицы связаны отношением [1 - 0..1]:

CREATE TABLE [TPTInheritance].[Animal]
(
[ID] INT PRIMARY KEY NOT NULL IDENTITY,
[Weight] DECIMAL(4,2) NOT NULL,
[FoodClassification] NVARCHAR(128) NOT NULL
   CHECK ([FoodClassification] IN (N'Omnivorous', N'Herbivorous', N'Carnivorous')),
)

CREATE TABLE [TPTInheritance].[Mammal]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPTInheritance].[Animal],
[BirthDate] DATETIME NOT NULL
)

CREATE TABLE [TPTInheritance].[Horse]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPTInheritance].[Mammal],
[MaximumSpeed] DECIMAL(4,2) NULL
)

CREATE TABLE [TPTInheritance].[Dog]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPTInheritance].[Mammal],
[Breed] NVARCHAR(128) NOT NULL
)

CREATE TABLE [TPTInheritance].[Reptile]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPTInheritance].[Animal],
[Length] DECIMAL(5,2) NOT NULL
)

CREATE TABLE [TPTInheritance].[Snake]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPTInheritance].[Reptile],
[IsAdder] BIT NOT NULL
)

CREATE TABLE [TPTInheritance].[Crocodile]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPTInheritance].[Reptile],
[Family] NVARCHAR(128) NULL,
[Genus] NVARCHAR(128) NULL
)
Преимущества и недостатки схемы Table per Type (TPT)
Преимущества (или когда использовать TPT)
  • Когда недостатки TPH существенны.
  • TPT является нормализированным путем хранения данных.
Недостатки (или когда не использовать TPT)
  • Если иерархия сложная - производительность может страдать из-за большого количества соединений между таблицами.

Table per Concrete Type (TPC)

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

Здесь я буду использовать немного измененную концептуальную модель:
Table per Concrete Type (TPC) conceptual model

Для простоты эксперимента (напоминаю, что дизайнер не поддерживает TPC) лишь одна сущность будет соответствовать схеме TPC, остальные строятся по схеме TPT:

CREATE TABLE [TPCInheritance].[Animal]
(
[ID] INT PRIMARY KEY NOT NULL IDENTITY,
[Weight] DECIMAL(4,2) NOT NULL,
[FoodClassification] NVARCHAR(128) NOT NULL
   CHECK ([FoodClassification] IN (N'Omnivorous', N'Herbivorous', N'Carnivorous')),
)

CREATE TABLE [TPCInheritance].[Mammal]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPCInheritance].[Animal],
[BirthDate] DATETIME NOT NULL
)

CREATE TABLE [TPCInheritance].[Horse]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPCInheritance].[Mammal],
[MaximumSpeed] DECIMAL(4,2) NULL
)

CREATE TABLE [TPCInheritance].[Dog]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPCInheritance].[Mammal],
[Breed] NVARCHAR(128) NOT NULL
)

CREATE TABLE [TPCInheritance].[BadDog]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPCInheritance].[Dog],
[Weight] DECIMAL(4,2) NOT NULL,
[FoodClassification] NVARCHAR(128) NOT NULL
   CHECK ([FoodClassification] IN (N'Omnivorous', N'Herbivorous', N'Carnivorous')),
[BirthDate] DATETIME NOT NULL,
[Breed] NVARCHAR(128) NOT NULL,
[AngerLevel] TINYINT NOT NULL
)

CREATE TABLE [TPCInheritance].[Reptile]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPCInheritance].[Animal],
[Length] DECIMAL(5,2) NOT NULL
)

CREATE TABLE [TPCInheritance].[Snake]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPCInheritance].[Reptile],
[IsAdder] BIT NOT NULL
)

CREATE TABLE [TPCInheritance].[Crocodile]
(
[ID] INT PRIMARY KEY NOT NULL FOREIGN KEY REFERENCES [TPCInheritance].[Reptile],
[Family] NVARCHAR(128) NULL,
[Genus] NVARCHAR(128) NULL
)

Для начала выполняем шаги, указанные в разделе Table per Type (TPT). Далее необходимо открыть edmx-файл в XML-редакторе и внести добавки:

<!-- C-S mapping content -->
<edmx:Mappings>
    <Mapping xmlns="urn:schemas-microsoft-com:windows:storage:mapping:CS" Space="C-S">
        <Alias Key="Model" Value="TPCInheritance" />
        <Alias Key="Target" Value="TPCInheritanceContainer.Store" />
        <EntityContainerMapping CdmEntityContainer="TPCInheritanceEntities" StorageEntityContainer="TPCInheritanceContainerStoreContainer">
            <EntitySetMapping Name="Animal">
                <EntityTypeMapping TypeName="IsTypeOf(TPCInheritanceContainer.Animal)">
                    <MappingFragment StoreEntitySet="Animal">
                        <ScalarProperty Name="FoodClassification" ColumnName="FoodClassification" />
                        <ScalarProperty Name="Weight" ColumnName="Weight" />
                        <ScalarProperty Name="ID" ColumnName="ID" />
                    </MappingFragment>
                </EntityTypeMapping>
                <EntityTypeMapping TypeName="IsTypeOf(TPCInheritanceContainer.Mammal)">
                    <MappingFragment StoreEntitySet="Mammal">
                        <ScalarProperty Name="ID" ColumnName="ID" />
                        <ScalarProperty Name="BirthDate" ColumnName="BirthDate" />
                    </MappingFragment>
                </EntityTypeMapping>
                <EntityTypeMapping TypeName="IsTypeOf(TPCInheritanceContainer.Horse)">
                    <MappingFragment StoreEntitySet="Horse">
                        <ScalarProperty Name="ID" ColumnName="ID" />
                        <ScalarProperty Name="MaximumSpeed" ColumnName="MaximumSpeed" />
                    </MappingFragment>
                </EntityTypeMapping>
                <EntityTypeMapping TypeName="IsTypeOf(TPCInheritanceContainer.Dog)">
                    <MappingFragment StoreEntitySet="Dog">
                        <ScalarProperty Name="ID" ColumnName="ID" />
                        <ScalarProperty Name="Breed" ColumnName="Breed" />
                    </MappingFragment>
                </EntityTypeMapping>
                <EntityTypeMapping TypeName="IsTypeOf(TPCInheritanceContainer.BadDog)">
                    <MappingFragment StoreEntitySet="BadDog">
                        <ScalarProperty Name="ID" ColumnName="ID" />
                        <ScalarProperty Name="AngerLevel" ColumnName="AngerLevel" />
                        <!-- MAPPINGS BELOW ARE ADDED MANUALLY -->
                        <ScalarProperty Name="Breed" ColumnName="Breed" />
                        <ScalarProperty Name="BirthDate" ColumnName="BirthDate" />
                        <ScalarProperty Name="FoodClassification" ColumnName="FoodClassification" />
                        <ScalarProperty Name="Weight" ColumnName="Weight" />
                    </MappingFragment>
                </EntityTypeMapping>
            </EntitySetMapping>
        </EntityContainerMapping>
    </Mapping>
</edmx:Mappings>

Думаю, смысл изменений понятен.

Преимущества и недостатки схемы Table per Concrete Type (TPC)
Преимущества (или когда использовать TPC)
  • Схема TPC отлично работает для неполиморфных запросов и ассоциаций.
Недостатки (или когда не использовать TPC)
  • Простой полиморфный запрос для корневой сущности вовлечет множество запросов и соединений. Такой запрос будет ограничивать производительность очень сильно.
  • Если корневая сущность ссылается на что-либо, то внешний ключ должен быть продублирован на все таблицы иерархии.
  • Любое изменение в корневой сущности сказывается на все таблицы иерархии.

Кодогенератор Entity Framework

Как я уже писал, Entity Framework состоит из 3-х частей: дизайнер, кодогенератор и библиотеки поддержки. Результатом работы дизайнера является edmx-файл. При сборке проекта он расщепляется на 3 файла - csdl, msl и ssdl, которые в свою очередь зашиваются в сборку в виде ресурсов. Кроме того, csdl-файл используется для генерации кода, необходимого для работы с Object Services и LINQ to Entities (о них - далее).

Дизайнером Entity Framework пользоваться не обязательно. Никто не запрещает написать файлы csdl, msl и ssdl вручную, затем воспользовавшись утилитой EDM Generator (EdmGen.exe) получить код. Естественно, с таким подходом build процедуру придется писать самому. С другой стороны, открываются все возможности Entity Framework, львиная часть которых не поддерживается версией 1.0 дизайнера.

У многих может возникнуть желание просто отредактировать edmx-файл вручную, полагая, что msbuild сделает все остальное сам. Это не так. Дело в том, что при сборке файл проверяется валидатором дизайнера и если в нем будет что-либо не соответствующее требованиям последнего - ошибка. Поэтому в текущей версии для использования Entity Framework без дизайнера придется немного пошевелить мозгами.

Если результат работы кодогенератора не удовлетворяет требованиям - не беда, можно написать свой! Не нужно пугаться, дело в том, что Entity Framework предоставляет разработчикам классы для работы с метаданными - пространство имен System.Data.Metadata.Edm сборки System.Data.Entity.dll. Этому вопросу я, пожалуй, посвящу отдельную статью.

Описывать результат генерации кода я не стану, замечу лишь, что

  1. все классы - парциальные (partial classes);
  2. присутствуют парциальные методы (partial methods), которые позволяют внедрять свой код в сгенерированный;
  3. код аннотирован атрибутами, в т.ч. отвечающими за сериализацию - SerializableAttribute, DataContractAttribute, DataMemberAttribute.

Библиотеки поддержки Entity Framework

Сгенерированный код сам по себе не содержит никакого функционала по взаимодействию с реляционной базой данных. Фактически, это та же концептуальная модель, просто написанная на языке программирования. На данный момент эта модель зависит от runtime-библиотеки Entity Framework (все сгенерированные классы имеют предка в этой библиотеке), но в будущем, во 2-й версии, нам обещают поддержку POCO (Plain Old C# Objects).

Библиотека поддержки Entity Framework состоит из единственной сборки - System.Data.Entity.dll. Архитектуру Entity Framework можно представить в виде следующей диаграммы:
Entity Framework runtime
Скажу сразу, что часть Data Providers скрыта от программиста и не представляет никакой ценности в контексте использования Entity Framework. Фактически, Data Providers - это связующий элемент между Entity Client и базой данных.

Entity Client

Entity Framework породила нового поставщика данных ADO.NET - Entity Client. Он выглядит, как любой другой провайдер ADO.NET, с которыми разработчики сталкивались на протяжении всего существования .NET, но на самом деле стоит на ступеньку выше с точки зрения абстракции.

Entity Client позволяет выполнять запросы в контексте EDM с помощью стандартных объектов - соединение (EntityConnection), команда (EntityCommand) и DataReader (EntityDataReader). Почему в контексте EDM? Да потому, что EntityConnection привязан к конкретной EDM и выполняет запросы на языке Entity SQL (eSQL). Entity SQL позволяет эффективно и удобно запрашивать данные, описанные в EDM.

Когда Entity Client выполняет eSQL-запрос, производится его разбор и по метаданным EDM строится Canonical Command Tree (CCT) - структура данных, представляющая исходный запрос в объектной форме. Далее Client View Engine выполняет над CCT преобразование отображения концептуального уровня на уровень хранения, устраняя все нереляционные концепции. Полученный CCT передается для исполнения нижестоящему поставщику данных ADO.NET, который преобразует его в специфический для конкретной базы данных запрос. Насколько я понимаю, в версии 3.5 SP1 .NET Framework поставщих данных System.Data.SqlClient был расширен и поддерживает CCT.

При чтении данных Entity Client выполняет их преобразование, снова пользуясь метаданными EDM. Весь процесс лучше объяснить на примере:

const String csdl = "res://*/Associations.Associations.csdl";
const String ssdl = "res://*/Associations.Associations.ssdl";
const String msl = "res://*/Associations.Associations.msl";
const String providerConnectionString =
    "Data Source=veastduo;Initial Catalog=Test;Integrated Security=True;MultipleActiveResultSets=True";
var entityConnectionStringBuilder = new EntityConnectionStringBuilder
                                        {
                                            Metadata = csdl + "|" + ssdl + "|" + msl,
                                            Provider = "System.Data.SqlClient",
                                            ProviderConnectionString = providerConnectionString
                                        };
using (var entityConnection = new EntityConnection(entityConnectionStringBuilder.ToString())) {
    entityConnection.Open();
    var entityCommand = entityConnection.CreateCommand();
    const String eSQL =
        "SELECT c, (SELECT DEREF(cs) FROM NAVIGATE(c, AssociationsModel.CourseCourseSchedule) AS cs)" +
        " FROM [AssociationsEntities].[Course] AS c";
    entityCommand.CommandText = eSQL;
    var entityDataReader = entityCommand.ExecuteReader(CommandBehavior.SequentialAccess);
    while (entityDataReader.Read()) {
        var course = entityDataReader.GetDataRecord(0);
        Trace.WriteLine(String.Format("Course: ID = {0}, Name = {1}", course["ID"], course["Name"]));
        var courseSchedules = entityDataReader.GetDataReader(1);
        while (courseSchedules.Read()) {
            var schedule = ((IExtendedDataRecord) courseSchedules).GetDataRecord(0);
            Trace.WriteLine(String.Format("\tSchedule: ID = {0}, BeginTime = {1}, EndTime = {2}, DateProvided = {3}",
                                          schedule["ID"], schedule["DateProvided"], schedule["BeginTime"], schedule["EndTime"]));
        }
    }
}
  1. Подключение к поставщику данных.

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

    1. расположение EDM метаданных (файлов csdl, ssdl и msl);
    2. имя поставщика данных ADO.NET, специфического для используемой базы данных;
    3. строку подключения, специфическую для этого провайдера.
  2. Создание EntityCommand.

    Язык запросов eSQL позволяет мне выбрать в первой колонке сущность Course, а во второй колонке - список сущностей CourseSchedule, которые ассоциированы с курсом. Обратите внимание, как для выборки CourseSchedule используется оператор NAVIGATE вместе с именем ассоциации [1 - *] между Course и CourseSchedule.

  3. Выполнение запроса.

    При выполнении моего eSQL-запроса, Entity Client формирует Canonical Command Tree, а нижестоящий поставщих данных ADO.NET преобразует его в такой SQL:

    SELECT
    [Project2].[ID] AS [ID],
    [Project2].[Name] AS [Name],
    [Project2].[C1] AS [C1],
    [Project2].[C2] AS [C2],
    [Project2].[ID1] AS [ID1],
    [Project2].[DateProvided] AS [DateProvided],
    [Project2].[BeginTime] AS [BeginTime],
    [Project2].[EndTime] AS [EndTime]
    FROM ( SELECT
       [Extent1].[ID] AS [ID],
       [Extent1].[Name] AS [Name],
       1 AS [C1],
       [Project1].[ID] AS [ID1],
       [Project1].[DateProvided] AS [DateProvided],
       [Project1].[BeginTime] AS [BeginTime],
       [Project1].[EndTime] AS [EndTime],
       [Project1].[C1] AS [C2]
       FROM  [Associations].[Course] AS [Extent1]
       LEFT OUTER JOIN  (SELECT
          [Extent2].[ID] AS [ID],
          [Extent2].[CourseID] AS [CourseID],
          [Extent2].[DateProvided] AS [DateProvided],
          [Extent2].[BeginTime] AS [BeginTime],
          [Extent2].[EndTime] AS [EndTime],
          1 AS [C1]
          FROM [Associations].[CourseSchedule] AS [Extent2] ) AS [Project1] ON [Extent1].[ID] = [Project1].[CourseID]
    )  AS [Project2]
    ORDER BY [Project2].[ID] ASC, [Project2].[C2] ASC
    

    Ни одна из существующих реляционных баз данных не позволяет выполнять запросы вида SELECT A, (SELECT B, C FROM DetailTable WHERE DetailTable.MasterTableID = MasterTable.ID) FROM MasterTable, поэтому Entity Client вынужден создавать монстрообразные и не интуитивные SQL-запросы, которые позволяют получить всю нужную информацию в удобной форме для обратного преобразования, при выполнения которого используются дополнительные информационные колонки [C1] и [C2].

    PS: Oracle позволяет преобразовать вложенную таблицу в курсор и поместить его в ячейку выборки: SELECT A, CURSOR(SELECT B, C FROM DetailTable WHERE DetailTable.MasterTableID = MasterTable.ID) FROM MasterTable. Но этот трюк нельзя использовать бесплатно - количество одновременно открытых курсоров ограничено.

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

  4. Чтение результата.

    Здесь все несколько сложнее, чем с обычными DataReader-ами. Дело в том, что в колонках могут храниться не только простые типы данных, но и другие объекты - структуры, списки и пр. В нашем случае в первой колонке находится IExtendedDataRecord (представляющий данные сущности Course), а во второй - IDataReader (вложенный DataReader), в первой колонке которого хранится IExtendedDataRecord (представляющий данные сущности CourseSchedule).

    Дополнительную информацию можно найти в статье How to Parse an EntityDataReader.

Entity SQL - достаточно сложный абстрактный язык. В то же время он не поддерживает SQL-аналоги операций INSERT, UPDATE, DELETE. Также невозможно использовать специфические для конкретной базы данных конструкции.

Entity Client - это низкоуровневое средство выполнения eSQL-запросов, которое позволяет читать данные в обобщенном виде. Entity Client не выполняет материализацию результата (преобразование его в объектную форму концептуальной модели). Этим занимается слой Object Services.

Object Services

Над поставщикой данных Entity Client существует следующий слой абстракций Entity Framework именуемый Object Services. Object Services позволяет работать не с обобщенными записями, которые возвращает Entity Client, а с объектами. Этот метод отходит от прямого взаимодействия с поставщиком Entity Client (хотя с этим поставщиком происходит скрытый обмен данными).

Основная задача Object Services - материализация результатов выполнения eSQL-запросов. Однако есть еще одна важная деталь - объект ObjectContext, который представляет собой сессию взаимодействия приложения и хранилища данных.

Entity SQL не поддерживает операторы ЯМД (DML), поэтому ObjectContext становится особенно важен - именно он выполняет операции по сохранению объектов.

Рассмотрим пример:

using (var associationsEntities = new ObjectContext("name=AssociationsEntities")) {
    associationsEntities.DefaultContainerName = "AssociationsEntities";

    var courses = associationsEntities.CreateQuery<Course>("[Course]");
    foreach (var course in courses) {
        Trace.WriteLine(String.Format("Course: ID = {0}, Name = {1}", course.ID, course.Name));
        // Capitalize
        course.Name = CapitalizedName(course);
    }

    const String newCourseName = "programming";
    var beginTime = TimeSpan.FromHours(8.0d);
    var duration = TimeSpan.FromHours(1.5d);
    var newCourse = new Course {Name = newCourseName, CourseDescription = new CourseDescription {Data = newCourseName}};
    newCourse.CourseSchedule.Add(new CourseSchedule
                                     {BeginTime = beginTime, EndTime = (beginTime + duration), DateProvided = DateTime.Now});
    associationsEntities.AddObject("Course", newCourse);

    associationsEntities.SaveChanges();

    var programmingCourse =
        associationsEntities.CreateQuery<Course>("SELECT VALUE c FROM [Course] AS c WHERE c.[Name] = @courseName",
                                                 new ObjectParameter("courseName", "programming"));
    foreach (var course in programmingCourse) {
        Trace.WriteLine(String.Format("Course: ID = {0}, Name = {1}", course.ID, course.Name));
        // Capitalize
        course.Name = CapitalizedName(course);
    }

    associationsEntities.SaveChanges();
}
  1. Создание ObjectContext.

    В конструктор ObjectContext я передаю строку подключения, которая в текущем варианте просто указывает, что ее нужно взять из файла app.config:

    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
        <connectionStrings>
            <add name="AssociationsEntities" connectionString="metadata=res://*/Associations.Associations.csdl|res://*/Associations.Associations.ssdl|res://*/Associations.Associations.msl;provider=System.Data.SqlClient;provider connection string=&quot;Data Source=veastduo;Initial Catalog=Test;Integrated Security=True;MultipleActiveResultSets=True&quot;" providerName="System.Data.EntityClient" />
        </connectionStrings>
    </configuration>
    
  2. Для удобства я устанавливаю свойство DefaultContainerName чтобы не указывать имя контейнера в именах сущностей или других объектов концептуальной модели.
  3. Далее с помощью метода CreateQuery<Course> я создаю объект типа ObjectQuery<Course>.

    В качестве eSQL-запроса я передаю просто [Course], а ObjectContext сам расширяет его до вида SELECT VALUE c FROM [Course] AS c. Запрос будет возвращать сущности Course, а ObjectQuery<Course> будет их материализировать.

  4. Далее в цикле происходит изменение полученных объектов.
  5. Созданный объект Course добавляется в контекст вызовом метода AddObject.
  6. Сохранение контекста осуществляется вызовом меода SaveChanges.

    При этом все изменения сохраняются в хранилище данных.

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

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

Кодогенератор создает не только классы, отвечающие сущностям концептуальной модели, но и наследника ObjectContext, который упрощает взаимодействие с моделью и LINQ to Entities:

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:2.0.50727.3053
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

[assembly: global::System.Data.Objects.DataClasses.EdmSchemaAttribute()]
[assembly: global::System.Data.Objects.DataClasses.EdmRelationshipAttribute("AssociationsModel", "StudentCourse", "Course", global::System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(Associations.Course), "Student", global::System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(Associations.Student))]
[assembly: global::System.Data.Objects.DataClasses.EdmRelationshipAttribute("AssociationsModel", "CourseCourseSchedule", "Course", global::System.Data.Metadata.Edm.RelationshipMultiplicity.One, typeof(Associations.Course), "CourseSchedule", global::System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(Associations.CourseSchedule))]
[assembly: global::System.Data.Objects.DataClasses.EdmRelationshipAttribute("AssociationsModel", "CourseDescriptionCourse", "CourseDescription", global::System.Data.Metadata.Edm.RelationshipMultiplicity.One, typeof(Associations.CourseDescription), "Course", global::System.Data.Metadata.Edm.RelationshipMultiplicity.One, typeof(Associations.Course))]

// Original file name:
// Generation date: 28.08.2008 22:26:49
namespace Associations
{

    /// <summary>
    /// There are no comments for AssociationsEntities in the schema.
    /// </summary>
    public partial class AssociationsEntities : global::System.Data.Objects.ObjectContext
    {
        /// <summary>
        /// Initializes a new AssociationsEntities object using the connection string found in the 'AssociationsEntities' section of the application configuration file.
        /// </summary>
        public AssociationsEntities() :
                base("name=AssociationsEntities", "AssociationsEntities")
        {
            this.OnContextCreated();
        }
        /// <summary>
        /// Initialize a new AssociationsEntities object.
        /// </summary>
        public AssociationsEntities(string connectionString) :
                base(connectionString, "AssociationsEntities")
        {
            this.OnContextCreated();
        }
        /// <summary>
        /// Initialize a new AssociationsEntities object.
        /// </summary>
        public AssociationsEntities(global::System.Data.EntityClient.EntityConnection connection) :
                base(connection, "AssociationsEntities")
        {
            this.OnContextCreated();
        }
        partial void OnContextCreated();
        /// <summary>
        /// There are no comments for Course in the schema.
        /// </summary>
        public global::System.Data.Objects.ObjectQuery<Course> Course
        {
            get
            {
                if ((this._Course == null))
                {
                    this._Course = base.CreateQuery<Course>("[Course]");
                }
                return this._Course;
            }
        }
        private global::System.Data.Objects.ObjectQuery<Course> _Course;
        /// <summary>
        /// There are no comments for Student in the schema.
        /// </summary>
        public global::System.Data.Objects.ObjectQuery<Student> Student
        {
            get
            {
                if ((this._Student == null))
                {
                    this._Student = base.CreateQuery<Student>("[Student]");
                }
                return this._Student;
            }
        }
        private global::System.Data.Objects.ObjectQuery<Student> _Student;
        /// <summary>
        /// There are no comments for CourseDescription in the schema.
        /// </summary>
        public global::System.Data.Objects.ObjectQuery<CourseDescription> CourseDescription
        {
            get
            {
                if ((this._CourseDescription == null))
                {
                    this._CourseDescription = base.CreateQuery<CourseDescription>("[CourseDescription]");
                }
                return this._CourseDescription;
            }
        }
        private global::System.Data.Objects.ObjectQuery<CourseDescription> _CourseDescription;
        /// <summary>
        /// There are no comments for CourseSchedule in the schema.
        /// </summary>
        public global::System.Data.Objects.ObjectQuery<CourseSchedule> CourseSchedule
        {
            get
            {
                if ((this._CourseSchedule == null))
                {
                    this._CourseSchedule = base.CreateQuery<CourseSchedule>("[CourseSchedule]");
                }
                return this._CourseSchedule;
            }
        }
        private global::System.Data.Objects.ObjectQuery<CourseSchedule> _CourseSchedule;
        /// <summary>
        /// There are no comments for Course in the schema.
        /// </summary>
        public void AddToCourse(Course course)
        {
            base.AddObject("Course", course);
        }
        /// <summary>
        /// There are no comments for Student in the schema.
        /// </summary>
        public void AddToStudent(Student student)
        {
            base.AddObject("Student", student);
        }
        /// <summary>
        /// There are no comments for CourseDescription in the schema.
        /// </summary>
        public void AddToCourseDescription(CourseDescription courseDescription)
        {
            base.AddObject("CourseDescription", courseDescription);
        }
        /// <summary>
        /// There are no comments for CourseSchedule in the schema.
        /// </summary>
        public void AddToCourseSchedule(CourseSchedule courseSchedule)
        {
            base.AddObject("CourseSchedule", courseSchedule);
        }
    }
    /// <summary>
    /// There are no comments for AssociationsModel.Course in the schema.
    /// </summary>
    /// <KeyProperties>
    /// ID
    /// </KeyProperties>
    [global::System.Data.Objects.DataClasses.EdmEntityTypeAttribute(NamespaceName="AssociationsModel", Name="Course")]
    [global::System.Runtime.Serialization.DataContractAttribute(IsReference=true)]
    [global::System.Serializable()]
    public partial class Course : global::System.Data.Objects.DataClasses.EntityObject
    {
        /// <summary>
        /// Create a new Course object.
        /// </summary>
        /// <param name="id">Initial value of ID.</param>
        /// <param name="name">Initial value of Name.</param>
        public static Course CreateCourse(int id, string name)
        {
            Course course = new Course();
            course.ID = id;
            course.Name = name;
            return course;
        }
        /// <summary>
        /// There are no comments for Property ID in the schema.
        /// </summary>
        [global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
        [global::System.Runtime.Serialization.DataMemberAttribute()]
        public int ID
        {
            get
            {
                return this._ID;
            }
            set
            {
                this.OnIDChanging(value);
                this.ReportPropertyChanging("ID");
                this._ID = global::System.Data.Objects.DataClasses.StructuralObject.SetValidValue(value);
                this.ReportPropertyChanged("ID");
                this.OnIDChanged();
            }
        }
        private int _ID;
        partial void OnIDChanging(int value);
        partial void OnIDChanged();
        /// <summary>
        /// There are no comments for Property Name in the schema.
        /// </summary>
        [global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(IsNullable=false)]
        [global::System.Runtime.Serialization.DataMemberAttribute()]
        public string Name
        {
            get
            {
                return this._Name;
            }
            set
            {
                this.OnNameChanging(value);
                this.ReportPropertyChanging("Name");
                this._Name = global::System.Data.Objects.DataClasses.StructuralObject.SetValidValue(value, false);
                this.ReportPropertyChanged("Name");
                this.OnNameChanged();
            }
        }
        private string _Name;
        partial void OnNameChanging(string value);
        partial void OnNameChanged();
        /// <summary>
        /// There are no comments for Students in the schema.
        /// </summary>
        [global::System.Data.Objects.DataClasses.EdmRelationshipNavigationPropertyAttribute("AssociationsModel", "StudentCourse", "Student")]
        [global::System.Xml.Serialization.XmlIgnoreAttribute()]
        [global::System.Xml.Serialization.SoapIgnoreAttribute()]
        [global::System.Runtime.Serialization.DataMemberAttribute()]
        public global::System.Data.Objects.DataClasses.EntityCollection<Student> Students
        {
            get
            {
                return ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.GetRelatedCollection<Student>("AssociationsModel.StudentCourse", "Student");
            }
            set
            {
                if ((value != null))
                {
                    ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.InitializeRelatedCollection<Student>("AssociationsModel.StudentCourse", "Student", value);
                }
            }
        }
        /// <summary>
        /// There are no comments for CourseSchedule in the schema.
        /// </summary>
        [global::System.Data.Objects.DataClasses.EdmRelationshipNavigationPropertyAttribute("AssociationsModel", "CourseCourseSchedule", "CourseSchedule")]
        [global::System.Xml.Serialization.XmlIgnoreAttribute()]
        [global::System.Xml.Serialization.SoapIgnoreAttribute()]
        [global::System.Runtime.Serialization.DataMemberAttribute()]
        public global::System.Data.Objects.DataClasses.EntityCollection<CourseSchedule> CourseSchedule
        {
            get
            {
                return ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.GetRelatedCollection<CourseSchedule>("AssociationsModel.CourseCourseSchedule", "CourseSchedule");
            }
            set
            {
                if ((value != null))
                {
                    ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.InitializeRelatedCollection<CourseSchedule>("AssociationsModel.CourseCourseSchedule", "CourseSchedule", value);
                }
            }
        }
        /// <summary>
        /// There are no comments for CourseDescription in the schema.
        /// </summary>
        [global::System.Data.Objects.DataClasses.EdmRelationshipNavigationPropertyAttribute("AssociationsModel", "CourseDescriptionCourse", "CourseDescription")]
        [global::System.Xml.Serialization.XmlIgnoreAttribute()]
        [global::System.Xml.Serialization.SoapIgnoreAttribute()]
        [global::System.Runtime.Serialization.DataMemberAttribute()]
        public CourseDescription CourseDescription
        {
            get
            {
                return ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.GetRelatedReference<CourseDescription>("AssociationsModel.CourseDescriptionCourse", "CourseDescription").Value;
            }
            set
            {
                ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.GetRelatedReference<CourseDescription>("AssociationsModel.CourseDescriptionCourse", "CourseDescription").Value = value;
            }
        }
        /// <summary>
        /// There are no comments for CourseDescription in the schema.
        /// </summary>
        [global::System.ComponentModel.BrowsableAttribute(false)]
        [global::System.Runtime.Serialization.DataMemberAttribute()]
        public global::System.Data.Objects.DataClasses.EntityReference<CourseDescription> CourseDescriptionReference
        {
            get
            {
                return ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.GetRelatedReference<CourseDescription>("AssociationsModel.CourseDescriptionCourse", "CourseDescription");
            }
            set
            {
                if ((value != null))
                {
                    ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.InitializeRelatedReference<CourseDescription>("AssociationsModel.CourseDescriptionCourse", "CourseDescription", value);
                }
            }
        }
    }
    /// <summary>
    /// There are no comments for AssociationsModel.Student in the schema.
    /// </summary>
    /// <KeyProperties>
    /// ID
    /// </KeyProperties>
    [global::System.Data.Objects.DataClasses.EdmEntityTypeAttribute(NamespaceName="AssociationsModel", Name="Student")]
    [global::System.Runtime.Serialization.DataContractAttribute(IsReference=true)]
    [global::System.Serializable()]
    public partial class Student : global::System.Data.Objects.DataClasses.EntityObject
    {
        /// <summary>
        /// Create a new Student object.
        /// </summary>
        /// <param name="id">Initial value of ID.</param>
        /// <param name="name">Initial value of Name.</param>
        public static Student CreateStudent(int id, string name)
        {
            Student student = new Student();
            student.ID = id;
            student.Name = name;
            return student;
        }
        /// <summary>
        /// There are no comments for Property ID in the schema.
        /// </summary>
        [global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
        [global::System.Runtime.Serialization.DataMemberAttribute()]
        public int ID
        {
            get
            {
                return this._ID;
            }
            set
            {
                this.OnIDChanging(value);
                this.ReportPropertyChanging("ID");
                this._ID = global::System.Data.Objects.DataClasses.StructuralObject.SetValidValue(value);
                this.ReportPropertyChanged("ID");
                this.OnIDChanged();
            }
        }
        private int _ID;
        partial void OnIDChanging(int value);
        partial void OnIDChanged();
        /// <summary>
        /// There are no comments for Property Name in the schema.
        /// </summary>
        [global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(IsNullable=false)]
        [global::System.Runtime.Serialization.DataMemberAttribute()]
        public string Name
        {
            get
            {
                return this._Name;
            }
            set
            {
                this.OnNameChanging(value);
                this.ReportPropertyChanging("Name");
                this._Name = global::System.Data.Objects.DataClasses.StructuralObject.SetValidValue(value, false);
                this.ReportPropertyChanged("Name");
                this.OnNameChanged();
            }
        }
        private string _Name;
        partial void OnNameChanging(string value);
        partial void OnNameChanged();
        /// <summary>
        /// There are no comments for Course in the schema.
        /// </summary>
        [global::System.Data.Objects.DataClasses.EdmRelationshipNavigationPropertyAttribute("AssociationsModel", "StudentCourse", "Course")]
        [global::System.Xml.Serialization.XmlIgnoreAttribute()]
        [global::System.Xml.Serialization.SoapIgnoreAttribute()]
        [global::System.Runtime.Serialization.DataMemberAttribute()]
        public global::System.Data.Objects.DataClasses.EntityCollection<Course> Course
        {
            get
            {
                return ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.GetRelatedCollection<Course>("AssociationsModel.StudentCourse", "Course");
            }
            set
            {
                if ((value != null))
                {
                    ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.InitializeRelatedCollection<Course>("AssociationsModel.StudentCourse", "Course", value);
                }
            }
        }
    }
    /// <summary>
    /// There are no comments for AssociationsModel.CourseDescription in the schema.
    /// </summary>
    /// <KeyProperties>
    /// ID
    /// </KeyProperties>
    [global::System.Data.Objects.DataClasses.EdmEntityTypeAttribute(NamespaceName="AssociationsModel", Name="CourseDescription")]
    [global::System.Runtime.Serialization.DataContractAttribute(IsReference=true)]
    [global::System.Serializable()]
    public partial class CourseDescription : global::System.Data.Objects.DataClasses.EntityObject
    {
        /// <summary>
        /// Create a new CourseDescription object.
        /// </summary>
        /// <param name="id">Initial value of ID.</param>
        /// <param name="data">Initial value of Data.</param>
        public static CourseDescription CreateCourseDescription(int id, string data)
        {
            CourseDescription courseDescription = new CourseDescription();
            courseDescription.ID = id;
            courseDescription.Data = data;
            return courseDescription;
        }
        /// <summary>
        /// There are no comments for Property ID in the schema.
        /// </summary>
        [global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
        [global::System.Runtime.Serialization.DataMemberAttribute()]
        public int ID
        {
            get
            {
                return this._ID;
            }
            set
            {
                this.OnIDChanging(value);
                this.ReportPropertyChanging("ID");
                this._ID = global::System.Data.Objects.DataClasses.StructuralObject.SetValidValue(value);
                this.ReportPropertyChanged("ID");
                this.OnIDChanged();
            }
        }
        private int _ID;
        partial void OnIDChanging(int value);
        partial void OnIDChanged();
        /// <summary>
        /// There are no comments for Property Data in the schema.
        /// </summary>
        [global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(IsNullable=false)]
        [global::System.Runtime.Serialization.DataMemberAttribute()]
        public string Data
        {
            get
            {
                return this._Data;
            }
            set
            {
                this.OnDataChanging(value);
                this.ReportPropertyChanging("Data");
                this._Data = global::System.Data.Objects.DataClasses.StructuralObject.SetValidValue(value, false);
                this.ReportPropertyChanged("Data");
                this.OnDataChanged();
            }
        }
        private string _Data;
        partial void OnDataChanging(string value);
        partial void OnDataChanged();
        /// <summary>
        /// There are no comments for Course in the schema.
        /// </summary>
        [global::System.Data.Objects.DataClasses.EdmRelationshipNavigationPropertyAttribute("AssociationsModel", "CourseDescriptionCourse", "Course")]
        [global::System.Xml.Serialization.XmlIgnoreAttribute()]
        [global::System.Xml.Serialization.SoapIgnoreAttribute()]
        [global::System.Runtime.Serialization.DataMemberAttribute()]
        public Course Course
        {
            get
            {
                return ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.GetRelatedReference<Course>("AssociationsModel.CourseDescriptionCourse", "Course").Value;
            }
            set
            {
                ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.GetRelatedReference<Course>("AssociationsModel.CourseDescriptionCourse", "Course").Value = value;
            }
        }
        /// <summary>
        /// There are no comments for Course in the schema.
        /// </summary>
        [global::System.ComponentModel.BrowsableAttribute(false)]
        [global::System.Runtime.Serialization.DataMemberAttribute()]
        public global::System.Data.Objects.DataClasses.EntityReference<Course> CourseReference
        {
            get
            {
                return ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.GetRelatedReference<Course>("AssociationsModel.CourseDescriptionCourse", "Course");
            }
            set
            {
                if ((value != null))
                {
                    ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.InitializeRelatedReference<Course>("AssociationsModel.CourseDescriptionCourse", "Course", value);
                }
            }
        }
    }
    /// <summary>
    /// There are no comments for AssociationsModel.CourseSchedule in the schema.
    /// </summary>
    /// <KeyProperties>
    /// ID
    /// </KeyProperties>
    [global::System.Data.Objects.DataClasses.EdmEntityTypeAttribute(NamespaceName="AssociationsModel", Name="CourseSchedule")]
    [global::System.Runtime.Serialization.DataContractAttribute(IsReference=true)]
    [global::System.Serializable()]
    public partial class CourseSchedule : global::System.Data.Objects.DataClasses.EntityObject
    {
        /// <summary>
        /// Create a new CourseSchedule object.
        /// </summary>
        /// <param name="id">Initial value of ID.</param>
        /// <param name="dateProvided">Initial value of DateProvided.</param>
        /// <param name="beginTime">Initial value of BeginTime.</param>
        /// <param name="endTime">Initial value of EndTime.</param>
        public static CourseSchedule CreateCourseSchedule(int id, global::System.DateTime dateProvided, global::System.TimeSpan beginTime, global::System.TimeSpan endTime)
        {
            CourseSchedule courseSchedule = new CourseSchedule();
            courseSchedule.ID = id;
            courseSchedule.DateProvided = dateProvided;
            courseSchedule.BeginTime = beginTime;
            courseSchedule.EndTime = endTime;
            return courseSchedule;
        }
        /// <summary>
        /// There are no comments for Property ID in the schema.
        /// </summary>
        [global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
        [global::System.Runtime.Serialization.DataMemberAttribute()]
        public int ID
        {
            get
            {
                return this._ID;
            }
            set
            {
                this.OnIDChanging(value);
                this.ReportPropertyChanging("ID");
                this._ID = global::System.Data.Objects.DataClasses.StructuralObject.SetValidValue(value);
                this.ReportPropertyChanged("ID");
                this.OnIDChanged();
            }
        }
        private int _ID;
        partial void OnIDChanging(int value);
        partial void OnIDChanged();
        /// <summary>
        /// There are no comments for Property DateProvided in the schema.
        /// </summary>
        [global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(IsNullable=false)]
        [global::System.Runtime.Serialization.DataMemberAttribute()]
        public global::System.DateTime DateProvided
        {
            get
            {
                return this._DateProvided;
            }
            set
            {
                this.OnDateProvidedChanging(value);
                this.ReportPropertyChanging("DateProvided");
                this._DateProvided = global::System.Data.Objects.DataClasses.StructuralObject.SetValidValue(value);
                this.ReportPropertyChanged("DateProvided");
                this.OnDateProvidedChanged();
            }
        }
        private global::System.DateTime _DateProvided;
        partial void OnDateProvidedChanging(global::System.DateTime value);
        partial void OnDateProvidedChanged();
        /// <summary>
        /// There are no comments for Property BeginTime in the schema.
        /// </summary>
        [global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(IsNullable=false)]
        [global::System.Runtime.Serialization.DataMemberAttribute()]
        public global::System.TimeSpan BeginTime
        {
            get
            {
                return this._BeginTime;
            }
            set
            {
                this.OnBeginTimeChanging(value);
                this.ReportPropertyChanging("BeginTime");
                this._BeginTime = global::System.Data.Objects.DataClasses.StructuralObject.SetValidValue(value);
                this.ReportPropertyChanged("BeginTime");
                this.OnBeginTimeChanged();
            }
        }
        private global::System.TimeSpan _BeginTime;
        partial void OnBeginTimeChanging(global::System.TimeSpan value);
        partial void OnBeginTimeChanged();
        /// <summary>
        /// There are no comments for Property EndTime in the schema.
        /// </summary>
        [global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(IsNullable=false)]
        [global::System.Runtime.Serialization.DataMemberAttribute()]
        public global::System.TimeSpan EndTime
        {
            get
            {
                return this._EndTime;
            }
            set
            {
                this.OnEndTimeChanging(value);
                this.ReportPropertyChanging("EndTime");
                this._EndTime = global::System.Data.Objects.DataClasses.StructuralObject.SetValidValue(value);
                this.ReportPropertyChanged("EndTime");
                this.OnEndTimeChanged();
            }
        }
        private global::System.TimeSpan _EndTime;
        partial void OnEndTimeChanging(global::System.TimeSpan value);
        partial void OnEndTimeChanged();
        /// <summary>
        /// There are no comments for Course in the schema.
        /// </summary>
        [global::System.Data.Objects.DataClasses.EdmRelationshipNavigationPropertyAttribute("AssociationsModel", "CourseCourseSchedule", "Course")]
        [global::System.Xml.Serialization.XmlIgnoreAttribute()]
        [global::System.Xml.Serialization.SoapIgnoreAttribute()]
        [global::System.Runtime.Serialization.DataMemberAttribute()]
        public Course Course
        {
            get
            {
                return ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.GetRelatedReference<Course>("AssociationsModel.CourseCourseSchedule", "Course").Value;
            }
            set
            {
                ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.GetRelatedReference<Course>("AssociationsModel.CourseCourseSchedule", "Course").Value = value;
            }
        }
        /// <summary>
        /// There are no comments for Course in the schema.
        /// </summary>
        [global::System.ComponentModel.BrowsableAttribute(false)]
        [global::System.Runtime.Serialization.DataMemberAttribute()]
        public global::System.Data.Objects.DataClasses.EntityReference<Course> CourseReference
        {
            get
            {
                return ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.GetRelatedReference<Course>("AssociationsModel.CourseCourseSchedule", "Course");
            }
            set
            {
                if ((value != null))
                {
                    ((global::System.Data.Objects.DataClasses.IEntityWithRelationships)(this)).RelationshipManager.InitializeRelatedReference<Course>("AssociationsModel.CourseCourseSchedule", "Course", value);
                }
            }
        }
    }
}

Явная и упреждающая загрузка

Entity Framework не поддерживает ленивую загрузку (Lazy Loading). Получение всех ассоциированных объектов выполняется двумя способами:

  1. Упреждающая загрузка (включение ассоциации в запрос).

    ObjectQuery<T> содержит метод Include, которому можно передать список имен ассоциаций, разделенных точкой.

  2. Явная загрузка коллекций вызовом метода Load.

Пример:

using (var associationsEntities = new ObjectContext("name=AssociationsEntities")) {
    associationsEntities.DefaultContainerName = "AssociationsEntities";

    var courses = associationsEntities.CreateQuery<Course>("[Course]").Include("CourseDescription");
    foreach (var course in courses) {
        Trace.WriteLine(String.Format("Course: ID = {0}, Name = {1}", course.ID, course.Name));
        Debug.Assert(course.CourseSchedule.Count == 0);
        Debug.Assert(!course.CourseSchedule.IsLoaded);
        Debug.Assert(course.CourseDescription != null);
        course.CourseSchedule.Load();
        Debug.Assert(course.CourseSchedule.IsLoaded);
        foreach (var courseSchedule in course.CourseSchedule) {
            Trace.WriteLine(String.Format("\tSchedule: ID = {0}, BeginTime = {1}, EndTime = {2}, DateProvided = {3}",
                                          courseSchedule.ID, courseSchedule.DateProvided, courseSchedule.BeginTime,
                                          courseSchedule.EndTime));
        }
    }
}

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

LINQ to Entities

LINQ to Entities - последнее звено абстракций Entity Framework, которое работает поверх Object Services, а точнее - поверх ObjectQuery<T>. LINQ-запрос трансформируется в Canonical Command Tree (CCT) и передается для исполнения в ObjectContext.

Как и Entity SQL, LINQ не поддерживает операторы ЯМД (DML) напрямую. Модифицировать сущности в базе данных можно только с помощью служб Object Services (используя метод SaveChanges).

Запросы LINQ предполагают строгий контроль типов и возможность возвращать сущности и проекции. С другой стороны, Entity SQL позволяет выполнять более сложные запросы.

Если обобщить, то Entity SQL работает с любыми EDM и позволяет выполнять динамические запросы, а LINQ to Entities оперирует над статическим EDM (кодом) и выполняет статические запросы (которые представляют собой тоже код).

Entity Client VS Object Services VS LINQ to Entities

Ниже представлена сводная таблица, демонстрирующая важные характеристики Entity Client, Object Services и LINQ to Entities:

Entity Client и Entity SQL Службы Object Services LINQ to Entities
Напрямую к поставщику Entity Client и Entity SQL Да Нет Нет
Хорош для оперативных запросов Да Да Нет
Может выдавать DML напрямую Нет Нет Нет
Строго типизирован Нет Нет Да
Может возвращать сущности как результат Нет Да Да

Архитектура Entity Framework

Архитектура Entity Framework очень сильно напоминает EJB: eSQL = EJBQL, ObjectContext = EntityManager, ejb-jar.xml = csdl.xml+ssdl.xml+msl.xml. Где-то Entity Framework слабее EJB, где-то сильнее.

To be continued...

Copyright 2007-2011 Chabster