четверг, января 31, 2008

Lean - agile в кубе, или 7 серебряных пуль в тело вашего проекта

Вот интересная вещь: никто не учит, как завалить проект, но все учат, как добиться успеха в разработке софта, методологии, XP, TDD, Agile, НЛП и т.д. и т.п. Тем не менее проекты заваливаются гораздо чаще, чем заканчиваются успехом.

Вот новое поветрие в организации разработки - Lean development. Слыхали? Я слышал это словосочетание довольно давно, однако сегодня мне попался на глаза текст, который буквально вогнал меня в ступор:

«Lean Software Development – это не методология управления разработкой проекта, как может показаться на первый взгляд. Это набор принципов, применяемых в различных проектах для улучшения процесса разработки и повышения его эффективности.

Цели Lean – сделать возможной разработку проекта за треть времени, с использованием только трети бюджета и с уровнем дефектов, уменьшенным до одной третьей. Как этого достичь?

Lean предлагает 7 ключевых принципов…»


Вот на целях меня и заклинило.
Знаете, я читал Брукса. Очень давно, мы тогда еще исходники на сетевой папке хранили, и не знали что такое тесты. Потом много думал. Потом было много проектов, и я усвоил много интересных вещей, которые сейчас кажутся мне простыми, а тогда были как откровение. Среди них «треугольник компромиссов», который постулирует принцип: ресурсы, сроки и фичи составляют жесткую конструкцию, в которой при фиксированной величине одного плеча, два другие связаны обратной зависимостью. Например, при фиксированных сроках больше фич можно сделать только большими ресурсами.

И вот читаю я про Lean и привычный миропорядок зашатался у меня под ногами. Все можно сделать в три раза быстрее, и при этом в три раза меньшими силами и в три раза качественнее! Дьявол, где были эти парни раньше?

Читаю дальше, что это за 7 серебряных пуль, которые дадут двадцати-семикратное общее ускорение моим захиревшим проектам? Так, первое «не заниматься ерундой», понятно. Второе, «учиться, учиться и учиться», это еще старик Крупский говорил… «Принимайте решения как можно позже», ну до этого каждый доходит после пары грандиозных рефакторингов на излете проекта :). Четвертое, «выкладывайте результаты как можно раньше», этому нас старина Кент Бек уже давно научил . Что там дальше, - команда нужна хорошая…, за качество боремся…, зрим в корень… Все.

Все? Это все?
По принципам, так вроде как agile получается, а по результатам, так настоящий agile в кубе.
Есть еще 22 практики, но чтоб про них прочитать надо книжку покупать. Но, если честно, как-то не тянет.
Книга, может и хорошая, но вот такие проповеди Lean в стиле гербалайфа и средств для увеличения размера мужского достоинства, отбивают всякую охоту читать что либо дальше.

Читайте лучше "Мифический человеко-месяц" или хотя бы "Серебрянной пули нет" дедушки Брукса. Либо, на худой конец "Путь камикадзе, или Как разработчику программного обеспечения выжить в безнадежном проекте" Йордона, если попадете на Lean проект.

среда, января 30, 2008

Entity Framework - mapping (часть 2)

Продолжение. Начало здесь

Один класс - несколько EntitySet.



Еще один интересный вариант маппинга один класс – несколько таблиц, когда все таблицы имеют одинаковую структуру. В терминах EF это называется multiple entity sets per type (MEST). Рассмотрим пример БД:



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

Если создать EDM визардом, мы получим две идентичные сущности Request и RequestHistory, и два соответствующих им EntitySet. Согласитесь это не очень удобно. Вот если бы EntitySet Request и RequestHistory остались, но оба использовали один и тот-же тип сущности. И это можно сделать. Однако нам придется опять расстаться с дизайнером, поскольку он не поддерживает такой вариант маппинга.

Итак, закрываем дизайнер и открываем edmx файл в XML редакторе. Изменяем описание EntitySet RequestHistory, чтобы он использовал класс Entity вместо EntityHistory:


<EntityContainer Name="MultiSet">
<EntitySet Name="Request" EntityType="MultiSetModel.Request" />
<EntitySet Name="RequestHistory" EntityType="MultiSetModel.Request" />
</EntityContainer>


Затем удаляем описание сущности RequestHistory (<EntityType Name="RequestHistory">).

Наконец правим маппинг EntitySet RequestHistory, заменяем тип RequestHistory на Request (измененное выделено):


<EntitySetMapping Name="RequestHistory">
<EntityTypeMapping TypeName="IsTypeOf(MultiSetModel.Request)">
<MappingFragment StoreEntitySet="RequestHistory">
<ScalarProperty Name="Id" ColumnName="Id" />
<ScalarProperty Name="IncomDate" ColumnName="IncomDate" />
<ScalarProperty Name="Text" ColumnName="Text" />
<ScalarProperty Name="AcceptDate" ColumnName="AcceptDate" />
</MappingFragment>
</EntityTypeMapping>
</EntitySetMapping>



Вот собственно и все. Теперь класс Request используется при запросах к обеим коллекциям (и дизайнер не может открыть нашу модель).

Маппинг на хранимые процедуры



Многие спрашивают, поддерживает ли EF при маппинге классов хранимые процедуры? Для любителей маппинга на хранимые процедуры у меня две новости, хорошая и плохая. Хорошая состоит в том, что маппинг сущностей на хранимые процедуры в EF есть. Плохая – полностью отказаться от использования запросов в маппинге не возможно. Использовать хранимые процедуры в маппинге сущностей можно для операций вставки, обновления и удаления. В EF beta3 эти возможности уже поддерживаются дизайнером EDM. Однако для выборки все равно нужен маппинг на таблицы или view.

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

Создадим таблицу Category с двумя полями Id (первичный ключ) и Title:



