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

Morshins'ka

У меня в машине в дверном отсеке лежит 2 бутылки Моршинской. Одна зеленая 0,5 литра и другая белая 0,75 литра (бутылочка с соской). Я заметил, что вода в зеленой бутылке замерзла, а в белой не замерзла при -7. Больше белую не пью!

Microsoft GraveYard

Наткнулся на забавный мувик...

BITS Manager v1.02.0001

Очередная версия BITS Manager. Теперь с возможностью посмотреть список файлов задачи. Добавил Caption Bar, в котором сейчас отображается версия службы Background Intelligent Transfer Service.

BITS Manager v1.02.0001

Download BITS Manager v1.02.0001

Next version: BITS Manager v1.02.0002.

BITS Manager v1.02.0000

Очередная версия BITS Manager. Теперь с возможностью посмотреть профиль задачи, изменять некоторые параметры и характеристики задачи, закреплять задачи. Также визуальное барахло - иконки, тултипы. Список файлов задачи пока что не отображается. Заблокированы некоторые элементы управления в окене Job profile. To be continued, короче. Забыл, добавлена обработка ошибок!

BITS Manager v1.02.0000

Download BITS Manager v1.02.0000

Next version: BITS Manager v1.02.0001.

Set caret to the beginning of EDIT control when focused

Как многим, наверное, известно, при получении фокуса элемент управления EDIT выделяет весь текст и выполняет скроллиорвание таким образом, что на экране обязательно виден конец выделения. Если почитать внимательно документацию по сообщению EM_SETSEL, можно сделать вывод, что это происходит по следующей причине: The control displays a flashing caret at the end position regardless of the relative values of start and end. А что делать, если я хочу показать начало строки, а не ее конец? И зачем вообще по умолчанию показывать хвост длинной строки? Тем более, что убить это чертово умолчание не так то просто...

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

// Set caret to the end, clear the selection
Edit_SetSel(pFocusedWnd->m_hWnd, INT_MAX, INT_MAX);
//    Send Shift+Home or just Home if Shift is at pressed state
INPUT inputs[4] = { 0 };
BOOST_FOREACH(INPUT &input, inputs) {
    input.type = INPUT_KEYBOARD;
}
// {SHIFT
inputs[0].ki.wVk = VK_SHIFT;
// {HOME
inputs[1].ki.wVk = VK_HOME;
// HOME}
inputs[2].ki.wVk = VK_HOME;
inputs[2].ki.dwFlags = KEYEVENTF_KEYUP;
// SHIFT}
inputs[3].ki.wVk = VK_SHIFT;
inputs[3].ki.dwFlags = KEYEVENTF_KEYUP;

bool shiftPressed = (GetKeyState(VK_SHIFT) < 0);

SendInput(_countof(inputs) - int(shiftPressed) * 2, &inputs[int(shiftPressed)], sizeof(INPUT));

Этот код нужно выполнить сразу после получения элементом управления фокуса. Логика простая - переместить каретку в конец строки и выделить ее нажатием комбинации клавиш Shift+Home. Только так можно выделить весь текст и поставить каретку в его начало! Здесь есть маленький нюанс - перемещение фокуса может осуществляться в обратном порядке и при этом клавиша Shift находится в нажатом состоянии. Поэтому нужно делать проверку и не нажимать ее программно.

Intercepting dialog focus events

В рамках моего мини-проекта BITS Manager решил сделать удобный и красивый диалог. Основная изюминка - заголовки элементов управления, которые меняют свой внешний вид в зависимости от движения фокуса. Если конкретнее - меняется шрифт у заголовка активного элемента:
MFC dialog example

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

