Доступ к данным

Запрет доступа к таблицам из Entity Framework без негативных последствий

Джули Лерман

Julie LermanЕдва ли не первое, что я слышала от владельцев баз данных, когда они видели в действии создание команды в Entity Framework: «Что? Я должен предоставлять доступ к своим таблицам?». Они реагировали так потому, что одна из основных возможностей Entity Framework — генерация команд SELECT, UPDATE, INSERT и DELETE.

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

Исследование генерации команд по умолчанию

Как работает генерация команд? Центральное место в Entity Framework отводится Entity Data Model (EDM) — концептуальной модели, описывающей объекты предметной области приложения. Entity Framework позволяет разработчикам выражать запросы к модели, а не к самой базе банных. Модель, ее сущности и их отношения определяются XML-кодом, а разработчики имеют дело со строго типизированными классами, основанными на сущностях модели. Для связывания классов с сущностями базы данных исполняющая среда Entity Framework использует XML модели в сочетании с дополнительными метаданными (которые описывают схему базы данных и сопоставления между этой схемой и моделью) (рис. 1).

Figure 1 The Entity Framework Runtime Metadata Is Used to Build Database Commands

Рис. 1. Метаданные среды выполнения Entity Framework используются для создания команд базы данных

В период выполнения с помощью специфичных для базы данных ADO.NET-провайдеров Entity Framework преобразует запросы к модели в запросы к хранилищу (например, в T-SQL), которые затем посылаются базе данных. Entity Framework преобразует результаты запроса в объекты, определенные строго типизированными классами сущностей, как показано на рис. 2.

Figure 2 The Entity Framework Executes Queries and Processes Their Results

Рис. 2. Entity Framework выполняет запросы и обрабатывает их результаты

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

Описание этого поведения по умолчанию в Entity Framework нередко приводит к тому, что владельцы баз данных с воплями выбегают из аудитории, но я предпочла бы выделить здесь словосочетание «по умолчанию». В Entity Framework многие поведения по умолчанию можно изменять.

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

Сопоставление сущностей с представлениями базы данных, а не с таблицами

Создать модель можно несколькими способами. Я буду уделять основное внимание моделям, создаваемым на основе анализа существующей базы данных. Для такой задачи в Visual Studio есть специальный мастер.

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

Обычно выбирают таблицы, и мастер создает по ним сущности. В процессе отслеживания изменений и работы метода SaveChanges, о котором я уже говорила, Entity Framework автоматически генерирует команды INSERT, UPDATE и DELETE для сущностей на основе таблиц.

Давайте сначала рассмотрим, как заставить Entity Framework выдавать запросы к представлениям, а не к таблицам.

Представления базы данных, включаемые в модель, тоже становятся сущностями. Entity Framework отслеживает изменения в этих сущностях точно так же, как для сущностей, сопоставленных с таблицами. Однако при использовании представлений есть одна тонкость с идентификационными ключами. В таблице базы данных, весьма вероятно, есть один или более столбцов, помеченных как основной ключ или ключи этой таблицы. По умолчанию мастер сформирует идентификационный ключ сущности на основе основного ключа или ключей таблицы. При создании сущностей, сопоставленных с представлениями (в которых нет основных ключей), мастер прилагает максимум усилий, чтобы логически определить этот идентификационный ключ, создавая составной ключ из значений таблицы, которые не могут быть равны null. Рассмотрим сущность, созданную из представления, в котором есть четыре столбца, где null-значения недопустимы: ContactID, FirstName, LastName и TimeStamp.

Эти четыре конечных свойства будут помечены как EntityKey (дизайнер использует значок ключа для обозначения EntityKey-свойств), т. е. сущность получит EntityKey, созданный из этих четырех свойств.

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

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

После создания ключа Entity Framework может уникально идентифицировать каждую сущность и поэтому получает возможность отслеживать изменения в этих сущностях, а затем сохранять изменения в базе данных при вызове метода SaveChanges.

Переопределение процесса генерации команд с помощью собственных хранимых процедур

Для сохранения изменений в базе данных можно переопределить генерацию команд по умолчанию и заставить Entity Framework использовать свои хранимые процедуры Insert, Update и Delete. Этот прием называют сопоставлением хранимых процедур. Давайте посмотрим, как все это работает.

Любая хранимая процедура, отобранная вами в EDM Wizard (или впоследствии в Update Wizard) для модели, становится функцией в разделе XML-метаданных модели, описывающих схему базу данных. Она не делается автоматически частью концептуальной модели, и вы не увидите ее представление в рабочей области дизайнера.

Вот простая хранимая процедура Insert для таблицы Person в одной из моих баз данных:

ALTER procedure [dbo].[InsertPerson]
           @FirstName nchar(50),
           @LastName nchar(50),
           @Title nchar(50)
AS
INSERT INTO [Entity FrameworkWorkshop].[dbo].[Person]
           ([FirstName]
           ,[LastName]
           ,[Title]           )
     VALUES
(@FirstName,@LastName,@Title)
SELECT @@IDENTITY as PersonID

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

Когда вы выбираете эту процедуру в мастере, она представляется в схеме модели как следующая функция:

<Function Name="InsertPerson" Aggregate="false" BuiltIn="false"   
 NiladicFunction="false" IsComposable="false" 
 ParameterTypeSemantics="AllowImplicitConversion" Schema="dbo">
  <Parameter Name="FirstName" Type="nchar" Mode="In" />
  <Parameter Name="LastName" Type="nchar" Mode="In" />
  <Parameter Name="Title" Type="nchar" Mode="In" />