CREATE TABLE Category(
Id int IDENTITY(1,1) NOT NULL,
Title nvarchar(50) NOT NULL,
CONSTRAINT PK_Category PRIMARY KEY CLUSTERED (Id ASC)



Обратите внимание, что ключевое поле Id объявлено как IDENTITY, это повлияет на некоторые детали маппинга.

Далее нам необходимо создать три хранимые процедуры для вставки, обновления и удаления записей в таблице Category:



CREATE PROCEDURE [dbo].[CreateCategory]
@Title nvarchar(50)AS
BEGIN
INSERT INTO [Category] ([Title])
VALUES(@Title)

SELECT [Id] from [Category] where @@ROWCOUNT > 0 and [Id] = scope_identity()
END

CREATE PROCEDURE [dbo].[UpdateCategory]
@Id int,
@Title nvarchar(50)AS
BEGIN
UPDATE [Category]
SET [Title] = @Title
WHERE [Id] = @Id
END

CREATE PROCEDURE [dbo].[DeleteCategory]
@Id int as
BEGIN
DELETE [Category]
WHERE [Id] = @Id
END



Из-за того, что столбец Id у нас объявлен как Identity, в процедуру CreateCategory передается только один параметр @Title, иначе надо было бы передавать еще и @Id. По этой же причине в процедуре присутствует select, который возвращает значение Id только что добавленной строки. И наконец, по этой же причине, в процедуре UpdateCategory параметр @Id используется исключительно для поиска нужной записи.

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

Теперь займемся маппингом. В левом углу панели «Mapping details» есть две кнопки, которые переключают режим работы панели: «Map Entity to Tables / Views» и «Map Entity to Functions». До сих пор мы пользовались только первым режимом. Теперь нам надо переключиться на второй:



В пустой панели предлагается выбрать три функции для Insert, Update и Delete. Выбираем функции в выпадающих списках. Затем мапим параметры. Для функции CreateCategory необходимо выполнить Result Column Bindings. Для этого на месте надписи <Add Result Binding> вводим «Id» и мапим его на свойство Id. Таким образом, EF сможет получать значение Id вставленной записи.

Для параметров функции Update мы видим флажки «Use Original Value». При помощи этого флажка мы говорим, что EF должен взять оригинальное, а не измененное значение поля Id и передать его в функцию UpdateCategory. Почему так, я думаю, вы сами догадались.

Остается добавить, что «Result Column Bindings» есть и для функции Update. В нашем случае он не используется. Однако можно представить себе ситуацию, когда хранимая процедура не только сохраняет переданные в параметрах значения в БД, но руководствуясь какой-то своей логикой, еще и изменяет их. В этом случае новые значения можно вернуть через «Result Column Binding». Это, конечно, кошмарный сценарий, но в жизни бывает всякое.

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



Избавиться от него не получится. EF для выборок будет использовать только его. Единственное, что можно сделать, это заменить таблицу на view.

Ассоциация «многие ко многим»



Все мы знаем, что объектная ассоциация многие ко многим в реляционных структурах реализуется посредством промежуточной таблицы и двух ассоциаций «один ко многим». Вот я и решил посмотреть, как с этой задачей справится EF. Для этого я сделал вот такую БД с клиническим классическим примером «студенты – курсы»:



После работы визарда получилась вот такая EDM модель:



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

Заключение



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

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

  • при маппинге столбцов необходимо устанавливать соответствие свойству и нельзя вместо свойства использовать константу

  • нет способа запретить создание навигационных свойств на одном из концов ассоциации.

  • нет возможности маппировать класс только на хранимые процедуры без использования таблиц и view.

  • не все возможности маппинга поддерживаются дизайнером EDM.

вторник, января 29, 2008

Entity Framework - mapping (часть 1)

В прошлых статьях я рассказывал про EDM и теперь приступаю к наиболее интересной ее части Mapping Specification. С того момента, как я взялся за описание этой темы, количество материала непрерывно увеличивалось, и сегодня я понял, что в один пост это все не влезет. Возможности маппинга в EF обширны. Я буду рассматривать их от простых к более сложным на конкретных примерах. Сегодня мы рассмотрим мапинг класса на несколько таблиц, маппинг ассоциаций и варианты маппинга при наследовании классов.

Один класс - одна таблица


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

Один класс - несколько таблиц


В маппинг EDM сущности (здесь и далее сущность и класс - это синонимы) изначально заложена такая возможность. Рассмотрим БД, в которой две таблицы БД Contact и Person:

В таблице Contact хранятся контактные данные людей. Для некоторых из них хранится дополнительная информация в таблице Person. Таблицы связаны отношением один к одному по первичным ключам. Наша задача, получить EDM сущность Person, данные которой хранятся в этих двух таблицах.
Для начала создадим EDM по данной БД при помощи визарда VS2008.

Согласитесь, получилось не совсем то, что надо, вернее, совсем не то. Нам не нужна ссылка на Contact из Person. Нам надо, чтобы все поля Contact были в Person. Поэтому, удаляем Contact из модели, а в Person добавляем Scalar Properties: FirstName, LastName, Email, Phone. Теперь переходим к маппингу, кликнув в контекстном меню дизайнера команду “Mapping details”, и выделив Person на диаграмме:

То же самое, но в Xml выглядит вот так:


<EntitySetMapping Name="Person">
<EntityTypeMapping TypeName="IsTypeOf(test1Model.Person)">
<MappingFragment StoreEntitySet="Person">
<ScalarProperty Name="PersonId" ColumnName="PersonId" />
<ScalarProperty Name="BirthDate" ColumnName="BirthDate" />
<ScalarProperty Name="Address" ColumnName="Address" />
</MappingFragment>
</EntityTypeMapping>
</EntitySetMapping>


“Maps to Person” в дизайнере соответствует Mapping Fragment в Xml описании EDM. Мапппинг любой сущности (EntityType) может состоять из нескольких фрагментов. Каждый фрагмент – это одна таблица в БД. В данный момент у нас выполнен маппинг только свойств, соответствующих таблице Person. Чтобы выполнить маппинг свойств добавленных в ручную жмем в дизайнере <add> и выбираем таблицу Contact. Дизайнер добавляет новый фрагмент и автоматически устанавливает соответствие между колонками таблицы и свойствами сущности, ориентируясь при этом на имена и типы данных:

Дизайнер все сделал замечательно, но у нас осталась без соответствия колонка Contact.ContactId. Устанавливаем ему в соответствие свойство PersonId, которое является ключом сущности Person. Все, теперь у нас данные сущности Person будут храниться в таблицах Person и Contact. Если вы что-то сделали не так, и вам надо удалить фрагмент маппинг свойства или фрагмент маппинга (таблицу) не ломайте кнопку Del на клавиатуре, не поможет. Для удаления элемента в дизайнере маппинга нужно в его выпадающем списке выбрать опцию <delete>. В первый раз я потратил на это пять минут :)