Я увидел следующие решения данной проблемы:

  1. В глобальном фильтре сообщений приложения ловить WM_SETFOCUS и WM_KILLFOCUS.

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

  2. Использовать перехват сообщений - Win32 Hooks.

    Ловушка WH_CBT с кодом HCBT_SETFOCUS позволяет отловить момент, когда система собирается установить фокус на окно. Это хорошо, но пришлось бы сохранять и обновлять идентификатор окна, которое будет терять фокус. Можно использовать и другие ловушки для перехвата сообщений WM_SETFOCUS и WM_KILLFOCUS.

  3. Воспользоваться тем, что все известные Win32 элементы управления шлют родителю уведомления о некоторых событиях, среди которых ***_SETFOCUS и ***_KILLFOCUS.

Я остановился на 3-м варианте, первые два мне показались черезчур сложными для такой задачи.

How to intercept dialog focus events in one place

Пришлось слегка почитать документацию и покопаться в коде MFC. Итак, есть два типа элементов управления. Первый тип использует сообщение WM_NOTIFY для уведомления родителя, второй тип использует WM_COMMAND. Оказалось, MFC в любом случае вызывает виртуальный метод BOOL OnCmdMsg(UINT nID, int nCode, void *pExtra, AFX_CMDHANDLERINFO *pHandlerInfo). После медитаций у меня получился такой код:

BOOL CJobProfileDlg::OnCmdMsg(UINT nID, int nCode, void *pExtra, AFX_CMDHANDLERINFO *pHandlerInfo) {
    const MSG &lastSentMsg = AfxGetThreadState()->m_lastSentMsg;

    LPCTSTR szEditClass = _T("EDIT");
    LPCTSTR szComboBoxClass = _T("COMBOBOX");
    LPCTSTR szListBoxClass = _T("LISTBOX");
    if (
        ((nCode == EN_KILLFOCUS) && Utility::CompareWindowClass(reinterpret_cast<HWND>(lastSentMsg.lParam), szEditClass))
        || ((nCode == CBN_KILLFOCUS) && Utility::CompareWindowClass(reinterpret_cast<HWND>(lastSentMsg.lParam), szComboBoxClass))
        || ((nCode == LBN_KILLFOCUS) && Utility::CompareWindowClass(reinterpret_cast<HWND>(lastSentMsg.lParam), szListBoxClass))
        || ((HIWORD(nCode) == WM_NOTIFY)) && (LOWORD(nCode) == LOWORD(NM_KILLFOCUS))) {
        OnControlLostFocus(nID);
    }
    if (((nCode == EN_SETFOCUS) && Utility::CompareWindowClass(reinterpret_cast<HWND>(lastSentMsg.lParam), szEditClass))
        || ((nCode == CBN_SETFOCUS) && Utility::CompareWindowClass(reinterpret_cast<HWND>(lastSentMsg.lParam), szComboBoxClass))
        || ((nCode == LBN_SETFOCUS) && Utility::CompareWindowClass(reinterpret_cast<HWND>(lastSentMsg.lParam), szListBoxClass))
        || ((HIWORD(nCode) == WM_NOTIFY) && (LOWORD(nCode) == LOWORD(NM_SETFOCUS)))) {
        OnControlGotFocus(nID);
    }

    return(__super::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo));
}

Здесь я долго пытался понять, почему иногда не работает, и в результате боролся с двумя проблемами:

  1. #define CBN_KILLFOCUS       4
    #define LBN_SETFOCUS        4
    

    В итоге пришлось добавить еще код проверки класса окна.

  2. #define NM_FIRST                (0U-  0U)       // generic to all controls
    #define NM_SETFOCUS             (NM_FIRST-7)
    #define NM_KILLFOCUS            (NM_FIRST-8)
    

    MFC пакует два значения в одно с помощью MAKELONG, в результате чего отрицательные значения NM_*** после извлечения теряют старшее слово. Нужно при сравнении обрезать оба операнда.

Приведу весь код для потомков:

bool Utility::CompareWindowClass(HWND hwnd, LPCTSTR pszWindowClass) {
    const int cch = _tcslen(pszWindowClass) + 1;
    LPTSTR pszClassName = reinterpret_cast<LPTSTR>(_alloca(sizeof(TCHAR) * cch));
    if (GetClassName(hwnd, pszClassName, cch)) {
        if (_tcsncicmp(pszClassName, pszWindowClass, -1) == 0)
            return(true);
    }
    return(false);
}
void CJobProfileDlg::OnControlGotFocus(UINT nID) {
    CWnd *pStatic = GetDlgItem(nID)->GetNextWindow(GW_HWNDPREV);
    if (pStatic && Utility::CompareWindowClass(pStatic->m_hWnd, _T("STATIC")))
        pStatic->SetFont(&m_fntStaticActive);
}

void CJobProfileDlg::OnControlLostFocus(UINT nID) {
    CWnd *pStatic = GetDlgItem(nID)->GetNextWindow(GW_HWNDPREV);
    if (pStatic && Utility::CompareWindowClass(pStatic->m_hWnd, _T("STATIC")))
        pStatic->SetFont(&m_fntStatic);
}

Предполагается, что порядок обхода задан таким образом, что заголовок элемента управления предшествует ему.

Mazda 3 and not Mazda 3

Когда я выбирал машину (а длилось это больше года), то искал в интернете сравнительные тесты автомобилей С-класса. Начитавшись вдоволь, поездил по салонам и посидел внутри. В результате выбор пал на Mazda 3. По словам многих это лидер С-класса и очень качественный автомобиль. И внешне эта машина, пожалуй, даже очень хороша.
Sex with Mazda 3

Но так сложилось, что я купил Volkswagen Golf V (5-е поколение) 1.6 АКПП и езжу на нем уже 2 месяца. А вот недавно мне довелось прокатиться на 2-литровой версии Mazda 3 в кузове хэтчбэк на ручке. Дальше, собственно, впечатления.

Рекомендую взглянуть для начала, что девушки пишут про матрёшку. Глупо отрицать, что это не женский автомобиль, когда на нем ездят в основном девушки.

Итак, почему эта машина - говно, по пунктам:

  • Подвеска. В Mazda 3 стоит многорычажная подвеска сзади и стойки МакФерсона спереди. Это один из немногих авто С-класса с многорычажной подвеской. Многорычажку, как правило, ставят на машины классом выше. Но, судя по всему, Mazda 3 чхать хотела на это. Говорят, это машина с намеком на спортивность, с жесткой подвеской. Знаете что? Засуньте себе в жопу эту спортивность и пусть создатели научатся делать нормальные автомобили. Это же просто зубодробилка! На ямах машина бьется днищем, у меня после двух ударов позвоночник болел остаток вечера. В том же режиме езды, на тех же ямах мой Golf V отработал на твердую пятерку! Не верьте людям, которые утверждают, что жесткая подвеска - это хорошо. Как минимум, это некомфортно по Киевским дорогам. И будьте уверены, что машины бывают двух видов: 1) быстрые и жесткие - японские 2) мягкие и быстрые - немецкие. Выбирать вам, я люблю комфорт. А управляемость от подвески мало зависит.
  • Шумоизоляция. Я не верил ушам своим, когда при спокойной езде слышал шум песка в колесных арках. Говорят, это позволяет иметь представление о дорожном покрытии. Знаете что? Засуньте свои представления себе в жопу и поймите, что машина должна ехать, как хочется водителю и куда хочется водителю, а не передавать состояние дороги.
  • Информативный руль. Многие говорят, что классно, когда руль дребезжит и дергается в такт состояния дорожного покрытия. Знаете что? Думаю, уже знаете. Во-первых, информативность руля - это абсолютно другой показатель. Во-вторых, на скорости руль Mazda 3 нужно держать крепко-крепко, чтобы не улетел в окно. Я свой Golf V рулю одним пальцем даже на скоростях выше 140. И он едет ровно туда, куда я ему сказал.
  • ...качество пластика внутри вызывает зависть у обладателей мерседесов... Мне смешно такое читать, честное слово. Качество отделки салона скудное. Не верьте менеджерам, не верьте друзьям. Не верьте людям, которые никогда не владели немецкой машиной. Я сел в Mazda 3 и долго раскручивал ремень безопасности. Он и на вид и на ощупь был хуже тряпки. Даже ключ зажигания - и тот похож на ключ от сарая моей бабушки. Спортивные сидения больше похожы на паралоновые.
  • Эргономика мне не понравилась. Места меньше, чем в моем Golf V. Что спереди, что сзади. Я завел эту девочку и потянулся рукой снять ручник. А рука его не нащупала. Наверное, ручником должен орудовать пассажир, т.к. он находится прямо возле него. Мой рост 185, длинные ноги. С задней двери выйти из машины мне тяжело. Консоль занимает в два раза больше места, чем могла бы. Машина ублюдочно пищит, если ты не пристегнулся. Так ублюдочно, что хочется дать по ней кувалдой. Не пикает, а непрерывно пищит! Багажник на вид больше, а по литражу меньше, чем в Golf V. При этом у мазды докатка, а у немца - полноценная запаска. Признавайтесь, японцы, куда место подевали?
  • Если говорить о АКПП, то абсолютно глупо утверждать, что 4 ступени у Mazda 3 лучше, чем 6 у Golf V. 6 ступеней - это гарантированно меньше расход и лучшая плавность хода. И японским инженерам еще долго нужно курить правильную траву, чтобы засунуть 6 ступеней в двигатель 1.6. В Мерседес С-класса, кстати, даже в двигатель 2.3 литра сумели 7 ступеней засунуть.

