LINQ to SQL: паттерн Repository

    Бар LINQВ этой статье будет рассмотрен один из вариантов реализации паттерна репозиторий на базе LINQ to SQL.

    Сегодня LINQ to SQL – это одна из технологий Microsoft, предназначенная для решения проблемы объектно-реляционного отображения (object-relational mapping). Альтернативная технология Entity Framework является более мощным инструментом, однако у LINQ to SQL есть свои преимущества – относительная простота и низкоуровневость.

    Данная статья — это попытка продемонстрировать сильные стороны LINQ to SQL. Паттерн репозиторий отлично ложится на парадигму LINQ to SQL.

    Репозиторий


    Для начала вспомним, что такое репозиторий.
    public interface IRepository<T> where T: Entity
    {
        IQueryable<T> GetAll();
        bool Save(T entity);
        bool Delete(int id);
        bool Delete(T entity);
    }


    * This source code was highlighted with Source Code Highlighter.

    Репозиторий – это фасад для доступа к базе данных. Весь код приложения за пределами репозитория работает с базой данных через него и только через него. Таким образом, репозиторий инкасулирует в себе логику работы с базой данных, это слой объектно-реляционного отображения в нашем приложении. Более точно, репозиторий, или хранилище, это интерфейс для доступа к данным одного типа – один класс модели, одна таблица базы данных в простейшем случае. Доступ к данным организуется через совокупность всех репозиторией. Обратите внимание, что интерфейс репозитория задается в терминах модели приложения: Entity – базовый класс для всех классов модели приложения (POCO-объекты).
    public abstract class Entity
    {
        protected Entity()
        {
            Id = -1;
        }

        public int Id { get; set; }

        public bool IsNew()
        {
            return Id == -1;
        }
    }


    * This source code was highlighted with Source Code Highlighter.

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

    Методы интерфейса IRepository обеспечивают полный набор CRUD-операций.

    GetAll – возвращает всю совокупность объектов данного типа, хранимых в БД. Фильтрация, сортировка и другие операции над выборкой объектов осуществляются на более высоком уровне, благодаря использованию интерфейса IQueryable<T>. Подробнее в разделе «Фильтры и конвейер».
    Save – сохраняет объект модели в базе данных. В случае, если он новый, выполняется операция INSERT, иначе – UPDATE.
    Delete – удаляет объект из базы данных. Предусмотрены два варианта вызова функции: с параметром id удаляемой записи и с параметром объектом класса модели приложения.

    Реализация


    Пусть у нас есть БД, состоящая из одной таблицы Customers.
    CREATE TABLE dbo.Customers
    (
        [Id] int IDENTITY(1,1) NOT NULL PRIMARY KEY,
        [Name] nvarchar(200) NOT NULL,
        [Address] nvarchar(1000) NULL,
        [Balance] money NOT NULL
    )

    * This source code was highlighted with Source Code Highlighter.

    Для начала добавим в проект файл dbml, в котором будут задаваться классы объектов модели базы данных и свойства их отображения. Для этого надо воспользоваться контекстным меню Solution Explorer (New Item…->Data->LINQ to SQL Classes) в Visual Studio. После появления окна дизайнера следует открыть Server Explorer и перетащить таблицу Customers в окно дизайнера. Вот что должно получиться:

    LINQ to SQL designer


    В результате, Visual Studio сгенерирует класс Customer модели базы данных. Модель самого приложения в общем случае отличается от модели базы данных, но в данном примере они практически совпадают. Ниже приведено описание класса Customer модели приложения:
    public class Customer : Entity
    {
        public string Name { get; set; }
        public string Address { get; set; }
        public decimal Balance { get; set; }
    }


    * This source code was highlighted with Source Code Highlighter.

    Пришло время заняться реализацией CustomersRepository – репозитория объектов типа Customer. Для того, чтобы избежать дублирования кода при создании репозиториев для других классов модели, большая часть функциональности вынесена в базовый класс.
    public abstract class RepositoryBase<T, DbT> : IRepository<T>
        where T : Entity where DbT : class, IDbEntity, new() 
    {
        protected readonly DbContext context = new DbContext();

        public IQueryable<T> GetAll()
        {
            return GetTable().Select(GetConverter());
        }

        public bool Save(T entity)
        {
            DbT dbEntity;

            if (entity.IsNew())
            {
                dbEntity = new DbT();
            }
            else
            {
                dbEntity = GetTable().Where(x => x.Id == entity.Id).SingleOrDefault();
                if (dbEntity == null)
                {
                    return false;
                }
            }

            UpdateEntry(dbEntity, entity);

            if (entity.IsNew())
            {
                GetTable().InsertOnSubmit(dbEntity);
            }

            context.SubmitChanges();

            entity.Id = dbEntity.Id;
            return true;
        }

        public bool Delete(int id)
        {
            var dbEntity = GetTable().Where(x => x.Id == id).SingleOrDefault();

            if (dbEntity == null)
            {
                return false;
            }

            GetTable().DeleteOnSubmit(dbEntity);

            context.SubmitChanges();
            return true;
        }

        public bool Delete(T entity)
        {
            return Delete(entity.Id);
        }

        protected abstract Table<DbT> GetTable();
        protected abstract Expression<Func<DbT, T>> GetConverter();
        protected abstract void UpdateEntry(DbT dbEntity, T entity);
    }


    * This source code was highlighted with Source Code Highlighter.

    Все классы модели LINQ to SQL имеют общий интерфейс IDbEntity:
    public interface IDbEntity
    {
        int Id { get; }
    }


    * This source code was highlighted with Source Code Highlighter.

    К сожалению, средства визуального дизайнера не позволяют указать базовый класс для объектов LINQ to SQL. Для этого необходимо открыть файл dbml в редакторе XML (Open with...) и указать атрибут EntityBase у элемента Database:
    <Database EntityBase="Data.Db.IDbEntity" ...>

    * This source code was highlighted with Source Code Highlighter.

    Далее приведено описание класса CustomersRepository.
    public class CustomersRepository : RepositoryBase<Customer, Db.Entities.Customer>
    {
        protected override Table<Db.Entities.Customer> GetTable()
        {
            return context.Customers;
        }

        protected override Expression<Func<Db.Entities.Customer, Customer>> GetConverter()
        {
            return c => new Customer
                            {
                                Id = c.Id,
                                Name = c.Name,
                                Address = c.Address,
                                Balance = c.Balance
                            };
        }

        protected override void UpdateEntry(Db.Entities.Customer dbCustomer, Customer customer)
        {
            dbCustomer.Name = customer.Name;
            dbCustomer.Address = customer.Address;
            dbCustomer.Balance = customer.Balance;
        }
    }


    * This source code was highlighted with Source Code Highlighter.

    Фильтры и конвейер


    Метод GetAll репозиториев возвращает объект, реализующий интерфейс IQueryable<T>. Это позволяет применять к выборке объектов операции фильтрации (метод Where), сортировки и любые другие операции, определенные над IQueryable<T>.
    Для удобства часто употребляемые операции могут быть вынесены в extension-методы. Например, фильтрация по имени клиента.
    public static IQueryable<Customer> WithNameLike(this IQueryable<Customer> q, string name)
    {
        return q.Where(customer => customer.Name.StartsWith(name));
    }


    * This source code was highlighted with Source Code Highlighter.

    Теперь мы можем использовать репозиторий следующим образом.
    IRepository<Customer> rep = new CustomersRepository();
    foreach (var cust in rep.GetAll().WithNameLike(“Google”).OrderBy(x => x.Name)) { … }


    * This source code was highlighted with Source Code Highlighter.

    Неважно, какой сложности фильтры или другие операции, которые мы используем. Неважно сколько их. В результате будет выполнен ровно один запрос к базе данных. Этот принцип называется отложенным выполнением запросов (deferred execution) – итоговый SQL-запрос генерируется и исполняется только в момент, когда требуется получить итоговую выборку. В данном случае, это происходит непосредственно перед выполнением первой итерации цикла foreach.
    Важное преимущество архитектуры – фильтры, как и всё приложение за исключением слоя репозиториев, работают над моделью приложения, а не над моделью базы данных.

    Анализ


    Далее проводится анализ генерируемых LINQ to SQL запросов к базе данных при выполнении той или иной операции над репозиторием.

    GetAll. В случае примера:
    rep.GetAll().WithNameLike(“Google”).OrderBy(x => x.Name)

    * This source code was highlighted with Source Code Highlighter.

    Делается единственный запрос:
    exec sp_executesql N'SELECT [t0].[Name], [t0].[Address], [t0].[Balance], [t0].[Id]
    FROM [dbo].[Customers] AS [t0]
    WHERE [t0].[Name] LIKE @p0
    ORDER BY [t0].[Name]'
    ,N'@p0 nvarchar(7)',@p0=N'Google%'


    * This source code was highlighted with Source Code Highlighter.

    Метод Save для нового объекта выполняет единственный запрос INSERT. Например:
    exec sp_executesql N'INSERT INTO [dbo].[Customers]([Name], [Address], [Balance])
    VALUES (@p0, @p1, @p2)
    SELECT CONVERT(Int,SCOPE_IDENTITY()) AS [value]'
    ,N'@p0 nvarchar(6),@p1 nvarchar(3),@p2 money',@p0=N'Google',@p1=N'USA',@p2=$10000.0000


    * This source code was highlighted with Source Code Highlighter.

    В случае вызова Save для существующего объекта или Delete выполняются два запроса. Первый – извлечение записи из базы данных. Например:
    exec sp_executesql N'SELECT [t0].[Id], [t0].[Name], [t0].[Address], [t0].[Balance]
    FROM [dbo].[Customers] AS [t0]
    WHERE [t0].[Id] = @p0'
    ,N'@p0 int',@p0=29


    * This source code was highlighted with Source Code Highlighter.

    Второй запрос – непосредственное выполнение операций UPDATE или DELETE, соответственно. Пример для DELETE:
    exec sp_executesql N'DELETE FROM [dbo].[Customers] WHERE ([Id] = @p0) AND ([Name] = @p1) AND ([Address] = @p2) AND ([Balance] = @p3)',N'@p0 int,@p1 nvarchar(6),@p2 nvarchar(3),@p3 money',@p0=29,@p1=N'Google',@p2=N'USA',@p3=$10000.0000

    * This source code was highlighted with Source Code Highlighter.

    В случае UPDATE и DELETE первый запрос является избыточным, однако без него не удастся сохранить или удалить объект, используя стандартные средства LINQ to SQL.
    Один из вариантов избавления от ненужного запроса – использование хранимых процедур.

    Заключение


    Основная цель статьи – дать общее представление о паттерне репозиторий и его реализации на LINQ to SQL. Рассмотренный пример применения подхода слишком прост. В реальных приложениях возникает множество проблем при реализации данной архитектуры. Вот некоторые из них.
    • Преобразование между объектом модели базы данных и объектом модели приложения может быть значительно более сложным. В таких случаях, невозможно реализовать фильтры над моделью приложения так, чтобы итоговый запрос можно было транслировать в SQL.
    • Часто в качестве результата выборки необходимо получить результат соединения (JOIN) нескольких таблиц, а не данные лишь одной таблицы.
    • Не все SQL-операции и функции имеют свой эквивалент в LINQ.
    Большинство проблем решаемы, но эти вопросы выходят за рамки данной статьи.

    Исходный код к статье (проект ASP.NET MVC).

    Ссылки по теме


    Паттерн Repository (Martin Fowler)

    Статьи по LINQ to SQL от Scott Guthrie

    Storefront MVC (screencasts):
    Repository Pattern
    Pipes and Filters
    Метки:
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 25
    • НЛО прилетело и опубликовало эту надпись здесь
      • +1
        Вообще-то существует утилита SQLMetal которая входит в состав студии, которая генерирует подобный функционал.
        • +2
          Но он предназначен для другого сценария работы: Unit of Work.
          • 0
            вот именно, долго искал до этого как именно репозиторий с l2s сделать
        • 0
          Вопросы:
          1. Если прочитать Фаулера по ссылке, видно что у репозитория не должно быть Save.
          Не очень понятно, зачем отказываться от Unit of Work, который считается более правильной практикой чем CRUD-методы.
          Потому что UoW не работает напрямик с доменными объектами если они не являются объектами Linq-to-Sql?

          2. Однако на практике использование атрибута первичного ключа в модели приложения часто приводит к получению даже более гибких схем.
          Например?
          • 0
            1. Все верно, UoW более полное решение проблемы ORM. Но, чтобы его развернуть на уровне доменных объектов, требуется значительно больше усилий. На уровне LINQ to SQL UoW и так есть. На самом деле, предлагаемое решение что-то среднее между репозиторием и модулем таблицы.

            2. Например, в веб-приложениях можно использовать Id, чтобы идентифицировать объект между двумя разными запросами.
            • +1
              1. Предлагаемое решение это по сути дела CRUD через Linq-to-SQL, то есть лучше чем ADO.NET, но красиво работает только для очень простых случаев (для которых м.б. стоит просто использовать Linq-to-Sql без обёрток?). Для более сложных я бы взял NHibernate/Entity Framework.

              2. Но это ведь не значит что Id должен быть свойством самого объекта?
              Например можно сделать IdService.GetId(o), которое используется только на UI.
          • 0
            Отличная статья, спасибо!
            На сколько я осведомлен о реализации репозитория, в большинстве решений он неразлучен от UnitOfWork, а так же является лишь хранилищем элементов, в то время как UnitOfWork принимает на себя работу со слоем сохраняемости.
            Отличный практический пример реализации можно найти в книге ".NET Domain-Driven Design with C#: Problem — Design — Solution" by Tim McCarthy.
            • 0
              >>Репозиторий – это фасад для доступа к базе данных (*)

              Мне кажется, данная мысль не соответствует исходной посылке Фаулера. По его мнению, репозиторий — это то, что позволяет оперировать с объектами, соблюдая принцип «Persistence Ignorance», т.е. не задумываясь, есть там БД или нет. У вас же получаются врапперы (правда, более или менее сложные) которые просто изолируют приложение от Linq (и то, не всегда — когда речь заходит об extension methods, используется IQueryable. Конечно, можно сказать, что классы ваших репозиториев могут на самом деле и не работать с БД, но тогда вы будете противоречить сами себе (*).

              В реальной практике редко (почти никогда) бывает возможно применить generic-репозитории, как это есть у вас; кроме того, модель CRUD превращается в кошмар, когда нужно работать не с раздельными объектами, а с их отношениями — в этом плане UnitOfWork куда удобнее.

              Тем не менее, как пример реализации CRUD на linq, статья полезная, спасибо.
              • +1
                … простите, отправил рано. Еще вопрос автору — как в модели управлять транзакциями? :)
                • 0
                  Что касается операций GetAll, Save, Delete, то они сами по себе атомарны. GetAll соответствует одному запросу на чтение и, естественно, атомарен. Save и Delete, хоть и делают два запроса, тоже атомарны — если между двумя запросами происходит изменение соответствующей строки в БД, то кидается ChangeConfictException при вызове SubmitChanges.

                  Если требуется реализовать транзакционность на более высоком уровне, например, несколько подряд идущих Save, то можно использовать TransactionScope.
                  • 0
                    транзакция начинает в начале метода Save и заканчивается в конце, лично я так делаю (но использую NHibernate вместо Linq2Sql) тоже самое с Delete, в HBM производительность из-за транзакции на сохранении падает в 10 раз (сам проверял).

                    кстати как с производительностью при транзакции в Linq2Sql, никто не проверял?
                    • 0
                      >> из-за транзакции на сохранении

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

                      >> в HBM производительность из-за транзакции на сохранении падает в 10 раз.

                      Придется Вас огорчить :) С версии 2.0 в NHibernate автоматические транзакции были объявлены небезопасными: вы обязаны выполнять в явной транзакции даже чтение данных
                • 0
                  в который раз завидую разработчикам слабонагруженых декстоп систем =) получил всю коллекцию, применил к ней выборки/сортировки, вытащил результат. Хороший патерн, конечно: хорошее расслоение системы.

                  поясните мне, как человеку плохо знакомому с С# как получается что rep.GetAll().WithNameLike(“Google”).OrderBy(x => x.Name) выполняет лишь один селект? неужели выполняющей функция является OrderBy? или покажите где можно почитать о том как происходит вызов.
                  просто я вижу эту строчку как-то так:
                  all = rep.GetAll();
                  res = all.WithNameLike(“Google”);
                  res = res..OrderBy(x => x.Name);
                  очевидно, однако, что это не так.
                  • 0
                    Фишка в том, что rep.GetAll().WithNameLike(“Google”).OrderBy(x => x.Name) вообще не выполняет селекта.
                    Он выполнится когда сделают foreach или ToArray/ToList/… — то есть когда неявно вызовут GetEnumerator(), а затем у него MoveNext().

                    Таким образом, Enumerator.MoveNext здесь и является «выполняющей функцией».
                    • +1
                      Соответственно
                      all = rep.GetAll();
                      res = all.WithNameLike(“Google”);
                      res = res.OrderBy(x => x.Name);

                      так и работает, т.к. OrderBy как и предыдущие методы просто добавляет новый параметр к запросу.
                      Сами по себе эти строчки к базе не обращаются.
                        • 0
                          > rep.GetAll().WithNameLike(“Google”).OrderBy(x => x.Name)

                          Наверняка эта строчка только составляет запрос, но не выполняет его. Почитайте «Domain-Specific Embedded Compilers», там эта идея очень хорошо подается.
                          • 0
                            >>в который раз завидую разработчикам слабонагруженых декстоп систем =)

                            Именно. Особенно тем разработчикам, которые могут внутри TransactionScope (http://habrahabr.ru/blogs/net/52173/#comment_1386154 делать раундтрип к БД на каждый Save для каждой Entity из некоторого набора =)
                          • 0
                            Вы создаете datacontext и удерживаете его (и соединение с сервером) в течении всего времени, пока существует репозиторий. А это плохо.
                            • 0
                              DataContext насколько я знаю не держит постоянное соединение с сервером (а зачем, собственно?).
                            • 0
                              как зелёный спец, плохо знающий язык Шекспира
                              прочитал про object-relational mapping тут:
                              ru.wikipedia.org/wiki/Object-relational_mapping

                              • 0
                                Правильно ли я понял, что при добавлении linq2sql как раз и получается использование паттерна репозиторий? Или Вы руками что-то допиливали?
                                • 0
                                  Спасибо, очень помогло, реализую сейчас такое в своем проекте =))
                                  • 0
                                    Спасибо большое за пост, какое то время пользовался здешними наработками.

                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.