Итак, для создания описанного типа маппинга «один класс - несколько таблиц», при котором данные экземпляра сущности «размазаны» по нескольким таблицам, необходимым условием является наличие первичных ключей во всех участвующих таблицах, и наличие отношения «один к одному» между ними (причем наличие в БД явно прописанных foreign keys не обязательно). Если теперь выполнить объектный запрос к EntitySet Person, то SQL трассировщик покажет нам следующий SQL запрос сформированный EF:


SELECT
[Extent1].[PersonId] AS [PersonId],
[Extent1].[BirthDate] AS [BirthDate],
[Extent1].[Address] AS [Address],
[Extent2].[FirstName] AS [FirstName],
[Extent2].[LastName] AS [LastName],
[Extent2].[Email] AS [Email],
[Extent2].[Phone] AS [Phone]
FROM [dbo].[Person] AS [Extent1]
INNER JOIN [dbo].[Contact] AS [Extent2] ON [Extent1].[PersonId] = [Extent2].[ContactId]


Обратите внимание на INNER JOIN. Он означает, что в выборку могут попасть не все записи таблицы Contact. Если вы помните, то в БД таблица Person содержит foreign key constraint на таблицу Contact. Это означает, что любой записи Person соответствует запись Contact. Однако обратное утверждение не верно, и в таблице Contact могут быть записи, для которых нет соответствия в таблице Person. Объектным запросом к EntitySet Person мы эти записи выбрать не сможем. Имейте это в виду.

Наследование, или одна таблица - несколько классов (TPH)


TPH – это Table per Hierarchy. Таким термином в EF окрестили вариант маппинга, когда данные нескольких сущностей хранятся в одной таблице БД, а сущности (или классы) представляют собой иерархию, связанную отношением наследования. Звучит довольно сложно, но выглядит все очень просто. Рассмотрим пример, в котором снова используем многострадальную таблицу Person.


Теперь мы собираемся хранить в таблице Person данные о студентах и инструкторах. Id является первичным ключом таблицы, колонки FirstName, LastName, Email - общие для студентов и инструкторов. Для студентов также храним дату зачисления - EnrollmentDate, а для инструкторов дату приема HireDate, размер зарплаты Salary и ссылку на подразделение в котором числится инструктор - DepartmentId. Я специально не стал делать foreign key constraint на поле DepartmentId, чтобы показать, как EF обходится без них при создании ассоциаций сущностей. И, наконец, в колонке PersonKind мы будем хранить 0 для студентов и 1 для инструкторов. Этой колонке предстоит сыграть большую роль в построении TPH маппинга.

По представленной базе нам надо построить вот такую модель:



Для этого сначала воспользуемся визардом. Затем уберем в сущности Person лишние поля и переименуем ее в PersonBase. Это будет базовый класс (сущность) для нашей иерархии. Далее мы создаем две сущности унаследованные от PersonBase: Student и Instructor. Для этого в диалоге “New Entity” мы выбираем PersonBase в списке “Base Type”. Добавляем свойства в новых сущностях, в Student - EnrollmentDate, а в Instructor - HireDate и Salary. Теперь самое интересное - маппинг. Для сущности Instructor добавляем маппинг на таблицу Person:


Нужные столбцы замаплены автоматом. Как мы помним, данные об инструкторах у нас содержат строки таблицы, в которых PersonKind = 1. Этот столбец в терминах EF называется столбцом дискриминатором (discriminator column), а для маппинга мы задаем условие (condition). Условие накладывается именно на столбец дискриминатор. Для сущности Instructor мы устанавливаем условие PersonKind = 1, а для Student соответственно PersonKind = 0. Если мы попробуем сейчас скомпилировать всою модель, то получим ошибку примерно такого содержания:

Error 3023: Problem in Mapping Fragment(s) starting at line(s) (122, 130, 136, 137): Column Person.PersonKind has no default value and is not nullable. A default value is required to store some of the EDM states.

EF говорит, что ему не понятно, что же делать с PersonKind для сущности PersonBase. Оно не имеет маппинга и значения по умолчанию, а в БД описано как NOT NULLABLE. В предыдущих CTP для разрешения этой проблемы приходилось вводить условие в маппинге базового класса. Но в нашей стрктуре не придусмотрена возможность хранения каких-то персон, кроме студентов и инструкторов. Студентам предназначено значение PersonKind=0 а инструкторам PersonKind=1. В beta3 эта проблема решена очень красиво. В свойствах сущности PersonBase мы можем указать специальное свойство «Inheritance Modifier» - abstract. Оно указывет на то, что класс PersonBase будет объявлен как абстрактный, а это именно то, что нам нужно.

Заметьте, что столбец PersonKind нигде не замаплен в виде свойства. Тем не менее, его значения будут заданы автоматически при сохранении экземпляров Student и Instructor. В этом смысле Condition можно рассматривать как разновидность маппинга.

Теперь рассмотрим, какие возможности существуют для задания условий. Прежде всего, мы можем задавать условия по нескольким столбцам. Для сравнения может быть использован оператор «=» или оператор «is null» или оператор «is not null». Если мы используем оператор «=», то для сравнения используется константа. Никаких «больше» или «меньше», никаких диапазонов выражений и т.д. Эти ограничения следует учитывать при дизайне модели. В дизайнере маппинга в EF beta3 присутствует баг связанный, с заданием условий. При задании условия PersonKind = 1 значение «1» записывается в edmx файл в каком то закодированном значении. Если при компиляции модели вы получаете сообщение об ошибке, подобное этому:

Error 2016: The value specified for the condition is not compatible with the type of the Member.

то вам надо открыть edmx файл в XML редакторе и исправить значение атрибута Value элемента Condition(<Condition ColumnName="PersonKind" Value="_x0031_" >)
Вместо “_x0031_” должно быть “1”. После этого сохраняем файл и вновь открываем его в дизайнере.

Теперь несколько слов о реализации отношений наследования. Сущности наследники не имеют своих EntitySet и принадлежат EntitySet базовой сущности. Это означает, что в классе ObjectContext нашей модели не будет свойств коллекций, соответствующих Student и Instructor. Как же тогда нам делать выборки судентов и инструкторов? Для этих целей существует метод ObjectQuery.OfType(). Используется он следующим образом (выбираем всех инструкторов):


using (TPH context = new TPH())
{
ObjectQuery<Instructor> q = context.Persons.OfType<Instructor>();
foreach (Instructor i in q)
{
if (!i.DepartmentReference.IsLoaded)
i.DepartmentReference.Load();
Console.WriteLine("Instructor: {0} {1} dep. {2}", i.FirstName, i.Lastname, i.Department.Title);
}
}