Уверен, можно долго перечислять недостатки Mazda 3 перед своими немецкими собратьями. Смысл в другом. Мне эта машина нравилась, честно. Нравилась ровно до того момента, когда я на ней проехался. Женщины выбирают этот автомобиль, наверное, исключительно из-за гламурного внешнего вида. Многие при этом ездят на ручке. А ради чего?

Мой вывод: машина неоправданно считается лидером С-класса. Хорошо, что я не купил это корыто! И вам тоже не советую. Разве что вы хотите рассказывать налево и направо, что у вас спортивная машина, и не обращать внимание на тот прискорбный факт, что едет Mazda 3, как говно.

BITS Manager v1.01.0000

Новая версия BITS Manager. Позволяет управлять состоянием задач, а также менять их приоритеты. Появился простенький About.

Download BITS Manager v1.01.000

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

Next version: BITS Manager v1.02.0000.

Too much exception detalization

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

waffles.fm invites

Господа, у меня есть пачка инвайтов на waffles.fm. Кому нужно, процедура следующая: я получаю от Вас письмо со скрином профиля на известном трекере, проверяю, что Ваше ратио существенно больше единицы. Далее, когда появляется возможность выслать инвайт - Вы его получаете. С глупыми просьбами аля "я хороший, обещаю бла-бла-бла" просьба не беспокоить.

For all those who can't understand Russian or just not aware of Google translator. The procedure is the following: you send me screens of your profile on a well known torrent tracker, I check your ratio >> 1 (considerably higher than 1) and send you an invite when registration opens. Please don't write messages like "I want an invite, I promise I'll keep my ratio and blah-blah-blah". If you prove that you are not a leacher - you'll get the invite.

Guys, 4 accounts have already been disabled for inactivity. Please don't ask for invites if you are going to waste them. Thanks.

UPD 17-Jun-2009: i'm still doing it, one invite in a week.

BITS monitoring and management tool

Представляю вашему вниманию первую версию утилиты по управлению службой Background Intelligent Transfer Service (BITS).
BITS Manager

Программа отображает активное состояние очереди задач. Управлять задачами пока еще нельзя. Скачать установщик можно здесь.