</Function>

После этого вы можете использовать окно Mapping Details в дизайнере для сопоставления этой функции InsertPerson с сущностью Person, созданной на основе таблицы Person, как показано на рис. 3.

Figure 3 Mapping Stored Procedures to an Entity

Рис. 3. Сопоставление хранимых процедур с сущностью

Заметьте, что на рис. 3 свойство PersonID сопоставлено со значением, возвращаемым хранимой процедурой. Это конкретное сопоставление заставит Entity Framework обновить объект Person в памяти ключом, генерируемым базой данных при выполнении в ней операции вставки.

Критическим требованием при сопоставлении функций является необходимость сопоставления всех параметров в функции со свойствами сущности. С параметрами нельзя сопоставить формулу или некое значение. Однако у разработчиков есть масса возможностей в конфигурировании классов Microsoft .NET Framework, представляющих эти сущности.

Вы также можете сопоставлять функции Update и Delete. Хотя необходимости в сопоставлении всех трех операций (Insert, Update и Delete) нет, разработчики должны обратить внимание на некоторые правила, описанные в документации и относящиеся к случаю сопоставления лишь некоторых из функций.

На рис. 3 справа от свойства есть еще два столбца, названия которых сокращены из-за нехватки места: Use Original Value и Rows Affected. Entity Framework поддерживает оптимистичную параллельную обработку (optimistic concurrency), и вы можете использовать эти атрибуты для проверки на параллельное выполнение функций Update и Delete. Подробнее об этом см. в MSDN документ «Walkthrough: Mapping an Entity to Stored Procedures (Entity Data Model Tools)».

В период выполнения, если пользователь создает новый тип Person, а затем инициирует вызов метода SaveChanges, Entity Framework ищет сопоставление функции Insert в метаданных (как определено на рис. 3). И вместо генерации собственной команды INSERT «на лету» посылает следующую команду, выполняющую хранимую процедуру:

exec [dbo].[InsertPerson] @FirstName=N'Julie',@LastName=N'Lerman',
@Title=N'Ms.'

Закрытие бреши и предотвращение доступа Entity Framework к таблицам

Entity Framework будет генерировать команды для сохранения данных из сущностей на основе представления, но представления могут оказаться не обновляемыми. В таком случае вы можете сопоставить хранимые процедуры Insert, Update и Delete с сущностями и обеспечить полный обмен данными, не предоставляя прямого доступа к таблицам базы данных.

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

В качестве примера таких возможностей приложение могло бы выдавать запрос к сущности CustomersInPastYear, дополнительно фильтруя представление по свойству LastName клиента:

from c in context.CustomersInPastYears
 where c.LastName.StartsWith("B")
 select c;

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

SELECT
[Extent1].[CustomerID] AS [CustomerID], [Extent1].[FirstName] AS [FirstName], 
[Extent1].[LastName] AS [LastName], [Extent1].[EmailAddress] AS [EmailAddress], 
[Extent1].[TimeStamp] AS [TimeStamp]
FROM (SELECT 
      [CustomersInPastYear].[CustomerID] AS [CustomerID], 
      [CustomersInPastYear].[FirstName] AS [FirstName], 
      [CustomersInPastYear].[LastName] AS [LastName], 
      [CustomersInPastYear].[EmailAddress] AS [EmailAddress], 
      [CustomersInPastYear].[TimeStamp] AS [TimeStamp]
      FROM [dbo].[CustomersInPastYear] AS [CustomersInPastYear]) AS [Extent1]
WHERE [Extent1].[LastName] LIKE N'B%'

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

На рис. 4 показана хранимая процедура, обновляющая таблицу Customer с использованием тех же полей, которые включены в представление Customers­InPastYear. Ее можно применять как функцию Update для сущности CustomersInPastYear.

Рис. 4. Хранимая процедура UpdateCustomerFirstNameLastNameEmail

ALTER PROCEDURE UpdateCustomerFirstNameLastNameEmail
@FirstName nvarchar(50),
@LastName nvarchar(50),
@Email nvarchar(50),
@CustomerId int,
@TimeStamp timestamp

AS

UPDATE Customer
   SET [FirstName] = @FirstName
      ,[LastName] = @LastName
      ,[EmailAddress] = @Email
 WHERE CustomerID=@CustomerId AND TimeStamp=@TimeStamp
 
 SELECT TimeStamp 
 FROM Customer
 WHERE CustomerID=@CustomerId

Теперь вы можете сопоставить эту хранимую процедуру с сущностью. Сопоставление, показанное на рис. 5, приводит к отправке исходного TimeStamp в хранимую процедуру, а затем с помощью Result Column Bindings к перехвату обновленного TimeStamp, возвращаемого хранимой процедурой.

Figure 5 Mapping a Stored Procedure to an Entity Based on a View

Рис. 5. Сопоставление хранимой процедуры с сущностью на основе представления

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

Джули Лерман  — Microsoft MVP, преподаватель и консультант по .NET, живет в Вермонте. Часто выступает на конференциях по всему миру и в группах пользователей по тематике, связанной с доступом к данным и другими технологиями Microsoft .NET. Ведет блог thedatafarm.com/blog и является автором очень популярной книги «Programming Entity Framework» (O’Reilly Media, 2009). Вы также можете встретить ее на Twitter.com: julielerman.

Выражаю благодарность следующим экспертам за рецензирование статьи: Ноам Бен-Ами (Noam Ben-Ami) и Срикант Мандади (Srikanth Mandadi)