Ассоциации



В нашей модели есть еще и ассоциация между Instructor и Department, которую мы создали руками, поскольку соответствующего constraint в БД не было. Рассмотрим создание ассоциации более подробно. Для создания ассоциации используем команду контекстного меню дизанера «Add association».



В появившемся диалоге указываем сущности (Entity) между, которыми устанавливается ассоциация, мощность отношения (Multiplicity), и навигационные сойства (Navigation Property), которые будут добавлены в сущности. Тут главное, не запутаться в концах ассоциации :). В этом плане очень помогает информационное поле в нижней части окна, в котором ассоциация описывается словами. Внимательно читайте эти описания и сопоставляйте их с тем, чего вы хотите добиться. Изменяйте параметры концов ассоциации, пока не добъетесь нужного результата. В нашем случае необходимо, чтобы Instrictor имел ссылку на 0 или 1 экземпляр Department, а Department ссылался на * (несколько) экземпляров Instructor посредством специального свойства - коллекции. Заметьте, что в зависимости от мощности отношения на конце ассоциации, соответствующее навигационное свойство будет представлять собой ссылку либо коллекцию. Для большей семантической ясности я рекомендую использовать множественное число для имен свойств – коллекций (Department.Instructors), хотя дизайнер по умолчанию дает имена в единственном числе всем navigation properties.

Итак, ассоциация создана, но это еще не все. Для ассоциаций, как и для сущностей в EF необходимо указывать маппинг. Поскольку в ассоциации участвуют две таблицы, в нашем случае это Department и Person, то какую из них выбирать для марппинга? Ответ очевиден, ту, в которой размещено поле внешнего ключа. В нашем случае это таблица Person и поле DepartmentId. А сам маппинг ассоциации выглядит так:



С ассоциациями EF связанна одна неприятная особенность. Как вы заметили, навигационные свойства создаются для обеих сущностей, участвующих в ассоциации. Причем способа отменить создание навигационного свойства на одном из концов ассоциации не существует (во всяком случае я его не обнаружил). Однако многие ассоциации имеют однонаправленный характер. Типичный пример, справочник валют. В большой системе десятки сущностей могут иметь ссылки на справочник валют, некоторые и не одну. Причем самомой сущности «справочник валют» вовсе не интересно, кто там на нее ссылается. В EF ваш справочник валют ощетинится десятками свойств коллекций, вовсе ему не нужных.

Наследование, или таблица для каждого класса (TPT)



Рассмотрим вариант реализации наследования, в котором каждому классу соответствует отдельная таблица в БД. В терминах EF данный вариант называется Table per Type (TPT).

Вернемся к схеме БД, которую мы использовали в самом начале статьи. В ней были две таблицы Contact и Person, связанные отношением один к одному. Кроме того PK таблицы Person одновременно является FK на таблицу Contact, поскольку предпологается, что в таблице Person будут храниться расширенные данные для некоторых контактов.

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

Обратимся опять к модели, которую создал визард по схеме БД.


Для реализации задуманного нам необходимо удалить ассоциацию FK_Person_Contact и вместо нее установить между двумя сущностями отношение наследования. Для этого надо выполнить команду контекстного меню «Add Inheritance…» и в появившемся диалоге указать родительскую и дочернюю сущность. После того как отношение наследования установлено, мы обнаруживаем, что маппинг дочерней сущности (Person) исчез и его необходимо выполнить заново. Это не баг, потому что мы уже знаем, что иерархии наследования в EF могут маппироваться различными способами.


Обратите внимание, что сущность Person не содержит первичного ключа, как и все унаследованные сущности. Свойство PersonId мы также удалили за ненадобностью (у нас есть ключ в родительском классе). Маппинг Person тоже предельно прост:


Добавляем таблицу Person. Никаких условий в нашем случае использовать не надо. Обратите внимание, осиротевший столбец PersonId маппим на свойство родительского класса ContactId.

Интересно взглянуть на SQL запрос, который генерируется при выборке по EntitySet Contact, т.е. по базовому классу:



SELECT
CASE WHEN ( NOT (([Project1].[C1] = 1) AND ([Project1].[C1] IS NOT NULL))) THEN '0X' ELSE '0X0X' END AS [C1],
[Extent1].[ContactId] AS [ContactId],
[Extent1].[FirstName] AS [FirstName],
[Extent1].[LastName] AS [LastName],
[Extent1].[Email] AS [Email],
[Extent1].[Phone] AS [Phone],
CASE WHEN ( NOT (([Project1].[C1] = 1) AND ([Project1].[C1] IS NOT NULL))) THEN CAST(NULL AS datetime) ELSE [Project1].[BirthDate] END
AS [C2],
CASE WHEN ( NOT (([Project1].[C1] = 1) AND ([Project1].[C1] IS NOT NULL))) THEN CAST(NULL AS nvarchar(200)) ELSE [Project1].[Address]
END AS [C3]
FROM [dbo].[Contact] AS [Extent1]
LEFT OUTER JOIN (SELECT
[Extent2].[PersonId] AS [PersonId],
[Extent2].[BirthDate] AS [BirthDate],
[Extent2].[Address] AS [Address],
cast(1 as bit) AS [C1]
FROM [dbo].[Person] AS [Extent2] ) AS [Project1] ON [Extent1].[ContactId] = [Project1].[PersonId]


Тут мы видим нечто удивительное. В запросе по базовому классу тянутся данные принадлежащие классу наследнику, а также хитрые ‘0X0X’, которые можно интерпретировать, как дискриминатор типа. Это заставляет нас предположить, что экземпляры, выбранные по запросу к базовому классу могут быть приведены к классам наследникам. Проверка подтверждает это предположение:


using (TPT context = new TPT())
{
ObjectQuery<Contact> q = context.Contact;
foreach (Contact c in q)
{
if(c is Person)
Console.WriteLine("person {0} {1}", c.LastName, ((Person)c).Address);
else
Console.WriteLine("contact only {0}", c.LastName);
}
}


Выражение (c is Person) возвращает true для тех контактов, у которых есть данные в таблице Person. Причем приведение к классу наследнику не порождает подчиненных запросов, подтягивающих дополнительные данные, при инстанциации экземпляров сразу используется нужный тип и он заполняется всеми данными. По полиморфизму зачет.

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