Программа, кстати, отображает список задач всех пользователей, если ее запустить из-под администратора.
Windows Updates
BITS Manager displays WUS download jobs

Even process explorer has bugs

Process Explorer error
Process Explorer error

if block and variable declaration

Известно, что С++ позволяет объявлять переменную в условии блока if:

if (SomeType st = SomeType(...)) {
   ...
}

Удивило, что использовать другой стиль инициализации невозможно:

if (SomeType st(...)) {
   ...
}

А почему?

Replacing standart scrollbars

Сейчас занимаемся созданием библиотеки элементов управления. UX команда, как обычно, выдумывает ненужный бред, а мы его рисуем. Порой, правда, приходится совсем туго (и даже не от того, что придурки-дизайнеры не думают головой). С одной из тугих проблем я боролся неделю. Проблема зовется заменить стандартные Windows Scrollbars на свои или просто owner draw scrollbars (custom scrollbar drawing). Гугление не дало абсолютно никакого полезного результата! Нашлась лишь одна статья на эту тему (Cool Scrollbars) и неработающий пример. Дальше вы прочитаете мой путь к решению и, конечно же, узнаете само решение.

Постановка задачи

Создать (запрограммировать) окно с полосой прокрутки вовсе несложно и есть два способа решения такой задачи: 1) рисовать на неклиентской (или даже клиентской) области, обрабатывать сообщения и т.д. 2) встраивать дополнительное окно, которое мимицирует поведение полосы прокрутки. Windows поддерживает оба механизма (смотреть There are two types of scrollbars и Scrollbars and Scrolling). Но для обоих случаев логика прокручивания ложится на разработчика и инкапсулируется в элементе управления. А что делать, если исходные коды недоступны, а полосы прокрутки нужно заменить на свои?

Итак, задача следующая - изменить внешний вид полос прокрутки у стандартных элементов управления (LISTBOX, COMBOBOX etc.), т.е. скроллбаров, которые появляются у окошек со стилем WS_VSCROLL или WS_HSCROLL.

Процесс подмены полос прокрутки (исследование проблемы)

Первое, что приходит в голову - убрать стиль WS_VSCROLL (займемся лишь вертикальной прокруткой, горизонтальная ничем не отличается), перехватить WM_NC-сообщения и рисовать свой красивый ползунок с прибамбасами. Все отлично, но есть проблемка: связь полоса прокрутки - элемент управления двунаправленная (скроллбар управляет окном и наоборот). В результате теряется реакция скроллбара на изменение состояния окна (например, активный элемент списка установлен нажатием клавиши END, соответственно, окно прокручено вниз).

Окно управляет состоянием ползунка с помощью метода int SetScrollInfo(HWND hwnd, int fnBar, LPCSCROLLINFO lpsi, BOOL fRedraw). В случае отсутствия стиля WS_VSCROLL SetScrollInfo не вызывается, соответственно, уведомления от окна о необходимости изменить положение ползунка не приходят. В результате стиль WS_VSCROLL обязателен и рисуется стандартный скроллбар. Что дальше? Дальше мы выясняем, что провоцирует отрисовку полосы прокрутки. И стараемся предотвратить возникновение этих ситуаций.

Реакция полос прокрутки связана с обработкой окном WM_NC-сообщений. Это просто и логично. Первое, что я сделал - перехватил эти сообщения, чтобы отключить их стандартную обработку. Я был очень удивлен, когда после этого опять увидел скроллбар!

Дальше я нашел функцию отрисовки скроллбара в библиотеке comctl32.dll и смотрел кто ее дергает. У меня Windows Vista и включен Aero. Вот, что выяснилось в результате:

>  comctl32.dll!DrawThumb2()
   comctl32.dll!xxxDrawThumb()  + 0x57 bytes
   comctl32.dll!_CCSetScrollInfo@16()  - 0x8ef9 bytes
   uxtheme.dll!_ThemeSetScrollInfoProc@16()  + 0x5c12 bytes
   user32.dll!_SetScrollInfo@16()  + 0x3c bytes
   comctl32.dll!ListBox_SetScrollParms()  + 0x336 bytes
   comctl32.dll!ListBox_NewITopEx()  + 0x51 bytes
   comctl32.dll!ListBox_NewITop()  + 0x12 bytes
   comctl32.dll!ListBox_InsureVisible()  - 0x36beb bytes
   comctl32.dll!ListBox_SetISelBase()  + 0x2b bytes
   comctl32.dll!ListBox_KeyInput()  + 0x47b bytes
   comctl32.dll!ListBox_WndProc()  + 0x49b bytes
   user32.dll!_InternalCallWinProc@20()  + 0x23 bytes
   user32.dll!_UserCallWinProcCheckWow@32()  + 0xb3 bytes
   user32.dll!_CallWindowProcAorW@24()  + 0x51 bytes
   user32.dll!_CallWindowProcW@20()  + 0x1b bytes
   ScrollbarsCust.exe!LBWndProc(HWND__ * hWnd=0x00460d0e, unsigned int message=0x00000100, unsigned int wParam=0x00000023, long lParam=0x014f0001)  Line 322   C++
   user32.dll!_InternalCallWinProc@20()  + 0x23 bytes
   user32.dll!_UserCallWinProcCheckWow@32()  + 0xb3 bytes
   user32.dll!_DispatchMessageWorker@8()  + 0xe6 bytes
   user32.dll!_DispatchMessageW@4()  + 0xf bytes
   ScrollbarsCust.exe!wWinMain(HINSTANCE__ * hInstance=0x008a0000, HINSTANCE__ * hPrevInstance=0x00000000, wchar_t * lpCmdLine=0x000949b6, int nCmdShow=0x00000001)  Line 95 + 0xb bytes  C++
   ScrollbarsCust.exe!__tmainCRTStartup()  Line 578 + 0x1c bytes  C
   kernel32.dll!@BaseThreadInitThunk@12()  + 0x12 bytes
   ntdll.dll!___RtlUserThreadStart@8()  + 0x27 bytes
   ntdll.dll!__RtlUserThreadStart@8()  + 0x1b bytes

Мы попали в функцию отрисовки после нажатия клавиши END, о чем говорят параметры вызова оконной процедуры ScrollbarsCust.exe!LBWndProc(HWND__ * hWnd=0x00460d0e, unsigned int message=0x00000100, unsigned int wParam=0x00000023, long lParam=0x014f0001). 0x00000100 - это WM_KEYDOWN. В стеке вызовов присутствует также функция SetScrollInfo - user32.dll!_SetScrollInfo@16(). И что самое интересное, именно она провоцирует отрисовку полосы прокрутки!

Погуглив функцию я нашел несколько заметок, из которых самой интересной оказалась SetScrollInfo() architecture bug. Ничего нового, впрочем, просто подтверждение моего результата.

Что делать? Единственный выход, который я увидел, - перехватить вызов функции SetScrollInfo. Продвинутые читатели, возможно, зададут вопрос - почему не перехватить функции отрисовки? Во-первых, адреса этих функций известны только через отладочные символы. Во-вторых, логика отрисовки инкапсулирована в библиотеке comctl32.dll и в других ее версиях этих функций может вовсе не быть. А SetScrollInfo - часть Windows API, который врядли будет меняться в ближайшее время.

Других факторов, провоцирующих отрисовку полос прокрутки я не нашел (WM_NC не в счет - их мы все равно перехватываем и обрабатываем сами). Правда, есть уверенность, что они существуют, просто не проявились.

На этом исследование мое завершилось, пора переходить к кодированию.

Процесс подмены полос прокрутки (кодирование)

Итак, перехватываем SetScrollInfo. Матчасть состоит из одной статьи Process-wide API spying - an ultimate hack и одной функции:

PVOID HookAPI(PBYTE pbModule, PCSTR pszName, PVOID pvOrg, PVOID pvNew) {
    PIMAGE_THUNK_DATA r;
    PIMAGE_NT_HEADERS p;
    PIMAGE_IMPORT_DESCRIPTOR q;

    p = (PIMAGE_NT_HEADERS)(pbModule + ((PIMAGE_DOS_HEADER)pbModule)->e_lfanew);
    q = (PIMAGE_IMPORT_DESCRIPTOR)(pbModule + p->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

    for (; q->Name; q++) {
        if (lstrcmpiA(pszName, (PCSTR)(pbModule + q->Name)) == 0) {
            for (r = (PIMAGE_THUNK_DATA)(pbModule + q->FirstThunk); r->u1.Function; r++) {
                if ((PVOID)r->u1.Function == pvOrg) {
                    WriteProcessMemory(GetCurrentProcess(), &r->u1.Function, &pvNew, sizeof(PVOID), NULL);
                    return(pvOrg);
                }
            }
        }
    }
    return(NULL);
}

Что делаем? Сохраняем адрес оригинальной функции SetScrollInfo и подменяем ее адрес на свою реализацию:

typedef int (WINAPI *SetScrollInfoFun)(HWND hwnd, int fnBar, LPCSCROLLINFO lpsi, BOOL fRedraw);
SetScrollInfoFun originalSetScrollInfo;

int WINAPI MySetScrollInfo(HWND hwnd, int fnBar, LPCSCROLLINFO lpsi, BOOL fRedraw) {
    ...
}
HMODULE user32dllHandle = GetModuleHandle(_T("user32.dll"));
HMODULE comctl32dllHandle = GetModuleHandle(_T("comctl32.dll"));
originalSetScrollInfo = (SetScrollInfoFun)GetProcAddress(user32dllHandle, "SetScrollInfo");
HookAPI((PBYTE)comctl32dllHandle, "user32.dll", originalSetScrollInfo, &MySetScrollInfo);

Здесь, кстати, важно, что мы подменяем функцию только для кода в библиотеке conctl32.dll!

Вообще, для таких вещей существует интересная библиотека Microsoft Detours. Рекомендую взглянуть.

Кульминация спектакля - код функции MySetScrollInfo:

int WINAPI MySetScrollInfo(HWND hwnd, int fnBar, LPCSCROLLINFO lpsi, BOOL fRedraw) {
    int rv = originalSetScrollInfo(hwnd, fnBar, lpsi, /*fRedraw*/FALSE);
    ::SendMessage(hwnd, WM_USER + 1, 0, 0);
    return(rv);
}

Все просто - вызов оригинальной SetScrollInfo с измененным 4-м параметром (мы просим оригинал не рисовать полосу прокрутки, иначе изображение будет мигать). Далее нужно нарисовать свой ползунок с прибамбасами, для чего я посылаю окну специальное сообщение, на которое оно реагирует отрисовкой скроллбара.

Остается написать реакции на WM_NC-сообщения, код отрисовки полос прокрутки, код подмены оконной процедуры интересующего контрола и, наверное, фильтрацию окон в MySetScrollInfo - мы же не все окна хотим насиловать своими глупостями.

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

Visual Studio TR1 debugger features

Только что заметил полезную особенность Visual Studio 2008 с установленным Feature Pack:
Visual Studio 2008 TR1 debugger features
Посмотреть счетчик ссылок в boost - настоящая морока.

Back again to DCOM

Сколько лет уже технологиям COM/DCOM, а я все чаще обращаюсь к документу The Component Object Model Specification. Version 0.9. October 24, 1995. Можно найти ответы на все вопросы. А вопросы иногда возникают жестокие [:-)].

Entity Framework rationale