На сегодня все.
В следующей части я расскажу о том, что такое MEST и чего не может дизайнер EDM.

пятница, января 25, 2008

Side effect

Работаю на виртуальной машине Microsoft Virtual PC 2004. Гостеавя ОС Windows 2003 SP1, родная - Windows XP SP2. Неожиданно вспоминаю, что надо бы подзарядить смартфон (Windows Mobile 5.01) и заодно засинхронизировать рабочий календарь Outlook и мобильника. Втыкаю USB кабель в смартфон, и... через несколько секунд вываливаюсь из виртуальной машины в родительскую OC - запустился Microsoft ActiveSync 4.2.0.

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

Вот это side effect! За протфельчик конечно спасибо, но я даже боюсь себе представить, каким образом виртуальная машина смогла среагировать на ActiveSync...

четверг, января 24, 2008

Глоссарий проекта

Вы ведете глоссарий проекта? Вы даже не знаете что это такое? А зачем он нужен?

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

Замечали, когда, вы приходите в проектную команду, которая довольно давно работает над проектом, то первое время вы чувствуете себя несколько неуютно из-за того, что люди вокруг вас говорят на каком-то особенном, своем языке. Они используют названия модулей, краткие названия каких-то фич, состояний, ситуаций, форм, ролей, документов, мэйлстоунов и много еще чего, что для свежего уха превращается в какой-то совершенно не понятный «птичий язык». Это все - глоссарий проекта. Он существует независимо от того, фиксируете вы его в документе или нет. Он появляется в первые дни работы над проектом, когда появляется первое рабочее название будущей системы, и очень активно формируется в последующие дни, во время бесед с заказчиками и обсуждений внутри команды. Фиксировать его на бумаге или нет - ваше дело. Но я смею утверждать, что глоссарий проекта это документ из тех, что больше нужен вам (проектной команде), чем заказчику, а потому сформировать его и поддерживать в рабочем состоянии весьма полезно.

Глоссарий входит в число необходимых документов во всех «тяжелых» методиках разработки (RUP, MSF). Но и для гибких (agile) методик, глоссарий, относится к вещам, которые лучше иметь, чем не иметь. Основополагающим принципом всех agile методик является поддержка внутри - командных коммуникаций на самом высоком уровне. Согласитесь, сложно общаться, не зная языка, а глоссарий – это и есть язык, на котором говорит команда. Для ведения глоссария вполне подходят wiki системы, хотя мне больше по душе простой лист бумаги.

Важная функция, которую должен выполнять глоссарий, это установление соответствия между терминами заказчика (предметной области) и терминами архитектуры системы. Это особенно важно для тяжелых методик, в которых проектные роли аналитика, архитектора, разработчика и тестировщика выполняют разные люди. Типичный пример – наименования состояний объектов. На уровне требований и пользовательского интерфейса они имеют свои имена. Когда мы спускаемся на уровень дизайна и реализации - те же вещи называются по-другому. Учитывая, что требования (use cases) и дизайн (design models) описаны в разных документах возникает путаница и всякие нелепые ошибки.

Что стоит включать в глоссарий? Реально глоссарий содержит всю специфическую для проекта терминологию, используемую в своей работе проектной командой. Вероятно, не стоит фиксировать на бумаге жаргонизмы, наподобие «финики» (роль пользователя системы - финансисты), и «юрики» (то же – юристы). Не место в глоссарии названиям проектных серверов, репозиториев и прочей технической информации.
В глоссарии желательно зафиксировать:
  • названия основных прецедентов использования, в форме понятной пользователю и заказчику системы, сопроводив их небольшим (не больше одного предложения) описанием. Это нужно, в первую очередь разработчикам (как ни странно)

  • названия всех понятий (сущностей) предметной области (домена приложения) в форме понятной пользователям и названия соответствующих им классов (объектов, модулей), если такое соответствие существует.

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

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

  • названия основных модулей и блоков системы, названия подсистем и их назначение

  • названия состояний сущностей системы и соответствующие им системные названия.

  • синонимы. Важно отразить, что для обозначения одного и того же понятия могут использоваться разные названия.

  • роли пользователей

В чем отличие Thread.Sleep() и Thread.SpinWait()

В чем отличие Thread.Sleep() и Thread.SpinWait()? Оба метода позволяют приостановить исполнение текущего потока. MSDN немногословна:
Thread.Sleep
Blocks the current thread for the specified number of milliseconds.

Thread.SpinWait
Causes a thread to wait the number of times defined by the iterations parameter.

После таких описаний, ИМХО, становится еще непонятной.
Между тем различие этих методов весьма велико. Thread.Sleep() не только блокирует текущий поток, но и сообщает планировщику потоков Windows (scheduler) о том, что текущий поток освобождает причитающийся ему квант процессорного времени (time slice), поэтому scheduler может передать этот квант следующему потоку в очереди. Если вы передали в Thread.Sleep() достаточно большое значение, то ваш поток на протяжении всего этого промежутка практически не будет потреблять процессорное время. При обходе очереди потоков, ожидающих своего кванта процессорного времени, планировщик будет просто пропускать ваш поток. Это делает метод Thread.Sleep() весьма полезным, например, для борьбы с явлением известным под названием "инверсия приоритетов", когда потоки с низким приоритетом, но "жадные" до ресурсов, вытесняют потоки с более высоким приоритетом.
Еще один интересный момент. Знаете ли вы, что обозначает вызов Thread.Sleep(0)? Поток вообще не заснет, или передаст управление другому потоку? MSDN говорит:
A value of zero causes the thread to relinquish the remainder of its time slice to any other thread of equal priority that is ready to run. If there are no other threads of equal priority ready to run, the function returns immediately, and the thread continues execution.

Т.е. остаток кванта времени, причитающегося потоку будет отдан другому потоку ожидающему исполнения, с тем-же приоритетом что и у текущего, а если такого потока нет, то текущий поток продолжит работу. Т.е. бороться с инверсией приоритетов при помощи Sleep(0) вообще бессмысленно. Джо Даффи призывает вовсе избегать использования Sleep(0), и даже завести правило в FxCop на этот счет.

Метод Thread.SpinWait() ведет себя совсем по другому. Главное отличие от Thread.Sleep() состоит в том, что здесь не происходит переключение контекста потока. Т.е. текущий поток не передает управление планировщику Windows. Вместо этого, внутри метода SpinWait() запускается некий холостой цикл, на что прозрачно указывает название метода. Число итераций этого передается в параметре метода. Отсюда можно сделать вывод, что время ожидания SpinWait() будет очень незначительным, даже по сравнению с вызовом Sleep(1). Ведь, согласно утверждению Джо Даффи, только переключение контекста потоков занимает 4000+ процессорных тактов.
Для чего стоит применять метод SpinWait()? В основном, в случаях когда потоку необходимо "немножко подождать", не передавая при этом управления другим потокам (понятно, что за время исполнения SpinWait планировщик может прервать текущий поток, но SpinWait этого "не заметит"). Обычно SpinWait() применяют в "тонких" техниках неблокирующих алгоритмов, когда ценой потери нескольких сот тактов мы можем избежать переключения контекста при явной блокировке на мониторе или вызове Sleep(), а также связанных с ними проблем обновления кэша и т.д. Т.е. SpinWait применяется довольно редко. Добавлю, что SpinWait дает хорошие результаты на много процессорных конфигурациях, и еще, его поведение отличается стабильностью и не зависит от аппаратной конфигурации (один процессор, HyperThread, много ядер).

вторник, января 22, 2008

Фигура из трех пальцев

Что-то давно ничего не было под ярлыком "Just fun". Хотите приколоться? Прямо сейчас. Нажмите на своей клавитатуре
Ctrl-Alt-Down Arrow
Сколько работаю в Windows а такого не видел :)

понедельник, января 21, 2008

TDD Anti-patterns от Джеймса Карра

Джеймс Карр (James Carr) в своем блоге опубликовал пост "TDD Anti-patterns" с кратким перечнем типичных ошибок допускаемых при написании модульных тестов. Ошибки. характерные не только для TDD, а для модульных тестов в принципе. Список показался мне настолько интересным, что я позволю себе представить его в своем вольном пересказе на русском.

Анти-патерны модульного тестирования.

Лжец (The Liar). Тест, который успешно проходит во всех случаях. Однако при ближайшем рассмотрении оказывается, что фактически ничего не тестирует.

Попробуй запусти! (Excessive Setup). Тест, который требует сложной и длительной настройки тестового окружения перед запуском. Иногда тест содержит сотни строк кода, подготавливающие необходимые условия для запуска одного теста. В результате очень сложно разобраться действительно тест не прошел, либо что-то случилось с настройками.

Гигант (The Giant). Огромный тест выполняющий сотни проверок и содержащий множество тест кейсов. Обычно это признак того, что мы имеем дело с God Object

Потемкинские деревни (The Mockery). Модульный тест содержит такое количество моков (mocks) стабов (stubs) и фэйков (fakes), что в самом тесте проверяется не столько тестируемая система, сколько поведение самих моков стабов и результаты их работы.

Инспектор (The Inspector). Модульный тест готов изнасиловать тестируемый класс в попытках добраться до самых приватных его членов, чтобы обеспечить 100% покрытие кода. Любая попытка рефакторинга тестируемого класса приводит к переделке теста.

Щедрые подачки (Generous Leftovers). Ситуация, когда один тест генерирует тестовые данные и сохраняет их где либо, а другой тест пытается этими данными воспользоваться. В результате если первый тест исполняется позже второго или не исполняется вовсе то второй валится.

Местный герой (The Local Hero). Тест написан так, что исполняется только в конкретном окружении, например, только на машине разработчика. Попытка выполнить тест в другом окружении (на сервере сборки, к примеру) ведет к неудаче.

Педант (The Nitpicker). Тест проверяет по шаблону весь выходной поток данных и валится при малейших изменениях в нем, в то время, как значимой для тестирования является лишь небольшая часть данных. Типичный обитатель тестов web приложений.

Тайнос агентос (The Secret Catcher). Тест на первый взгляд ничего не проверяет. На самом деле, тест рассчитывает на то, что при его исполнении не возникнет определенное исключение. А уж если оно возникнет, то тестовый фреймворк отрапортует об этом.

Лентяй (шланг) (The Dodger). Тест который тестирует множество побочных эффектов тестового случая вместо того чтобы тестировать требуемое поведение (обычно из-за того что побочные эффекты протестировать легче).

Болтун (The Loudmouth). Модульный тест заваливает консоль диагностическими сообщениями, логами и прочей чепухой, даже в случае успешного прохождения. Обычно это следствие того, что разработчик отлаживал свой код при помощи этого теста, однако не удалил свои отладочные сообщения после того, как они стали не нужны.

Проглот (The Greedy Catcher). Тест, который проглатывает исключения в исходном коде, иногда вместе с трассировкой стека, заменяя их своими, менее информативными исключениями, либо вовсе выбрасывая диагностическое сообщение (см. Болтун) и завершаясь успешно.

Секвенсор (The Sequencer). Тест, который проявляет чувствительность к последовательности элементов несортированного списка (либо, например, к последовательности выполнения тестов).

Скрытая зависимость (Hidden Dependency). Близкий родственник «Местного героя». Тест, который проявляет зависимость от данных которые должны быть созданы где-то и кем-то до того как тест будет исполнен. Если нужных данных нет, тест валится с каким нибудь невнятным сообщением без всякого намека на то, что же ему нужно было для успешного завершения. И правильно, заставим их продираться через гектары кода в поисках того, что же нужно этому тесту для работы.

Счетчик (The Enumerator). Тест, каждый метод в котором именуется просто: test1, test2, test3 и т.д. В результате абсолютно не понятно, что где тестируется и единственный способ узнать это – смотреть код теста.

Посторонний (The Stranger). Тестовый метод, который тестирует что-то, не имеющее никакого отношения к тому, что тестирует остальной модульный тест. Иногда это случается из-за того что надо протестировать другой объект, связанный с тестируемым объектом, но делается это в отрыве от означенной связи.

ОС евангелист (The Operating System Evangelist). Тест, который полагается на то, что будет исполняться под конкретной операционной системой. Хорошим примером может служить тест, использующий в асерте последовательность символов перевода строки для Windows, он свалится при исполнении в Linux.

Всем врагам на зло (Success Against All Odds). Тест написанный по принципу «pass first» вместо «fail first», т.е. он пройдет успешно даже если тестируемого класса еще нет в природе (см. также "Лжец").

Заяц (The Free Ride) Вместо того, чтобы писать новый тестовый метод, добавляем еще один асерт к уже существующему. И все дела…