Традиционные клиент-сервер приложения делегируют операции запроса и сохранения информации системам баз данных. Последние оперируют данными в форме строк и таблиц, а приложения - в рамках высокоуровневых конструкций языка (классы, структуры). Такая нестыковка имеет вполне определенное имя - Object-relational impedance mismatch. С наступлением эпохи SOA, серверов приложений а также многоуровневых приложений, необходимость в службах манипуляций данными, которые тесно интегрированы со средой разработки, крайне возросла.

ADO.NET Entity Framework - это платформа разработки data-driven приложений, которая повышает уровень абстракции с реляционного уровня до концептуального, тем самым существенно уменьшая проблему impedance mismatch.

Мотивация

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

  1. Физическая модель.

    Описывает способ хранения данных в контексте физических ресурсов - память, диски, форматы, индексы и т.д.

  2. Логическая (реляционная) модель.

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

  3. Концептуальная модель.

    Охватывает основные информационные сущности предметной области, а также их связи. Хорошо известная всем (и самая популярная) Entity-Relationship Model была представлена Peter Chen в 1976 году. UML является более свежим примером концептуальной модели. Большинство приложений входят в этап концептуального прототипирования на ранних этапах жизненного цикла приложений. К сожалению, концептуальные модели остаются картинками, которые невозможно совместить с реалиями реализации. Важным достижением Entity Framework является возможность представления концептуальнй модели, как реальной абстракции платформы разработки.

  4. Модель программирования\представления.

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

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

Entity Framework - это платформа разработки, которая повышает уровень абстракции с реляционного до концептуального. Она имеет собственный язык запросов, гибкую архитектуру. До некоторой степени концептуальная модель не зависит от реляционной - отображение является независимой внешней единицей метаданных. Получается, можно менять схему базы данных в некоторых границах без аффекта кода приложения. Механизм провайдеров теоретически позволяет использовать любую базу данных, которая удовлетворяет требованиям Entity Framework.

В моей вступительной статье The splendour and misery of Entity Framework designer, architecture and runtime я рассказывал о Entity Data Model, или сокращенно - EDM. Зачем понадобилось изобретать колесо в виде новой концепции моделирования? Было ли действительно изобретено колесо? Почему бы не использовать или расширить одну из существующих концепций моделирования? Кандидаты на эту позицию следующие:

  1. SQL data model.

    Стандарт SQL99 расширяет модель, добавляя объектно-реляционные возможности.

  2. Managed data model (CLR).
  3. XSD data model.
  4. UML data model.

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

Важным аспектом Entity Data Model является ориентированность на данные (как в SQL), а не на объектно-ссылочные концепции (как в C#). Именно поэтому Entity Framework больше тяготеет к ORM нежели к доменной модели приложения. Именно поэтому Entity Framework имеет мощный язык запросов - eSQL. При этом ничто не мешает построить уровень бизнес логики поверх концептуальной модели Entity Framework.

C++ recursive template

Попробуйте угадать результат компиляции и запуска такой программы:

#include "stdafx.h"

template<class T>
struct Z
{
    Z<T*> z;
};

int _tmain(int argc, _TCHAR* argv[])
{
    Z<int> z;
    return 0;
}

Development pearls

Хохочу уже минуты две...

Dread (18:12:16 9/09/2008)
очень часто я пишу onLick вместо onClick

Google pages bitter end

Google is shutting down Google Page Creator program and will now be replaced with Google Sites. Read more.

New Metallica album 'Death Magnetic' leaks

Не знал, что Metallica готовит новый альбом. Релиз 12-го сентября, но уже украден.

Треклист:

  1. That Was Just Your Life
  2. The End of the Line
  3. Broken, Beat & Scarred
  4. The Day That Never Comes
  5. All Nightmare Long
  6. Cyanide
  7. The Unforgiven III
  8. The Judas Kiss
  9. Suicide & Redemption
  10. My Apocalypse

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.

Lightweight browser Google Chrome

Google сделали свой браузер и назвали его Google Chrome. Бета версия:

Приколисты, блин:

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