Одиночка (The One). Комбинация нескольких паттернов, особенно «Гиганта» и множества «Зайцев». Тест содержит всего один метод, который тестирует всю функциональность тестируемого класса. Характерный признак – тестовый метод имеет такое же название как и тестовый класс, а в самом методе сначала долгая настройка а затем куча асертов.

Любопытная Варвара (The Peeping Tom). Тест, который благодаря разделяемым ресурсам, может «подсмотреть» результаты других тестов, и вследствие этого свалиться, несмотря на то, что тестируемая система находится в валидном состоянии. Часто такая ситуация возникает при использовании статических переменных для хранения данных, которые не были очищены предыдущими тестами. Также известен под именем «Незваный гость».

Не дождетесь! (Копуша) (The Slow Poke). Тест, который исполняется необычайно медленно. Когда разработчик запускает такой тест, он может спокойно сходить в туалет, перекурить, или еще хуже когда он предпочитает запускать такой тест в конце рабочего дня перед уходом домой.

Entity Framework - ComplexType

Если вы воспользуетесь визардом создания Entity Data Model (EDM) в Visual Studio 2008 и натравите его на свою базу данных, то в результате получите модель наподобие этой:


В полученной модели мы видим сущности и связи между ними. Сущности содержат скалярные (Scalar properties) и навигационные (Navigation properties) свойства. Навигационные свойства соответствуют концам (Ends) связей и предствляют собой ссылки на другие сущности или коллекции сущностей, это зависит от мощности соответствующего конца связи. Скалярные свойства могут иметь простой (Simple type) или комплексный (Complex type) тип. Простые типы в EDM, это строки, массив байт (binary), Boolean, DateTime, Guid, Decimal, а также стандартный набор числовых типов (знаковые и беззнаковые целые различной размерности, и числа с плавающей точкой). Для некоторых типов при помощи атрибутов определяются фасеты, призванные обеспечить правильное соответствие типов при маппировании (например, для строк можно определить фиксированную или переменную длину). Кстати, плохая новость, перечисления (enum) не входят в перечень поддерживаемых типов! Проблема довольно легко решается. Поскольку все генерируемые дата-классы EF представляют собой partial классы, довольно просто создать свойство обертку с типом enum над соответствующим скалярным свойством. Поддержку enum обещают… в следующей версии EF.

А теперь вспомним о том, что я сказал немного выше. Скалярные свойства сущностей могут иметь комплексный тип. Не пытайтесь создать или найти ComplexType на диаграмме EDM. ComplexTypes не поддерживаются инструментами Visual Studio. Рассмотрим вот такую сущность Person:


Четыре ее свойства (Site, StreeAddress, Region, PostalCode) предназначены для представления адреса человека. Вероятно, удобнее было бы работать с объектом типа Address для представления этих данных. Для этих целей как раз и используется ComplexType. Для того, чтобы создать ComplexType нам придется закрыть красивое, но увы, бесполезное в нашем случае окно дизайнера, и открыть заново edmx файл модели командой «Open with…» в Xml редакторе студии. Кстати, должен вам заметить, что создавать и редактировать свойства сущностей гораздо удобнее именно в Xml редакторе, интелисенс по Xsd схеме очень помогает. А вот редактировать ассоциации лучше в дизайнере, поскольку в этом случае необходимо вносить согласованные изменения в различные части edmx схемы.

ComplexType описывается в разделе ConceptualModels, там же где и EntityType. Опишем комплексный тип CAddress следующим образом:


<ComplexType Name="CAddress" >
<Property Name="Sity" Type="String" Nullable="true" MaxLength="50" />
<Property Name="StreetAddress" Type="String" Nullable="true" MaxLength="50" />
<Property Name="Region" Type="String" Nullable="true" MaxLength="50" />
<Property Name="PostalCode" Type="String" Nullable="true" MaxLength="6" FixedLength="true" />
</ComplexType>



А в EntityType Person заменим четыре свойства на одно Address типа CAddress:


<EntityType Name="Person">
<Key>
<PropertyRef Name="Id" />
</Key>
<Property Name="Id" Type="Int32" Nullable="false" />
<Property Name="FirstName" Type="String" Nullable="false" MaxLength="50" />
<Property Name="LastName" Type="String" Nullable="false" MaxLength="50" />
<Property Name="BirthDate" Type="DateTime" Nullable="false" />
<Property Name="Address" Type="Self.CAddress" Nullable="false" />
</EntityType>



Как видно, объявление свойства комплексного типа ничем не отличается от прочих. Просто указан тип свойства «Self.CAddress», здесь Self это алиас заданный для концептуальной модели. Атрибут Nullable="false" в нашем случае обязателен, это одно из ограничений. Теперь надо поправить маппинг для сущности Person:


<EntityTypeMapping TypeName="IsTypeOf(TestModel.Person)">
<MappingFragment StoreEntitySet="Person">
<ScalarProperty Name="BirthDate" ColumnName="BirthDate" />
<ScalarProperty Name="LastName" ColumnName="LastName" />
<ScalarProperty Name="FirstName" ColumnName="FirstName" />
<ComplexProperty Name="Address" TypeName="TestModel.CAddress" >
<ScalarProperty Name="Sity" ColumnName="Sity" />
<ScalarProperty Name="PostalCode" ColumnName="PostalCode" />
<ScalarProperty Name="Region" ColumnName="Region" />
<ScalarProperty Name="StreetAddress" ColumnName="StreetAddress" />
</ComplexProperty>
<ScalarProperty Name="Id" ColumnName="Id" />
</MappingFragment>
</EntityTypeMapping>



Здесь вместо ScalarProperty используется элемент ComplexProperty а в атрибуте TypeName указывается уже не «Self.CAddress», а «TestModel.CAddress», где «TestModel» это имя namespace заданное в концептуальной схеме. Не запутайтесь!
Теперь вместо четырех свойств в сущности Person у нас есть одно - Address типа CAddress. Можно снова открыть модель в дизайнере, ничего не сломается (во всяком случае, в beta 3), но тип CAddress вы не увидите и свойство Address лучше не редактировать в дизайнере (это же касается и маппинга).
Можно заметить, что объявление ComplexType и EntityType очень похожи. Почему бы нам не объявить для представления адреса EntityType вместо ComplexType? Между ними есть одно очень важное отличие. ComplexType не может быть самостоятельно замаплен на объкты в БД, он мапится только в составе других сущностей. Т.о. если бы для представления адреса в БД у нас использовалась отдельная таблица, нам следовало бы использовать EntityType и Navigation Property в сущности Person для отражения связи Person – Address.
В EF существует ряд ограничений на использование ComplexType:
  • Свойства комплексных типов не должны иметь значение null, иначе при сохранении изменений получим исключение

  • Нельзя наследовать complexType от другого ComplexType

  • Когда изменяется одно из всойств в ComplexType, при сохранении ComplexType обновляется целиком

  • При реализации класса для ComplexType разработчики настоятельно советуют наследовать его от ComplexObject :)

четверг, января 17, 2008

Entity Framework - что такое EDM?

Если вы решили познакомится с Entity Framework то термин Entity Data Model будет, пожалуй, первым, с чем придется столкнуться:

В основу архитектуры Entity Framework положены две основные концепции. Первая, и основная из них - Entity Data Model, вторая – Entity Client. Причем между ними есть некоторые противоречия. Как и почему они возникли - тема отдельного разговора, а сегодня я хочу рассказать об Entity Data Model (EDM).

EDM - это фундамент на котором возведено все здание Entity Framework (EF). EDM - это также та вещь, которая отличает EF от множества других ORM движков. Обычно ORM движки опираются на объектную модель приложения в виде классов, и предоставляют различные механизмы маппинга классов домена приложения на таблицы в реляционной БД. Для этих целей используются либо Xml манифесты (описания), либо характерный для .Net механизм атрибутов. Т.е. фактически обычно ORM движки используют метаданные в виде описания маппинга объектов на реляционные структуры. EF помимо описания маппинга вводит описание объектной модели, которое называется Conceptual Model, и реляционной модели - Storage Model. Таким образом EDM состоит из трех частей:
  • Conceptual schema

  • Storage schema

  • Mapping specification

Все это хранится в Xml файле с расширением edmx. Интересно, что судя по XML схеме, один edmx может содержать несколько экземпляров концептуальных схем, схем БД и спецификаций маппинга (только вот дизайнер в 2008 студии всего этого не поддерживает).

Для чего это сделано? Во-первых, во имя давней идеи отделения логического слоя домена приложения от слоя хранения данных. Действительно, в EF сущностях, которые представлены классами, генерируемыми по Conceptual Schema ничего не напоминает о БД. Кстати, атрибуты, которыми помечены эти классы и их свойства, предназначены вовсе не для обеспечения маппинга на БД, а для связи классов с сущностями концептуальной модели. В документации EF неоднократно подчеркивается, что концептуальная модель может разрабатываться отдельно от модели БД, и при этом несколько различных приложений каждое со своей концептуальной моделью могут маппироваться на одну и ту же модель БД (в рамках нескольких EDM, конечно). Справедливо и обратное: модель БД может изменяться отдельно от концептуальной модели (в определенных пределах, конечно).

Во-вторых, EF вводит дополнительный слой абстракции в виде концептуальной модели. Подобно тому, как WCF предлагает новую парадигму разработки «contract first», при следовании которой сначала определяется контракт сервиса и данных а затем их реализация, так EF предлагает парадигму «model first». Сначала разрабатываем концептуальную модель, которая суть модель домена приложения, а уже классы по этой модели автоматически будут сгенерированы инструментами EF. Концептуальная модель разрабатывается в привычных объектных терминах: сущности (entities), отношения наследования (inheritance and hierarchу), ассоциации (associations). Есть и специфические вещи, которые не дают нам забыть, что EF это все-таки ORM и где-то там внизу существует база данных. К таким вещам относится EntitySet и AssociationSet – логические контейнеры для сущностей и ассоциаций одного типа. Если сущности (entity) соответствует класс C# или тип, то EntitySet - это коллекция объектов. Важно отметить, что свой EntitySet полагается не каждой сущности. Для иерархии сущностей связанных наследованием существует только один EntitySet соответствующий корневой родительской сущности.

Для создания и редактирования EDM в VS2008 есть визард и дизайнер.

К сожалению, с дизайнером пока (beta3) связано большое количество багов. К тому же дизайнер поддерживает не все возможности EDM, в частности в дизайнере нельзя создать ComplexType и задать его в качестве Scalar Property сущности. Надеюсь это временные трудности.

Плохо другое. Как я уже говорил, наличие EDM вроде-бы ненавязчиво подталкивает нас к использованию сценария разработки «model first». Разрабатываем модель сущностей домена приложения и их связей, затем разрабатываем структуру БД ориентируясь уже на готовую модель домена и особенности конкретной БД, а затем описываем маппинг сущностей домена на объекты БД. А все исходники генерируются автоматически. Так вот, если вы попробуете так работать при помощи имеющихся инструментов VS2008, то у вас ничего не получится. Можно создать пустую модель при помощи визарда. Можно создать в нем сущности, их свойства и связи при помощи дизайнера, хотя это очень не удобно (в аналогичных дизайнерах SQL2005 все гораздо удобней). Но когда дело дойдет до описания схемы БД (Storage schema), вам придется иметь дело с голым XML. Существующий визард «Update model from Database» одновременно с загрузкой Storage schema удалит все незамапленные сущности, т.е. всю вашу работу. Поэтому пока более предпочтительным выглядит другой сценарий. Сначала создаете БД (или берете готовую). Натравливаете на нее визард создания EDM, а затем редактируете уже готовые сущности, описываете наследование сущностей (дизайнер это позволяет), редактируете свойства и правите маппинг.

Роль EDM не ограничивается design time и генерацией классов по xml описаниям сущностей в модели. EDM используется и в runtime. В ранних CTP соответствующие XML файлы помещались студией после компиляции проекта в TargetDir рядом с файлом сборки. В beta 3 появилась возможность указать в свойствах дизайнера EDM куда помещать метаданные: как и прежде в файлы, либо в ресурсы сборки. Первый вариант удобен тем, что позволяет изменить маппинг и схему БД без перекомпиляции сборки (до известных пределов конечно). Второй вариант позволяет избавиться от csdl ssdl и msl файлов при развертывании приложения. Способ размещения файлов влияет на формат строки подключения EF. Впрочем, ничто не мешает нам комбинировать оба спсоба: при компиляции разместить EDM в ресурсы сборки, а в последствии, при необходимости изменить, к примеру, маппинг можно выпустить отдельный msl файл и соответствующим образом изменить строку подключения.

На сегодня все. Подробный разбор формата отдельных частей EDM и способов маппинга (особенно) требует отдельного довольно объемного описания.