BLToolkit – BLToolkit

  • Login
  • Preferences
  • Help/Guide
  • About Trac
  • Wiki
  • Timeline
  • Roadmap
  • Browse Source
  • View Tickets
  • Search

Context Navigation

  • Start Page
  • Index
  • History
  • Last Change

  1. Business Logic Toolkit
    1. Введение
      1. Те же и Oracle
    2. Зачем все это надо
      1. Немного истории
      2. За что боролись отцы и деды
    3. Класс DbManager
      1. Инициализация и создание экземпляра объекта
      2. Механизм дата провайдеров
      3. Параметры
      4. Методы Execute
        1. ExecuteDataSet
        2. ExecuteDataTable и ExecuteDataTables
        3. ExecuteReader
        4. ExecuteNonQuery
        5. ExecuteScalar
        6. ExecuteScalarList
        7. ExecuteScalarDictionary
        8. ExecuteObject
        9. ExecuteList
        10. ExecuteDictionary
        11. ExecuteForEach
        12. ExecuteResultSets
    4. Отображение данных
      1. Общие сведения
      2. Map & MappingSchema
      3. Семейства функций MappingSchema
        1. Семейства Convert
        2. Семейства Map
      4. Метаданные
        1. Атрибуты
        2. XML
    5. DataAccess
      1. DataAcessor
      2. SqlQuery

Business Logic Toolkit ¶

Основная задача статьи дать вводную информацию о библиотеке BLToolkit. Она поможет Вам сделать первый шаг в обуздании этого маленького монстра. Данная статья основана на оригинальной статье Игоря Ткачева "Пространство имен Rsdn.Framework.Data", и как следствие, содержит части оной.

Введение ¶

BLToolkit (ранее известна как Rsdn.Framework.Data) является библиотекой, содержащей набор классов, представляющих собой высокоуровневую обёртку над ADO.NET (вообще, это не совсем правда, содержит она гораздо больше, но исторически BLT создавалась как раз для этих целей). Казалось бы, ADO.NET сама по себе штука достаточно высокоуровневая, и зачем над ней ещё городить какой-то огород? Всё это так, но как это часто бывает, в борьбе добра со злом обычно, увы, побеждает лень.

Рассмотрим в качестве примера функцию, которая возвращает список объектов, содержащих информацию о людях: ID, имя, фамилию, отчество и пол (здесь и далее мы будем использовать базу данных BLToolkitData - это тестовая БД к BLT, на данной базе работают блочные тесты, здесь и далее для примеров я постараюсь использовать именно блочные тесты от BLT). Вот как это может выглядеть с использованием ADO.NET:

Таблица Person имеет следующий вид:

CREATE TABLE Person
(
   PersonID   int          NOT NULL IDENTITY(1,1) CONSTRAINT PK_Person PRIMARY KEY CLUSTERED,
   FirstName  nvarchar(50) NOT NULL,
   LastName   nvarchar(50) NOT NULL,
   MiddleName nvarchar(50)     NULL,
   Gender     char(1)      NOT NULL CONSTRAINT CK_Person_Gender CHECK (Gender in ('M', 'F', 'U', 'O'))
)
ON [PRIMARY]

public enum Gender
{
   Female,
   Male,
   Unknown,
   Other
}

public class Person
{
   public int    ID;
   public string FirstName;
   public string MiddleName;
   public string LastName;
   public Gender Gender;
}

Person GetPerson(int personId)
{
    string connectionString =
        "Server=.;Database=BLToolkit;Integrated Security=SSPI";
    string commandText = @"
        SELECT
            p.PersonId,
            p.FirstName,
            p.LastName,
            p.MiddleName,
            p.Gender
        FROM Person p
        WHERE p.PersonId = @PersonId";

    using (SqlConnection con = new SqlConnection(connectionString))
    {
        con.Open();

        using (SqlCommand cmd = new SqlCommand(commandText, con))
        {
            cmd.Parameters.Add("@min", min);

            using (SqlDataReader rd = cmd.ExecuteReader())
            {
                Person p = null;

                if (rd.Read())
                {
                    p = new Person();

                    p.ID           = Convert.ToInt32 (rd["PersonId"]);
                    p.FirstName    = Convert.ToString(rd["FirstName"]);
                    p.LastName     = Convert.ToString(rd["LastName"]);
                    p.MiddleName   = Convert.ToString(rd["MiddleName"]);
                    string gender  = Convert.ToString(rd["Gender"]);

                    switch(gender)
                    {
                        case "M":
                            p.Gender = Gender.Male;
                            break;
                        case "F":
                            p.Gender = Gender.Female;
                            break;
                        case "U":
                            p.Gender = Gender.Unknown;
                            break;
                        case "0":
                            p.Gender = Gender.Other;
                            break;
                    }
                }
                return p;
            }
        }
    }
}

А теперь то же самое, в исполнении BLToolkit:

public enum Gender
{
   [MapValue("F")] Female,
   [MapValue("M")] Male,
   [MapValue("U")] Unknown,
   [MapValue("O")] Other
}

public class Person
{
   [MapField("PersonID")]
   public int    ID;
   public string FirstName;
   public string MiddleName;
   public string LastName;
   public Gender Gender;
}

Person GetPerson(int personId)
{
    using (DbManager db = new DbManager())
    {
        return db
            .SetCommand(@"
                         SELECT
                             p.PersonId,
                             p.FirstName,
                             p.LastName,
                             p.MiddleName,
                             p.Gender
                         FROM Person p
                         WHERE p.PersonId = @PersonId",
                db.Parameter("@PersonId", personId))
            .ExecuteObject<Person>();
    }
}

Те же и Oracle ¶

Если вы начинаете тренироваться с BLToolkit на базе Oracle, последний фрагмент кода должен иметь вид:

Person GetPerson(int personId)
{
   using (DbManager db = new DbManager())
   {
      return db
        .SetCommand( @"
               SELECT
                  p.PersonId,
                  p.FirstName,
                  p.LastName,
                  p.MiddleName,
                  p.Gender
               FROM Person p
               WHERE p.PersonId = :PersonId",
           db.Parameter("PersonId", personId))
       .ExecuteObject<Person>();
   }
}
...
   var p = new BLToolkit.Data.DataProvider.OracleDataProvider();
   p.ParameterPrefix = null;
   DbManager.AddDataProvider(p);
   DbManager.AddConnectionString("Oracle",
      string.Format("Data Source={0};User ID={1};Password={2};",
      oracleAlias, oracleUser, oraclePassword));

   var person = GetPerson(1);

Не трудно заметить, что вариант BLToolkit заметно короче. Фактически все, что у нас осталось - это текст самого запроса. Класс DbManager самостоятельно осуществляет всю работу по созданию объекта и отображению (mapping) полей рекордсета на заданную структуру.

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

public abstract class PersonAccessor<Person, PersonAccessor>
{
    [SqlQuery(@"
               SELECT
                   p.PersonId,
                   p.FirstName,
                   p.LastName,
                   p.MiddleName,
                   p.Gender
               FROM Person p
               WHERE p.PersonId = @PersonId")]
    public abstract Person GetPerson(int personId){}
}

//и уже где-то совсем в другом месте программы
//получим нужного человека:

Person p = PersonAccessor.CreateInstance().GetPerson (10);

//:

Но давайте обо всём по порядку.

Зачем все это надо ¶

Немного истории ¶

В конечном итоге все упирается в фундаментальные проблемы: в коде мы имеем дело с классами и объектами, в реляционной БД мы имеем дело с таблицами и их отношениями - с данными.

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

Подробнее с проблемами, возникающими вокруг ORM, можно ознакомиться у Мартина Фаулера ( Архитектура корпоративных программных приложений).

BLToolkit является маленькой и шустрой системой, позволяющей отображать данные на объекты, а объекты на данные. Библиотека может стать удобным подспорьем для реализации любого паттерна от "Шлюза таблицы данных (Table Data Gateway)" до "Преобразователя данных (Data Mapper)" (термины приведены в соответствии с г-ном Фаулером).

Если посмотреть с внешней стороны, то можно выделить следующих ведущих игроков (именно о них мы будем говорить ниже):

  • DbManager - класс, предоставляющий высокоуровневую обёртку над ADO.NET.
  • Map & MappingSchema - первый класс является статическим, и делегирует свои вызовы к экземпляру MappingSchema. MappingSchema, в свою очередь, обеспечивает отображение ежа на ужа, а ужа на слона.
  • DataAccessor<T, D> - ода лени. Позволяет отделить мух от котлет - организовать уровень абстракции объектов от данных и способа их извлечения и сохранения.
  • Метаданные - мощный и удобный механизм .Net, предоставляющий возможность описать представление объекта в БД, не внося изменений в открытый интерфейс класса.

За что боролись отцы и деды ¶

А боролись они за высокую производительность, как программиста, так и BLT. Фактически, большая часть потрохов BLT это генераторы абстрактных классов: библиотека "на лету" эмитит небольшие классы, что позволяет избавить программиста от рутинной работы с одной стороны, и повысить производительность с другой.

Таким образом, BLT добавляет некоторые издержки на момент первого вызова, связанные с эмитом и кэшированием. Все последующие вызовы являются максимально оптимальными. Издержки же на отображение настолько низки, что ими можно пренебречь (для примера можно посмотреть здесь).

Класс DbManager ¶

Инициализация и создание экземпляра объекта ¶

Класс DbManager является основным в пространстве имен BLToolkit.Data и в единственном лице представляет собой замену всем основным объектам ADO.NET.

Для создания экземпляра объекта служит целый набор конструкторов:

public DbManager();

public DbManager(
    string configurationString
    );

public DbManager(
    string providerName,
    string configuration
    );

public DbManager(
    IDbConnection connection
    );

public DbManager(
    IDbTransaction transaction
    );

public DbManager(
    DataProviderBase dataProvider,
    string           connectionString
    );

public DbManager(
    DataProviderBase dataProvider,
    IDbConnection    connection
    );

public DbManager(
    DataProviderBase dataProvider,
    IDbTransaction   transaction
    );

Остановимся подробней на следующих параметрах (прочие параметры не должны вызвать вопросов у тех, кто хотя бы поверхностно знаком с ADO .NET):

  • configurationString - это не строка соединения (Connecting String), а ключ, по которому строка соединения будет читаться из файла конфигурации.
  • providerName - так же ключ, является именем провайдера данных, который следует использовать.
  • dataProvider - экземпляр провайдера данных, который следует использовать. Механизм провайдеров данных достоин отдельного обсуждения, что и будет сделано ниже.

Рассмотрим подробнее правила работы с файлом конфигурации:

<appSettings>
<!-- Конфигурация по умолчанию -->
<add
      key   = "ConnectionString"
      value = "Server=.;Database=BLToolkitData;Integrated Security=SSPI"/>
<!-- Конфигурация Development для SQL Server -->
<add
      key   = "ConnectionString.Development"
      value = "Server=.;Database=BLToolkitData;Integrated Security=SSPI"/>
<!-- Конфигурация Production для SQL Server -->
<add
      key   = "ConnectionString.Production"
      value = "Server=.;Database=BLToolkitData;Integrated Security=SSPI"/>
<!-- Конфигурация для SQL Server -->
<add
      key   = "ConnectionString.Sql"
      value = "Server=.;Database=BLToolkitData;Integrated Security=SSPI"/>
<!-- Конфигурация для Oracle -->
<add
      key   = "ConnectionString.Oracle"
      value = "User Id=/;Data Source=BLToolkitData"/>
<!-- Конфигурация OLEDB -->
<add
      key   = "ConnectionString.OleDb"
      value = "Provider=SQLNCLI.1;Data Source=.;Integrated Security=SSPI;Initial Catalog=BLToolkitData"/>
<!-- Конфигурация Development для OLEDB -->
<add
      key   = "ConnectionString.OleDb.Development"
      value = "Provider=SQLNCLI.1;Data Source=.;Integrated Security=SSPI;Initial Catalog=BLToolkitData"/>
<!-- Конфигурация Production для OLEDB -->
<add
      key   = "ConnectionString.OleDb.Production"
      value = "Provider=SQLNCLI.1;Data Source=.;Integrated Security=SSPI;Initial Catalog=BLToolkitData"/>
</appSettings>

Как видим, поле key - содержит ключевое значение ConntctionString и разделенные c ним через точку configurationString и providerName.

Конфигурацию можно задавать и в разделе connectionStrings:

<connectionStrings>
  <add name="Development" connectionString="Server=.;Database=BLToolkitData;Integrated Security=SSPI" />
</connectionStrings>

Рассмотрим следующие примеры создания DbManager:

// Использование конфигурации по умолчанию.
DbManager db = new DbManager();

// Использование конфигурации для Sql Server
// аналогично для Oracle DbManager ("Oracle")
DbManager db = new DbManager("Sql");

// Использование конфигурации Development для Sql Server
// аналогично Production для OLEDB DbManager ("OleDb" & "Production")
DbManager db = new DbManager("Sql", "Development");

Дополнительно есть возможность указать конфигурацию по умолчанию. Если в файл конфигурации добавить вот такую секцию:

<appSettings>
   <add
      key   = "BLToolkit.DefaultConfiguration"
      value = "Oracle"/>
</appSettings>

Т.е. вызов конструктора без параметров будет аналогичен вызову DbManager("Oracle").

Таким образом мы можем работать с различными конфигурациями и базами данных. Секция appSettings может находиться как в app.config или web.config так и machine.config файле.

Если же вам не хочется возиться с конфигурационными файлами, то для задания строки соединения можно воспользоваться методом AddConnectionString:

DbManager.AddConnectionString("MyConfig", connectionString);

using (DbManager db = new DbManager("MyConfig"))
{
    // ...
}

или

DbManager.AddConnectionString(connectionString);

using (DbManager db = new DbManager())
{
    // ...
}

Метод AddConnectionString достаточно вызвать один раз для каждой конфигурации в начале программы.

Механизм дата провайдеров ¶

Отличительной особенностью класса DbManager является то, что он работает исключительно с интерфейсами пространства имён System.Data и вполне может использоваться для работы с различными провайдерами данных. На данный момент поддерживается работа с MS Sql Server, Oracle, OLE DB (включая отдельный провайдер для Jet DB), ODBC, Firebird, MySql?, Sql Server CE, SQLIte и Sybase. Выбор провайдера осуществляется также с помощью строки конфигурации. Для этого достаточно добавить к ней соответствующий постфикс: ".OleDb", ".Odbc", ".Oracle", ".Sql" и т.п. Если постфикс не задан, то по умолчанию выбирается провайдер по умолчанию (Sql).

К вопросам о мифичности и реальности поддержки в одном проекте различных баз данных. Исходный код BLToolkit покрыт блочными тестами. При этом есть возможность в качестве тестовой базы данных использовать: MS Sql Server, Access, Oracle (используется OdpDataProvider .\Source\Data\DataProvider\OdpDataProvider.cs), Firebird (используется FdpDataProvider .\Source\Data\DataProvider\FdpDataProvider.cs). Так же примером может служить проект RSDN@HOME, где механизмами BLT осуществлена поддержка нескольких БД.

Таким образом, механизм дата провайдеров позволяет абстрагировать DbManager от специфики конкретного клиента и его реализации. Для примера можно рассмотреть OdpDataProvider.

В дополнение к существующим провайдерам совсем несложно подключить любой другой. Следующий пример демонстрирует подключение Borland Data Providers for .NET (BDP.NET):

using System;
using System.Data;
using System.Data.Common;

using Borland.Data.Provider;

using Rsdn.Framework.Data;
using Rsdn.Framework.Data.DataProvider;

namespace Example
{
    public class BdpDataProvider: IDataProvider
    {
        IDbConnection IDataProvider.CreateConnectionObject()
        {
            return new BdpConnection();
        }

        DbDataAdapter IDataProvider.CreateDataAdapterObject()
        {
            return new BdpDataAdapter();
        }

        void IDataProvider.DeriveParameters(IDbCommand command)
        {
            BdpCommandBuilder.DeriveParameters((BdpCommand)command);
        }

        Type IDataProvider.ConnectionType
        {
            get
            {
                return typeof(BdpConnection);
            }
        }

        string IDataProvider.Name
        {
            get
            {
                return "Bdp";
            }
        }
    }

    class Test
    {
        static void Main()
        {
            DbManager.AddDataProvider(new BdpDataProvider());
            DbManager.AddConnectionString(".bdp",
                "assembly=Borland.Data.Mssql,Version=1.1.0.0, " +
                "Culture=neutral,PublicKeyToken=91d62ebb5b0d1b1b;" +
                "vendorclient=sqloledb.dll;osauthentication=True;" +
                "database=Northwind;hostname=localhost;provider=MSSQL");

            using (DbManager db = new DbManager())
            {
                int count = (int)db
                    .SetCommand("SELECT Count(*) FROM Categories")
                    .ExecuteScalar();

                Console.WriteLine(count);
            }
        }
    }
}

Параметры ¶

Большинство используемых запросов требуют тот или иной набор параметров для своего выполнения. В приведённом выше примере таким параметром является @personId - идентификатор человека в базе. Зачастую, среднеленивый программист предпочитает использовать в подобных случаях обычную конкатенацию строк, т.е. что-то наподобие следующего:

void Test(int id)
{
    string commandText = @"
        SELECT FirstName
        FROM   Person
        WHERE  PersonId = " + id;

    // ...
}

К сожалению, при всей своей простоте, такой стиль плохо читаем, часто ведёт к непредсказуемым ошибкам и долгим мучениям с подбором формата, если в качестве параметра, например, используется дата. Более того, если наш параметр имеет строковый тип, то применение такого подхода в Web-приложениях может сделать их весьма уязвимыми для хакерских атак. Поэтому, отложим шутки в сторону и серьёзно займёмся рассмотрением возможностей, предоставляемых классом DbManager для работы с параметрами.

Для создания параметров служит следующий набор методов:

public IDbDataParameter Parameter(
   string parameterName,
   object value
);

public IDbDataParameter InputParameter(
   string parameterName,
   object value
);

Создаёт входной (ParameterDirection.Input) параметр с именем parameterName и значением value.

public IDbDataParameter NullParameter(
    string parameterName,
    object value
);

Делает тоже, что и предыдущие методы и в дополнение проверяет значение value. Если оно представляет собой null, пустую строку, значение даты DateTime.MinValue или 0 для целых типов, то вместо заданного значения подставляется DBNull.Value.

public IDbDataParameter OutputParameter(
    string parameterName,
    object value
);

Создаёт выходной (ParameterDirection.Output) параметр.

public IDbDataParameter InputOutputParameter(
    string parameterName,
    object value
);

Создаёт параметр, работающий как входной и выходной (ParameterDirection.InputOutput).

public IDbDataParameter ReturnValue(
    string parameterName
);

Создаёт параметр-возвращаемое значение (ParameterDirection.ReturnValue).

public IDbDataParameter Parameter(
    ParameterDirection parameterDirection,
    string parameterName,
    object value
);

Создаёт параметр с заданными значениями.

Создание выходных параметров и возвращаемое значение используются для работы с сохранёнными процедурами. Входной параметр можно использовать для построения любых запросов.

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

public IDbDataParameter Parameter(
    string parameterName
);

Каждая версия метода Execute: имеет в своём составе метод, принимающий в качестве последнего аргумента список параметров запроса. Например, для ExecuteNonQuery одна из таких функций имеет следующий вид:

public int ExecuteNonQuery(
    string commandText,
    params IDbDataParameter[] commandParameters
);

Таким образом, список параметров задаётся простым перечислением через запятую (с таблицей Region и примерами с ней связанными я отойду от правила использовать БД BLToolkit):

void InsertRegion(int id, string description)
{
    using (DbManager db = new DbManager())
    {
        db
            .SetCommand(@"
                INSERT INTO Region (
                    RegionID,
                    RegionDescription
                ) VALUES (
                    @id,
                    @desc
                )",
                db.Parameter("@id",   id),
                db.Parameter("@desc", description))
            .ExecuteNonQuery();
    }
}

Для создания списка параметров из бизнес объектов существует метод CreateParameters, который принимает в качестве аргумента объект DataRow или любой бизнес-объект. Допустим, у нас имеется класс Region, содержащий информацию о регионе. В этом случае мы могли бы переписать предыдущий пример следующим образом:

public class Region
{
    public int    ID;
    public string Description;
}

void InsertRegion(Region region)
{
    using (DbManager db = new DbManager())
    {
        db
            .SetCommand(@"
                INSERT INTO Region (
                    RegionID,
                    RegionDescription
                ) VALUES (
                    @ID,
                    @Description
                )",
                db.CreateParameters(region)).
            .ExecuteNonQuery();
    }
}

Более общий вид функции CreateParameters для бизнес объекта (аналогично для DataRow) выглядит следующим образом:

public IDbDataParameter[] CreateParameters(
   object                    obj,
   string[]                  outputParameters,
   string[]                  inputOutputParameters,
   string[]                  ignoreParameters,
   params IDbDataParameter[] commandParameters);

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

public class Region
{
    public int    ID;
    public string Description;
}

void InsertRegion(Region region)
{
    using (DbManager db = new DbManager())
    {
        db
            .SetCommand(@"
                INSERT INTO Region (
                    RegionDescription
                ) VALUES (
                    @Description
                )
                SELECT Cast(SCOPE_IDENTITY() as int) ID",
                db.CreateParameters(region, new string[]{"ID"}, null, null)).
            .ExecuteObject(region);
    }
}

В результате данного вызова объекту region в соответствующее поле будет задано значение ID только что вставленной записи (считаем, что поле ID в таблице Region - автоинкрементное).

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

DataSet SelectByName(string firstName, string lastName)
{
    using (DbManager db = new DbManager())
    {
        return db
            .SetSpCommand("Person_SelectListByName", firstName, lastName)
            .ExecuteDataSet();
    }
}

В данном случае важен лишь порядок следования аргументов процедуры. Данная функция самостоятельно строит список параметров исходя из списка параметров сохранённой процедуры.

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

public IDbDataParameter Parameter(
    string parameterName
);

Например, в приведённом выше примере возвращаемое значение сохранённой процедуры можно (ну тут я слукавил - !Person_SelectByName не возвращает такого значения, но если бы возвращала, то было бы можно) проверить следующим образом:

DataSet SelectByName(string firstName, string lastName)
{
    using (DbManager db = new DbManager())
    {
        DataSet dataSet = db
            .SetSpCommand("Person_SelectListByName", firstName, lastName)
            .ExecuteDataSet();

        int returnValue = (int)db.Parameter("@RETURN_VALUE").Value;

        if (returnValue != 0)
        {
            throw new Exception(
                string.Format("Return value is '{0}'", returnValue));
        }

        return dataSet;
    }
}

Последней возможностью работы с параметрами, которую нам осталось рассмотреть, является использование функции подготовки запроса Prepare, которая может быть полезной при выполнении одного и того же запроса несколько раз. Фактически в данном случае вызов метода Execute: разбивается на две части: первая - вызов Prepare с заданием типа, текста и параметров запроса, вторая - вызов соответствующего метода Execute: для выполнения запроса определённое число раз. Следующий пример демонстрирует данную возможность.

void InsertRegionList(Region[] regionList)
{
    using (DbManager db = new DbManager())
    {
        db
            .SetCommand (@"
                INSERT INTO Region (
                    RegionID,
                    RegionDescription
                ) VALUES (
                    @ID,
                    @Description
                )",
                db.Parameter("@ID",          regionList[0].ID),
                db.Parameter("@Description", regionList[0].Description))
            .Prepare();

        foreach (Region r in regionList)
        {
            db.Parameter("@ID").Value          = r.ID;
            db.Parameter("@Description").Value = r.Description;

            db.ExecuteNonQuery();
        }
    }
}

Либо мы можем упростить его следующим образом для бизнес объектов:

void InsertRegionList(Region[] regionList)
{
    using (DbManager db = new DbManager())
    {
        db
            .SetCommand(@"
                INSERT INTO Region (
                    RegionID,
                    RegionDescription
                ) VALUES (
                    @ID,
                    @Description
                )",
                db.CreateParameters(regionList[0]))
            .Prepare();

        foreach (Region r in regionList)
        {
            db.AssignParameterValues(r);
            db.ExecuteNonQuery();
        }
    }
}

и класса DataRow

static void InsertRegionTable(DataTable dataTable)
{

    using (DbManager db = new DbManager())
    {
        db
            .SetCommand(@"
                INSERT INTO Region (
                    RegionID,
                    RegionDescription
                ) VALUES (
                    @ID,
                    @Description
                )",
                db.CreateParameters(dataTable.Rows[0]))
            .Prepare();

        foreach (DataRow dr in dataTable.Rows)
            db.AssignParameterValues(dr).ExecuteNonQuery();
    }
}

Конечно, для совсем ленивых есть вот такой вариант (метод ExecuteForEach использует именно описанный выше механизм):

void InsertRegionList(Region[] regionList)
{
    using (DbManager db = new DbManager())
    {

        db
            .SetCommand(@"
                INSERT INTO Region (
                    RegionID,
                    RegionDescription
                ) VALUES (
                    @ID,
                    @Description
                )")
            .ExecuteForEach(regionList);
    }
}

Методы Execute ¶

Класс DbManager содержит целый набор семейств методов Execute. Каждое семейство отличается типом возвращаемой сущности, это может быть DataSet, бизнес объект, коллекция бизнес объектов и так далее. Ниже мы рассмотрим все семейства Execute.

ExecuteDataSet ¶

public DataSet ExecuteDataSet();

public DataSet ExecuteDataSet(DataSet dataSet);

public DataSet ExecuteDataSet(NameOrIndexParameter table);

public DataSet ExecuteDataSet(
    DataSet              dataSet,
    NameOrIndexParameter table);

public DataSet ExecuteDataSet(
    DataSet              dataSet,
    int                  startRecord,
    int                  maxRecords,
    NameOrIndexParameter table);

Как видно из названия метода результатом данного выражения является объект класса DataSet (подобное семантическое правило сохраняется для всех семейств).

Рассмотрим подробнее параметры методов:

  • dataSet - результирующий датасет (он будет заполнен и возвращен). Если null, то будет создан новый экземпляр датасета. Подобный подход импользуется так же в других методах семейств Execute.
  • table - имя или номер таблицы для заполнения в результирующем датасете. Отдельный интерес представляет класс NameOrIndexParameter, для ознакомления с технологией работы лучше прочитать статью: Унифицированная система передачи строковых/числовых параметров.
  • startRecord - номер записи с которой начинать заполнение (считается с нуля).
  • maxRecords - максимальное число записей для заполнения.

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

ExecuteDataTable и ExecuteDataTables ¶

public DataTable ExecuteDataTable();

public DataTable ExecuteDataTable(DataTable dataTable);

public void ExecuteDataTables(
    int    startRecord,
    int    maxRecords,
    params DataTable[] tableList);

public void ExecuteDataTables(params DataTable[] tableList);

Как видим, в данном семействе есть два вида методов: ExecuteDataTable - заполняет одну таблицу, ExecuteDataTables - заполняет массив таблиц, заданный параметром tableList.

ExecuteReader ¶

public IDataReader ExecuteReader();

public IDataReader ExecuteReader(CommandBehavior commandBehavior)

Возвращает экземпляр !IDataReader.

C commandBehavior подробней можно ознакомиться в MSDN.

ExecuteNonQuery ¶

public int ExecuteNonQuery();

public int ExecuteNonQuery(
    string returnValueMember,
    object obj);

public int ExecuteNonQuery(object obj);

public int ExecuteNonQuery(
    string          returnValueMember,
    params object[] objects);

public int ExecuteNonQuery(params object[] objects);

Данное семейство используется для выполнения UPDATE, INSERT и DELETE запросов. Все методы возвращают число записей, обработанных запросом.

Рассмотрим подробнее параметры методов:

  • returnValueMember - грубо говоря, это имя поля в объекте, в которое необходимо записать возвращаемое значение. Если же быть точным, то это имя маппера поля (MemberMapper) в которое следует записать возвращаемое значение. Подробнее о мапинге (отображении) мы поговорим ниже.
  • obj - объект в который будут отображены (записаны) параметры команды.
  • objects - коллекция объектов в которые будут отображены (записаны) параметры команды.

Здесь мы впервые столкнулись с проявлениями мапинга (отображения). Ранее мы говорили, что BLT как раз занимается в первую очередь отображением данных из БД на объекты в коде; пришло время рассмотреть первый пример этого отображения.

Первые участники действа это хранимые процедуры:

-- OutRefTest
CREATE Procedure OutRefTest
   @ID             int,
   @outputID       int output,
   @inputOutputID  int output,
   @str            varchar(50),
   @outputStr      varchar(50) output,
   @inputOutputStr varchar(50) output
AS

SET @outputID       = @ID
SET @inputOutputID  = @ID + @inputOutputID
SET @outputStr      = @str
SET @inputOutputStr = @str + @inputOutputStr

-- Scalar_ReturnParameter
CREATE Function Scalar_ReturnParameter()
RETURNS int
AS
BEGIN
   RETURN 12345
END

И собственно тесты:

public class ReturnParameter
{
   public int Value;
}

[Test]
public void MapReturnValue()
{
   ReturnParameter e = new ReturnParameter();
   using (DbManager db = new DbManager())
   {
      db
         .SetSpCommand("Scalar_ReturnParameter")
         .ExecuteNonQuery("Value", e);
   }

   Assert.AreEqual(12345, e.Value);
}

Как видим в данном тесте BLT успешно отображает возвращаемое функцией !Scalar_ReturnParameter значение на поле Value объекта класса ReturnParameter.

Рассмотрим еще два теста:

public class OutRefTest
{
   public int    ID             = 5;
   public int    outputID;
   public int    inputOutputID  = 10;
   public string str            = "5";
   public string outputStr;
   public string inputOutputStr = "10";
}

[Test]
public void MapOutput()
{
   OutRefTest o = new OutRefTest();

   using (DbManager db = new DbManager())
   {
      db
         .SetSpCommand("OutRefTest", db.CreateParameters(o,
            new string[] {      "outputID",      Str" },
            new string[] { "inputOutputID", utputStr" },
            null))
         .ExecuteNonQuery(o);
   }

   Assert.AreEqual(5,     o.outputID);
   Assert.AreEqual(15,    o.inputOutputID);
   Assert.AreEqual("5",   o.outputStr);
   Assert.AreEqual("510", o.inputOutputStr);
}

[Test]
public void MapDataRow()
{
   DataTable dataTable = new DataTable();
   dataTable.Columns.Add("ID",             typeof(int));
   dataTable.Columns.Add("outputID",       typeof(int));
   dataTable.Columns.Add("inputOutputID",  typeof(int));
   dataTable.Columns.Add("str",            typeof(string));
   dataTable.Columns.Add("outputStr",      typeof(string));
   dataTable.Columns.Add("inputOutputStr", typeof(string));

   DataRow dataRow = dataTable.Rows.Add(new object[]{5, 0, 10, "5", 10"});

   using (DbManager db = new DbManager())
   {
      db
         .SetSpCommand("OutRefTest", teParameters(dataRow,
            new string[] {      "outputID",      Str" },
            new string[] { "inputOutputID", utputStr" },
            null))
         .ExecuteNonQuery(dataRow);
   }

   Assert.AreEqual(5,     dataRow["outputID"]);
   Assert.AreEqual(15,    dataRow["inputOutputID"]);
   Assert.AreEqual("5",   dataRow["outputStr"]);
   Assert.AreEqual("510", dataRow["inputOutputStr"]);
}

Здесь я специально рассмотрел два примера, хотя, по сути, демонстрируют они одинаковое использование ExecuteNonQuery. Разница заключается в том, что в первом тесте отображение происходит на бизнес объект класса OutRefTest а во втором на объект класса DataRow. BLT с успехом справляется с задачей отображения "ужа на ежа" а при необходимости и "ужа на слона".

Отличием этих двух тестов от предыдущего, является то, что мы не сообщили явно куда и что отображать. Это не есть проявление телепатии, это есть проявление здравого смысла - при мапинге система ориентируется в частности на имена полей и параметров. В рассмотренном примере имена полей класса OutRefTest и ячеек объекта dataRow совпадают с именами параметров хранимой процедуры OutRefTest, именно по этим признакам система поняла, что и куда раскладывать.

ExecuteScalar ¶

public object ExecuteScalar();

public object ExecuteScalar(ScalarSourceType sourceType);

public object ExecuteScalar(
    ScalarSourceType     sourceType,
    NameOrIndexParameter nameOrIndex);

public T ExecuteScalar<T>();

public T ExecuteScalar<T>(ScalarSourceType sourceType);

public T ExecuteScalar<T>(
    ScalarSourceType     sourceType,
    NameOrIndexParameter nameOrIndex);

Семейство предназначено для получения скалярных величин. Функции без параметров возвращают значение в первой колонке первой строки полученного запросом кортежа.

Подробнее рассмотрим параметры:

  • sourceType - одно из значений перечисления ScalarSourceType, может принимать следующие значения: DataReader - будет возвращено значение в первой колонке первой строки кортежа (поведение по умолчанию); OutputParameter - будет возвращен первый выходной параметр; ReturnValue - позволяет получить возвращаемое значение; AffectedRows - количество строк, обработанных запросом.
  • nameOrIndex - позволяет задать имя \ номер колонки (для ScalarSource.DataReader) либо параметра (для ScalarSource.OutputParameter) которые следует возвращать.

Generic версии методов позволяют явно задать тип возвращаемого значения.

Мы не будем рассматривать примеры для семейств ExecuteScalar*, вместо этого мы рассмотрим примеры к ExecuteObject и родственным ему семействам. По структуре они практически идентичны, но ExecuteObject гораздо интереснее :).

ExecuteScalarList ¶

public IList ExecuteScalarList(
    IList                list,
    Type                 type,
    NameOrIndexParameter nameOrIndex);

public IList ExecuteScalarList(
    IList list,
    Type  type);

public ArrayList ExecuteScalarList(
    Type                 type,
    NameOrIndexParameter nameOrIndex);

public ArrayList ExecuteScalarList(Type type);

public List<T> ExecuteScalarList<T>();

public List<T> ExecuteScalarList<T>(NameOrIndexParameter nameOrIndex);

public IList<T> ExecuteScalarList<T>(
    IList<T>             list,
    NameOrIndexParameter nameOrIndex);

public IList<T> ExecuteScalarList<T>(IList<T> list);

Семейство предназначено для вычитки списка скалярных величин. Практически все параметры идентичны по семантике параметрам семейства ExecuteScalar.

Параметр type задает требуемый тип вычитываемой скалярной величины.

ExecuteScalarDictionary ¶

public IDictionary ExecuteScalarDictionary(
   IDictionary dic,
   NameOrIndexParameter keyField,   Type keyFieldType,
   NameOrIndexParameter valueField, Type valueFieldType);

public Hashtable ExecuteScalarDictionary(
   NameOrIndexParameter keyField,   Type keyFieldType,
   NameOrIndexParameter valueField, Type valueFieldType);

public IDictionary<K,T> ExecuteScalarDictionary<K,T>(
   IDictionary<K,T>     dic,
   NameOrIndexParameter keyField,
   NameOrIndexParameter valueField);

public Dictionary<K,T> ExecuteScalarDictionary<K,T>(
   NameOrIndexParameter keyField,
   NameOrIndexParameter valueField);

Одной из приятностей BLToolkit является возможность возвращать не просто списки, а словари. Итак, данное семейство возвращает словари скалярных величин из кортежа. Параметры по семантике аналогичны семейству ExecuteScalar, прификс key - для ключа, прификс value для значения.

Но, на этом еще не все. У данного семейства есть еще подсемейство:

public IDictionary ExecuteScalarDictionary(
   IDictionary          dic,
   MapIndex             index,
   NameOrIndexParameter valueField,
   Type                 valueFieldType);

public Hashtable ExecuteScalarDictionary(
   MapIndex             index,
   NameOrIndexParameter valueField,
   Type                 valueFieldType);

public IDictionary<CompoundValue,T> ExecuteScalarDictionary<T>(
   IDictionary<CompoundValue, T> dic,
   MapIndex                      index,
   NameOrIndexParameter          valueField);

public Dictionary<CompoundValue,T> ExecuteScalarDictionary<T>(
   MapIndex             index,
   NameOrIndexParameter valueField)

Отличается оно тем, что вместо параметров с префиксом key используется параметр index. Параметр index позволяет строить индекс не по одному ключевому полю, а по их совокупности. Таким образом, ключом в результирующем словаре будет экземпляр класса CompaundValue, представляющий сложный ключ как единый объект. Мы обязательно рассмотрим пример использования "индексированных" словарей, но ниже.

ExecuteObject ¶

public object ExecuteObject(object entity);

public object ExecuteObject(object entity, params object[] parameters);

public object ExecuteObject(Type type);

public object ExecuteObject(Type type, params object[] parameters);

public T ExecuteObject<T>();

public T ExecuteObject<T>(params object[] parameters);

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

Рассмотрим параметры функций:

  • entity - объект, куда будет осуществлено чтение.
  • type - задает требуемый тип возвращаемого объекта.
  • parameters - дополнительные параметры, которые будут переданы в конструктор. Здесь стоит отметить, что переданы они будут как соответствующее свойство объекта InitContext, класс бизнес объекта, в свою очередь, должен иметь конструктор вида MyObject(InitContext context).

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

[Test]
public void ExecuteObject()
{
   using (DbManager db = new DbManager())
   {
      Person p = db
         .SetCommand("SELECT * FROM Person WHERE PersonID = @id",
         db.Parameter("id", 1))
         .ExecuteObject<Person>();

      TypeAccessor.WriteConsole(p);
      Assert.AreEqual(1,           p.ID);
      Assert.AreEqual("John",      p.FirstName);
      Assert.AreEqual("Pupkin",    p.LastName);
      Assert.AreEqual(Gender.Male, p.Gender);
   }
}

Кортеж будет иметь следующий вид:

PersonId | FirstName | LastName | MiddleName | Gender
1 | John | Pupkin | NULL | M

Итак, система отобразила выбранную запись на бизнес объект типа Person. Подробней стоит остановиться на полях Person.ID и Person.Gender. Отметим пару интересных моментов:

  • В исходном кортеже нет поля ID, а в классе Person поля PersonId. Эта проблема была решена атрибутом MapField("PersonId"), установленным на поле Person.ID. Так мы сообщили системе, что при мапинге у данного поля будет псевдоним отличный от "родового имени".
  • В исходном кортеже поле Gender имеет символьный тип, Person.Gender - является перечислением. Здесь нас выручил атрибут MapValue("M") - им мы указали системе, что при отображении данное значение является эквивалентным "M".

ExecuteList ¶

public ArrayList ExecuteList(Type type);

public ArrayList ExecuteList(Type type, params object[] parameters);

public IList ExecuteList(IList list, Type type);

public IList ExecuteList(
    IList           list,
    Type            type,
    params object[] parameters);

public List<T> ExecuteList<T>();

public List<T> ExecuteList<T>(params object[] parameters);

public IList<T> ExecuteList<T>(IList<T> list);

public IList<T> ExecuteList<T>(IList<T> list, params object[] parameters);

public L ExecuteList<L,T>(L list, params object[] parameters)
    where L : IList<T>;

public L ExecuteList<L,T>(params object[] parameters)
    where L : IList<T>, new();

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

Не используйте данное семейство для вычитки списка скалярных величин, для этого существует семейство ExecuteScalarList. Рассмотрим небольшой пример использования данного семейства:

[Test]
public void ExecuteList1()
{
   using (DbManager db = new DbManager())
   {
      ArrayList list = db
         .SetCommand("SELECT * FROM Person")
         .ExecuteList(typeof(Person));

      Assert.IsNotEmpty(list);
   }
}

ExecuteDictionary ¶

public Hashtable ExecuteDictionary(
   NameOrIndexParameter keyField,
   Type                 keyFieldType,
   params object[]      parameters);

public IDictionary ExecuteDictionary(
   IDictionary          dictionary,
   NameOrIndexParameter keyField,
   Type                 type,
   params object[]      parameters);

public Dictionary<TKey, TValue> ExecuteDictionary<TKey, TValue>(
   NameOrIndexParameter keyField,
   params object[]      parameters);

public IDictionary<TKey, TValue> ExecuteDictionary<TKey, TValue>(
   IDictionary<TKey, TValue> dictionary,
   NameOrIndexParameter      keyField,
   params object[]           parameters);

public IDictionary<TKey, TValue> ExecuteDictionary<TKey, TValue>(
   IDictionary<TKey, TValue> dictionary,
   NameOrIndexParameter      keyField,
   Type                      destObjectType,
   params object[]           parameters)

Позволяет вычитывать словарь бизнес объектов из кортежа. Семантика параметров аналогична ExecuteScalarDictionary и ExecuteObject. Параметру type и destObjectType - задают требуемый тип бизнес объекта.

Не используйте данное семейство для вычитки словарей скалярных величин, для этого есть семейство ExecuteScalarDictionary.

Как и в случае с ExecuteScalarDictionary ExecuteDictionary имеет "индексированное" подсемейство:

public Hashtable ExecuteDictionary(
   MapIndex        index,
   Type            type,
   params object[] parameters);

public IDictionary ExecuteDictionary(
   IDictionary     dictionary,
   MapIndex        index,
   Type            type,
   params object[] parameters);

public Dictionary<CompoundValue, TValue> ExecuteDictionary<TValue>(
   MapIndex        index,
   params object[] parameters);

public IDictionary<CompoundValue, TValue> ExecuteDictionary<TValue>(
   IDictionary<CompoundValue, TValue> dictionary,
   MapIndex                           index,
   params object[]                    parameters);

public IDictionary<CompoundValue, TValue> ExecuteDictionary<TValue>(
   IDictionary<CompoundValue, TValue> dictionary,
   MapIndex                           index,
   Type                               destObjectType,
   params object[]                    parameters)

Опять-таки, нам тут все знакомо, поэтому для закрепления понимания сразу перейдем к примеру:

private const int     _id = 1;

[Test]
public void DictionaryMapIndexTest3()
{
   using (DbManager db = new DbManager())
   {
      Hashtable table = new Hashtable();
      db
         .SetCommand("SELECT * FROM Person")
         .ExecuteDictionary(table,
         new MapIndex("@PersonID", 2, 3), typeof(Person));

      Assert.IsNotNull(table);
      Assert.IsTrue(table.Count > 0);

      Person actualValue = (Person)table[new CompoundValue(_id, "", "Pupkin")];
      Assert.IsNotNull(actualValue);
      Assert.AreEqual("John", actualValue.FirstName);
   }
}

В примере используется сложный ключ, состоящий из полей PersonId, третьего поля в кортеже (все считается с нуля) - SecondName и четвертого поля в кортеже - MiddleName. Ключом в словаре является объект класса CompaundValue.

Ну и как всегда, если нам не нужны такие изыски (сложные ключи) то можно сделать все гораздо проще:

[Test]
public void GenericsDictionaryTest()
{
   using (DbManager db = new DbManager())
   {
      Dictionary<int, Person> dic = db
         .SetCommand("SELECT * FROM Person")
         .ExecuteDictionary<int, Person>("ID");

      Assert.IsNotNull(dic);
      Assert.IsTrue(dic.Count > 0);

      Person actualValue = dic[1];
      Assert.IsNotNull(actualValue);
      Assert.AreEqual("John", actualValue.FirstName);
   }
}

Как видно из примеров, в одном случае используется @PersonId а в другом ID. Разница в следующем: если не указано "@", то значение берётся из поля уже смапленного объекта, если "@" присутствует, то из исходной записи.

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

ExecuteForEach ¶

public int ExecuteForEach(ICollection collection);

public int ExecuteForEach<T>(ICollection<T> collection);

public int ExecuteForEach(DataTable table);

public int ExecuteForEach(DataSet dataSet);

public int ExecuteForEach(DataSet dataSet, NameOrIndexParameter nameOrIndex);

Ранее я уже приводил пример данного семейства. Но не грех и повторить: данное семейство выполняет SQL выражение для заданного множества. Сначала команда готовит выражение, используя метод Prepare() после чего выполняет ExecuteNonQuery() для каждого элемента коллекции (из элементов коллекции заполняются значения параметров).

Параметры, подробно описывать не буду, замечу только, что для dataSet без nameOrIndex выражение будет выполнено для первой таблицы (индекс == 0).

ExecuteResultSets ¶

public MapResultSet[] ExecuteResultSet(params MapResultSet[] resultSets);

public MapResultSet[] ExecuteResultSet(
   Type masterType,
   params MapNextResult[] nextResults);

public MapResultSet[] ExecuteResultSet<T>(params MapNextResult[] nextResults);

Это семейство позволяет выполнять комплексное отображение данных на сложную связанную иерархию объектов.

Можно долго рассказывать, как и что, но проще разобрать все на примере. В примере заданы следующие связи (в рамках объектной модели):

  • Parent к Child - ко многим
  • Child к Parent - к одному
  • Child к Grandchild - ко многим
  • Grandchild к Child - к одному.

Таким образом, имеем иерархию из 3 взаимосвязанных классов.

Особое внимание следует обратить на то, что повязка осуществляется по именам для отображения.

Читайте, наслаждайтесь (примечания в комментариях к тексту):

[TestFixture]
public class ComplexMapping
{
   // запрос с 3 связанными таблицами
   const string TestQuery = @"
      -- Parent Data
      SELECT       1 as ParentID
      UNION SELECT 2 as ParentID

      -- Child Data
      SELECT       4 ChildID, 1 as ParentID
      UNION SELECT 5 ChildID, 2 as ParentID
      UNION SELECT 6 ChildID, 2 as ParentID
      UNION SELECT 7 ChildID, 1 as ParentID

      -- Grandchild Data
      SELECT       1 GrandchildID, 4 as ChildID
      UNION SELECT 2 GrandchildID, 4 as ChildID
      UNION SELECT 3 GrandchildID, 5 as ChildID
      UNION SELECT 4 GrandchildID, 5 as ChildID
      UNION SELECT 5 GrandchildID, 6 as ChildID
      UNION SELECT 6 GrandchildID, 6 as ChildID
      UNION SELECT 7 GrandchildID, 7 as ChildID
      UNION SELECT 8 GrandchildID, 7 as ChildID
";
   // верхний класс
   public class Parent
   {
      [MapField("ParentID")]
      public int ID;
      // Список подчиненных объектов
      public List<Child> Children = new List<Child>();
   }
   // класс связанный с Parent
   [MapField("ParentID", "Parent.ID")]
   public class Child
   {
      [MapField("ChildID")]
      public int ID;
      // родительский объект
      public Parent Parent = new Parent();
      //Список подчиненных объектов
      public List<Grandchild> Grandchildren = new List<Grandchild>();
   }
   // Класс связи связанный с Child
   [MapField("ChildID", "Child.ID")]
   public class Grandchild
   {
      [MapField("GrandchildID")]
      public int ID;
      // родительский объект
      public Child Child = new Child();
   }

   [Test]
   public void Test()
   {
      // список родительских объектов - "корень" который будет заполнен
      List<Parent>   parents = new List<Parent>();
      // массив резалтсетов
      MapResultSet[] sets    = new MapResultSet[3];

      //создадим резалтсет для корневого списка
      // в качестве параметров переданы тип корневого объекта и
      // и список объектов, который следует заполнить
      sets[0] = new MapResultSet(typeof(Parent), parents);
      sets[1] = new MapResultSet(typeof(Child));
      sets[2] = new MapResultSet(typeof(Grandchild));

      // зададим связь резалтсету "Parent" устанавливается подчиненная
      // связь к резалтсету "Child"
      // параметры:
      // имя поля отображения по которому осуществляется связь в подчиненном объекте
      // имя поля отображения по которому осуществляется связь в родительском объекте
      // имя поля отображения в родительском объекте для заполнения дочерними
      sets[0].AddRelation(sets[1], "ParentID", "ParentID", "Children");
      // все практически аналогично, но теперь задается обратная связь
      // от Child к Parent
      // таким образом в результате отображения будет заполнено не только
      // поле Parent.Children но и для каждого Child из Children будет задан Parent
      sets[1].AddRelation(sets[0], "ParentID", "ParentID", "Parent");

      // Аналогично, но уже от Child к Grandchild и наоборот
      sets[1].AddRelation(sets[2], "ChildID", "ChildID", "Grandchildren");
      sets[2].AddRelation(sets[1], "ChildID", "ChildID", "Child");

      using (DbManager db = new DbManager())
      {
         db
            .SetCommand      (TestQuery)
            .ExecuteResultSet(sets);
      }
      // здесь проверки правильности заполнения
      Assert.IsNotEmpty(parents);

      foreach (Parent parent in parents)
      {
         Assert.IsNotNull(parent);
         Assert.IsNotEmpty(parent.Children);

         foreach (Child child in parent.Children)
         {
            Assert.AreEqual(parent, child.Parent);
            Assert.IsNotEmpty(child.Grandchildren);

            foreach (Grandchild grandchild in child.Grandchildren)
            {
               Assert.AreEqual(child,  grandchild.Child);
               Assert.AreEqual(parent, grandchild.Child.Parent);
            }
         }
      }
   }
}

В приведенном примере для осуществления повязки используются строковые имена, аналогично можно использовать составные индексы, при помощи уже известного нам класса MapIndex (если забыли то см. ExecuteScalarDictionary и ExecuteDictionary и их индексированные подсемейства).

Отображение данных ¶

Общие сведения ¶

ADO.NET поддерживает два способа чтения данных из источника: прямое чтение из объекта класса DataReader, либо с помощью класса DataAdapter в экземпляр класса DataSet, который по сути представляет собой единственный вариант бизнес сущностей, предлагаемых и культивируемых Microsoft.

Оставим сегодня в покое достоинства и преимущества класса DataSet, и лишь заметим, что часто бывает необходимо уметь читать данные непосредственно в бизнес объекты приложения. При этом иногда нужно выполнять некоторые действия по отображению данных, например, из строковых значений в перечислители (enumerators) или замене значений NULL на нечто более удобоваримое. Как вы заметили из нашего самого первого примера, класс DbManager великодушно предоставляет нам такие возможности.

Вернемся к примеру использования семейства ExecuteList, и разберём подробнее что там происходит.

Метод ExecuteList создаёт экземпляр класса Person для каждой записи в таблице, затем осуществляет отображение данных на поля объекта и добавляет его в список. Для отображения колонок таблицы на поля и свойства нашего объекта используется механизм Reflection, единственным недостатком которого является некоторая нерасторопность. Для решения этой проблемы применён ещё один механизм .NET - генерация исполняемого кода во время выполнения программы (System.Reflection.Emit namespace), что позволяет максимально увеличить производительность и свести использование Reflection только для начальной инициализации.

В отображении участвуют поля и свойства класса, удовлетворяющие следующим требованиям:

  • Модификатор доступа - public, либо internal (работает в случае с динамически генерируемыми классами).
  • Тип является либо скалярным, либо одним из перечисленных: Guid, SqlBinary, SqlBoolean, SqlByte, SqlDateTime, SqlDecimal, SqlDouble, SqlGuid, SqlInt16, SqlInt32, SqlInt64, SqlMoney, SqlSingle, SqlSting, !Stream, XmlReader, XmlDocument.
  • Для поля \ свойства задан MemberMapper (подробнее ниже).
  • Для поля \ свойства задан атрибут MapIgnore(false) (подробнее об атрибутах ниже). В данном случае допускаются к использованию поля типа: byte[], Stream, SqlBytes, SqlChars, SqlXml.

Map & MappingSchema ¶

Как и следовало ожидать, DbManager вовсе не сам выполняет операции по отображению. Эти действия он делегирует объекту класса MappingSchema. В заголовке упомянут так же класс Map - это статический класс, предназначенный для упрощения доступа к функциям MappingSchema, ввиду чего мы не будем подробно его рассматривать, и сосредоточимся на MappingSchema.

Итак, MappingSchema содержит в себе весь необходимый для выполнения отображения контекст, а так же набор семейств функций по отображению ужей на ежей. Более того, MappingSchema содержит так же правила преобразования (конвертации) данных из одного формата в другой (ну, например из Int32 в Boolean, из String в Boolean и наоборот). Все это превращает MappingSchema в мощный инструмент по преобразованию и отображению данных.

Лирическое отступление на тему OdpDataProvider:

Орлы из оракла не пользуются SqlString, SqlInt32 и т.п. типами, а напридумывали велосипедов. Поэтому приходится прилагать так много усилий, чтобы привести всякие OracleDecimal хотя бы к System.Decimal.

Семейства функций MappingSchema ¶

Данные семейства можно разделить на два больших класса:

  • Convert - преобразование данных.
  • Map - отображение данных.

Семейства Convert ¶

Тут все достаточно просто и понятно по семантике методов:

// шаблон имени выгляди следующим образом:
// ConvertToDestinatonType(object value) ;
// где DestinatonType - тип в который необходимо преобразовать.
// к Int32
ConvertToInt32(object value);
// к Int32?
ConvertToNullableInt32(object value);
// к SqlInt32
ConvertToSqlInt32(object value);

По умолчанию MappingSchema делегирует подобные вызовы к классу Convert (BLToolkit.Common.Convert). Данный класс можно расценивать как замену стандартному классу System.Convert, который можно смело назвать "младшим братом", т.к. BLToolkit.Common.Convert значительно превосходит его по возможностям.

Отдельно стоит отметить следующие "высокоуровневые" функции:

public virtual object ConvertChangeType(
   object value,
   Type   conversionType);

public virtual object ConvertChangeType(
   object value,
   Type   conversionType,
   bool   isNullable);

public virtual T ConvertTo<T, P>(P value);

Разберем подробнее параметры

  • value - значение, которое необходимо преобразовать.
  • conversionType - результирующий тип к которому необходимо преобразовать.
  • isNullable - указывает допускает ли результирующий тип значение null.
  • <T, P> - T - результирующий тип, P - исходный тип.

Обратите внимание, что в отличие от System.Converter<Input, Output>, используемого, в частности, System.Array.Convert, параметры типа ConvertTo задаются слева-на-право, т.е. в виде ConvertTo<Output, Input>.

Ввиду прозрачности функций Convert* примеров не будет.

Семейства Map ¶

Вкратце изложу структуру данного раздела: во-первых, я расскажу про общие принципы именования методов и стандартные виды отображений; во-вторых, я более подробно расскажу о том как это все работает на "низком" уровне.

Семантика методов семейства Map следующая: MapSourceToDestination - все просто: отобразить источник на конечную сущность.

Стандартные участники мапинга (в обе стороны):

  • DataReader (!IDataReader).
  • DataRow.
  • DataTable.
  • Dictionary (IDictionary, IDictionary<K, T>).
  • List (IList, IList<T>).
  • Object (бизнес объект).
  • ScalarList(IList, IList<T>).
  • ResultSet.
  • EnumToValue & ValueToEnum.

Как вы уже догадались, в большинстве методов Execute класса DbManager прячется обращение к семейству MapDataReaderToDestination, мы достаточно подробно разобрали методы Execute и их параметры, так что, разобраться с параметрами семейств Map для вас не должно составить особого труда.

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

public void MapSourceToDestination(
   IMapDataSource      source, object sourceObject,
   IMapDataDestination dest,   object destObject,
   params object[]     parameters);

public void MapSourceToDestination(
   object          sourceObject,
   object          destObject,
   params object[] parameters);

public virtual void MapSourceListToDestinationList(
   IMapDataSourceList      dataSourceList,
   IMapDataDestinationList dataDestinationList,
   params object[]         parameters)

Последний метод отличается от двух первых - он отображает списки объектов.

И как всегда, подробнее о параметрах:

  • source - источник, наследник IMapDataSource - предоставляет методы для извлечения данных из источника.
  • dest - получатель, наследник IMapDataDestination - предоставляет методы для записи данных.
  • sourceObject - собственно объект, из которого происходит отображение.
  • destObject - объект в который происходит отображение.
  • parameters - набор параметров передаваемый в конструктор объекта получателя через экземпляр InitContext.
  • dataSourceList и dataDestinationList - то же, что и source и dest, только для списков.

Разберём пример отображения DataReader на Person:

public Person MapDataReaderToPerson(IDataReader reader, Person p)
{
   MappingSchema schema     = new MappingSchema();

   IMapDataSource source    = schema.CreateDataReaderMapper(reader);
   IMapDataDestination dest = schema.GetObjectMapper       (p.GetType());

   Schema.MapDataReaderToObject(source, reader, dest, p);

   return p;
}

Вот, примерно так оно и происходит.

Теперь давайте подробней рассмотрим IDataSource и IDataDestination. Данные интерфейсы описывают методы, которые предоставляют возможности чтения из источника и записи в получателя.

Вкратце рассмотрим данные интерфейсы:

public interface IMapDataSource
{
   // общее количество доступных для чтения полей
   int      Count { get; }

   // тип поля по индексу
   Type     GetFieldType (int index);
   // имя поля по индексу
   string   GetName      (int index);
   // получает индекс поля по имени
   int      GetOrdinal   (string name);
   // получить значение из объекта по заданному индексу
   object   GetValue     (object o, int index);
   // получить значение из объекта по заданному имени
   object   GetValue     (object o, string name);

   // поле по заданному индексу IsNull
   bool     IsNull       (object o, int index);

   // поддерживает типизированные значения для поля по индексу
   bool     SupportsTypedValues(int index);

   // получить типизированное значение по заданному индексу
   SByte    GetSByte     (object o, int index);
   Int16    GetInt16     (object o, int index);
   // и так далее
   // XXX GetXXX (object o, int index);
}

public interface IMapDataDestination
{
   // тип поля по индексу
   Type GetFieldType (int index);
   // получает индекс поля по имени
   int  GetOrdinal   (string name);
   // устанавливает значение value в объекте о по индексу
   void SetValue     (object o, int index,   object value);
   // устанавливает значение value в объекте о по имени
   void SetValue     (object o, string name, object value);

   // устанавливает значение null в объекте по индексу
   void SetNull      (object o, int index);

   // поддерживает типизированные значения для поля по индексу
   bool SupportsTypedValues(int index);

   // устанавливают типизированное значение value в объекте о по byltrce
   void SetSByte     (object o, int index, SByte    value);
   void SetInt16     (object o, int index, Int16    value);
   // и так далее
   // SetXXX(object o, int index, XXX value);
}

Про поддержку типизированных значений стоит написать отдельно, и не своими словами:

В интерфейсах IMapDataSource и IMapDataDestination есть методы типа

Int32    GetInt32     (object o, int index);

Означающие "возьмите у объекта o поле за номером index и верните его как int".

Смысл в том, что если у нас есть миллион объектов с двумя полями типа int, то при мапинге через GetValue/SetValue половина работы уходит на boxing/unboxing.

Т.е. CLR на полном серьёзе выделяет в куче 2 миллиона маленьких объектов, оборачивает в них наши числа и потом как-нибудь эти 2 миллиона объектов высвобождает.

Получаем на ровном месте фрагментацию памяти и лишние вызовы сборщика мусора.

При мапинге через TypedValues boxing'а не происходит. GetInt32 вычитывает целое число и сохраняет его в регистр EAX. SetInt32 берёт из EAX и выставляет нашему полю это значение. Если поле имеет тип, например, Int64, то код будет более мудрёным: destMapper.SetInt64(destObj,dstIndex,Converter.ConvertInt32ToInt64(srcMapper.GetInt32(srcObj, srcIndex));

Опять-таки никакого выделения/освобождения памяти.

Так вот, SupportsTypedValues как раз и сообщает маперу, что источник/получатель умеет работать с числами, датами и т.п. без boxing'а.

Дополню, что SupportsValueTypes работает в паре с GetFieldType, который должен сообщить правильный тип поля.

Для отображения списков (коллекции, таблицы и т.п.) существуют еще два дополнительных интерфейса:

public interface IMapDataSourceList
{
   void InitMapping      (InitContext initContext);
   bool SetNextDataSource(InitContext initContext);
   void EndMapping       (InitContext initContext);
}

public interface IMapDataDestinationList
{
   void                InitMapping       (InitContext initContext);
   IMapDataDestination GetDataDestination(InitContext initContext);
   object              GetNextObject     (InitContext initContext);
   void                EndMapping        (InitContext initContext);
}

InitMapping и EndMapping - инициализация и окончание отображения. В остальном, все сводится к тому, что при маппинге списков производится поочередное отображение каждого их элемента.

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

Теперь вы получили представление о том, как работает отображение объектов. Но это еще не все. Кроме представлений объектов, есть еще представления полей. Для этого используются ValueMapper и MemberMapper. Первый используется для отображения скалярных полей, второй - всех прочих. Механизм ValueMapper инкапсулирован в BLT, поэтому рассматривать мы его не будем. А вот MemberMapper мы рассмотрим более подробно. MemberMapper позволяет вам: ну тут проще показать. Приведу простой пример: допустим, у некоторого объекта есть свойство со словарем строк. При сохранении данного объекта необходимо так же сохранить и словарь.

public class SimpleDictionaryMapper : MemberMapper
{
   public override object GetValue(object o)
   {
      Dictionary<string, string> dic = base.GetValue(o) as Dictionary<string, string>;

      if (dic == null) return null;

      StringBuilder sb = new StringBuilder();
      foreach (string key in dic.Keys)
         sb.AppendFormat("{0}={1};", key, dic[key]);

      return sb.ToString();

   }

   public override void SetValue(object o, object value)
   {
      string s = MappingSchema.ConvertToString(value);

      if (s == string.Empty) base.SetValue(o, null);

      Dictionary<string, string> dic = new Dictionary<string, string>();

      foreach (string pair in s.Split(';'))
      {
         if (pair.Length < 3) continue;

         string[] keyValue = pair.Split('=');

         if (keyValue.Length != 2) continue;

         dic.Add(keyValue[0], keyValue[1]);
      }

      base.SetValue(o, dic);
   }
}

Приведенный пример отображает словарь на строку в заданном формате (GetValue) при обратном отображении (SetValue) данная строка разбирается и из нее заново собирается словарь.

Используется MemberMapper следующим образом:

public class TestObject
{
   [MemberMapper(typeof(SimpleDictionaryMapper)]
   public Dictionary<string, string> Dictionary;
}

Метаданные ¶

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

Метаданные задаются двумя способами:

  1. Атрибутами.
  2. XML расширениями.

Первый механизм является статическим, второй позволяет менять правила игры в динамике.

Атрибуты ¶

MapFieldAttribute - позволяет изменять алиасы полей, участвующих в маппинге. Мы уже использовали данный атрибут, для изменения алиаса поля ID класса Person. Но у данного атрибута есть еще некоторые применения:

// Задает алиас полю Field1.
// Данный подход можно использовать для "переименования" унаследованных полей.
[MapField("MapName", "Field1")]
public class Object1
{
   public int Field1;
   [MapField("intfld")]
   public int Field2;
}

[MapValue(true,  "Y")]
[MapValue(false, "N")]
public class Object2
{
   public bool Field1;
   public int  Field2;
}

public class Object3
{
   public Object2 Object2 = new Object2();
   public Object4 Object4;
}

//При необходимости пожно задать алиасы для полей вложенных объектов.
[MapField("fld1", "Object3.Object2.Field1")]
[MapField("fld2", "Object3.Object4.Str1")]
public class Object4
{
   public Object3 Object3 = new Object3();
   public string  Str1;
   // Простой способ для отображения вложенных объектов и их полей -
   // задать формат алиаса.
   [MapField(Format="InnerObject_{0}"]
   public Object2 InnerObject = new Object2();
}

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

public class Object1
{
   [MapValue(true,  "Y")]
   [MapValue(false, "N")]
   public bool Bool1;

   [MapValue(true,  "Y", "Yes")]
   [MapValue(false, "N", "No")]
   public bool Bool2;
}

Использовать атрибут можно так же и на весь класс, задавая таким образом синонимы по умолчанию для полей данного типа:

[MapValue(true,  "Y")]
[MapValue(false, "N")]
public class Object2
{
   public bool Bool1;

   [MapValue(true,  "Y", "Yes")]
   [MapValue(false, "N", "No")]
   public bool Bool2;
}
  • MapIgnoreAttribute - поле, помеченное данным атрибутом будет проигнорировано при отображении.
  • MemberMapperAttribute - позволяет задать для поля специфический MemberMapper, пример использования был выше.
  • NullableAttribute - позволяет указать системе, что значение данного поля может принимать null.
  • NullValueAttribute - позволяет задать для поля значение, которое будет интерпретироваться как DbNull. Для данного атрибута есть несколько "специализированых" наследников: NullDateTimeAttribute, NullDecimalAttribute, NullGuidAttribute
  • TrimmableAttribute - используется для строк. Строковые поля, помеченные данным аттрибутом автоматически "тримятся" при маппинге.

XML ¶

coming soon?... :)

DataAccess ¶

Здесь

DataAcessor ¶

Здесь

SqlQuery ¶

Здесь

Download in other formats:

  • Plain Text

Powered by Trac 0.11.6
By Edgewall Software.

RSDN projects space http://projects.rsdn.ru


DataAccess – BLToolkit

  • About Trac
  • Wiki
  • Timeline
  • Roadmap
  • Browse Source
  • View Tickets
  • Search

Context Navigation

  • Start Page
  • Index
  • History
  • Last Change

  1. Реализация слоя доступа к данным при помощи BLToolkit
    1. BLToolkit.DataAccess
      1. Общие правила использования
      2. SqlQuery
      3. DataAccessor
        1. Возвращаемое значение
        2. Имя метода
        3. Параметры
        4. Атрибуты
    2. Рекомендации по использванию
      1. Реализация методов ручками
      2. Генерация SQL запросов в абстрактном аксессоре
    3. Полезные ссылки

Реализация слоя доступа к данным при помощи BLToolkit ¶

BLToolkit.DataAccess ¶

Пространство имен BLToolkit.DataAccess содержит набор классов, позволяющих легко разделить слой модели домена со слоем доступа к данным, с одной стороны, и «автоматизировать» труд программиста с другой. Вкратце:

  • DataAccessor – используется для динамической генерации классов, осуществляющих доступ к данным и отображение данных на объекты и наоборот.
  • SqlQuery – используется так же для доступа к данным и отображения, отличие от первого в том, что в данном случае динамически генерируются SQL запросы к БД.

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

Общие правила использования ¶

  • Если в конструкторе (методе CreateInstance) аксесссору в качестве параметра передается экземпляр DbManager, то все обращения к базе производятся через данный объект. То же справдедливо и для методов. За DbManager.Dispose() в этом случае отвечает вызывающий. Иными словами передавая аксессору DbManager вы можете выполнить несколько действий в одном соединении.
  • Если ни в конструкторе, ни в методе в качестве параметра не передан экземпляр DbManager, то в методе будет создан и уничтожен свой экзкмпляр. Иными словами - каждый метод будет открывать и закрывать соединение с БД.

SqlQuery ¶

Как было сказано выше класс SqlQuery автоматически (на основе метаданных) генерирует SQL запросы, а именно: вставка (Insert) удаление (Delete, DeleteByKey), обновление (Update), выборка (Select, SelectByKey). Как было сказано, для генерации запросов используются метаданные. «Расширить» метаданные можно как при помощи XML-расширений, так и атрибутами. Рассмотрим используемые атрибуты:

TableNameAttribute – указывает имя таблицы для бизнес объекта (по умолчанию имя таблицы совпадает с именем класса).
MapFieldAttribute – позволяет изменять алиасы полей, участвующих в маппинге. Подробнее здесь.
PrimaryKeyAttribute – указывает на то что данное поле используется в качестве первичного ключа в таблице. Т.е. данные поля будут использованы в условии WHERE для выборки, удаления либо обновления. Атрибут может быть задан для нескольких полей.
NonUpdatableAttribute – поле не будет обновлено в инструкции Update. Так же данное поле не будет использоваться в Insert запросах.
SqlIgnoreAttribute - позволяет явно указать использовать ли данное поля при генерации SQL запросов.
Рассмотрим пример использования (в примере я использую типизированную версию SqlQuery – SqlQuery<T>, просто лень писать приведение типов ;) ):

public enum Gender
{
        [MapValue("F")] Female,
        [MapValue("M")] Male,
        [MapValue("U")] Unknown,
        [MapValue("O")] Other
}
public class Person
{
        [MapField("PersonID"), NonUpdatable, PrimaryKey]
        public int    ID;
        public string FirstName;
        public string MiddleName;
        public string LastName;
        public Gender Gender;
}

[Test]
public void SqlQueryTest()
{
    SqlQuery<Person> da = new SqlQuery<Person>();

    Person p1 = da.SelectByKey(1);

    Assert.IsNotNull(p1);
    Assert.AreEqual(1,           p1.ID);
    Assert.AreEqual("John",      p1.FirstName);
    Assert.AreEqual(Gender.Male, p1.Gender);

    p1.ID        = 101;
    p1.FirstName = "John II";

    int r = da.Update(p1);

    Assert.AreEqual(1, r);

    Person p2 = da.SelectByKey(1);

    Assert.IsNotNull(p2);
    Assert.AreEqual(1,           p2.ID);
    Assert.AreEqual("John II",   p2.FirstName);
    Assert.AreEqual(Gender.Male, p2.Gender);

    da.Delete(p1); // da.DeleteByKey(1);

    p2 = da.SelectByKey(p1);

    Assert.IsNull(p2);

    List<Person> persons = da.SelectAll();

    Assert.IsNotNull(persons);
}

В ходе данного теста были сгенерированы и выполнены следующие запросы:

-- SelectByKey
SELECT
        [PersonId],
        [FirstName],
        [MiddleName],
        [LastName],
        [Gender]
FROM [Person]
WHERE [PersonId] = @PersonId_W

-- Update
UPDATE [Person]
SET
        [FirstName] = @FirstName,
        [MiddleName] = @MiddleName,
        [LastName] = @LastName,
        [Gender] = @Gender
WHERE [PersonId] = @PersonId_W

-- Delete, DeleteByKey
DELETE FROM [Person]
WHERE [PersonId] = @PersonId_W

-- SelectAll
SELECT
        [PersonId],
        [FirstName],
        [MiddleName],
        [LastName],
        [Gender]
FROM [Person]

Как вы видите в коде запросов используются символы экранирования и имена параметров специфичные для MS SQL Server, поэтому следует оговориться, что в генерации запросов участвует DataProvider, а если быть точным то его метод Convert(…), именно через него код запроса «наделяется» спецификой конкретного сервера. Если бы запрос генерировался с использованием OdpDataProvider то он бы выглядел примерно так:

SELECT
        PersonId,
        FirstName,
        MiddleName,
        LastName,
        Gender
FROM Person
WHERE PersonId = :PersonId_W

А для FdpDataProvider так:

SELECT
        "PersonId",
        "FirstName",
        "MiddleName",
        "LastName",
        "Gender"
FROM "Person"
WHERE "PersonId" = @PersonId_W

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

DataAccessor ¶

В отличие от SqlQuery, используемого для генерации SQL запросов, DataAccessor используется для эмита кода, что избавляет программиста от нудных и рутинных операций. Для начала рассмотрим небольшой пример:

namespace DataAccessorTest
{
    public enum Gender
    {
                [MapValue("F")] Female,
                [MapValue("M")] Male,
                [MapValue("U")] Unknown,
                [MapValue("O")] Other
    }
        public class Person
    {
                [MapField("PersonID")]
                public int    ID;
                public string FirstName;
                public string MiddleName;
                public string LastName;
                public Gender Gender;
    }

    public abstract class PersonAccessor : DataAccessor<Person, PersonAccessor>
    {
         [SqlQuery(@"
               SELECT
                   p.PersonId,
                   p.FirstName,
                   p.SecondName,
                   p.MiddleName,
                   p.Gender
               FROM Person p
               WHERE p.PersonId = @PersonId")]
        public abstract Person GetPerson(int personId);
    }

    [Test]
    public void Test()
    {
        PersonAccessor pa = PersonAccessor.CreateInstance();

        Person p = pa.GetPerson(1);

        Assert.IsNotNull(p);
        Assert.AreEqual("John", p.FirstName);
    }
}

Как видно, класс PersonAccessor – абстрактный, абстрактным так же является его метод GetPerson. При обращении к PersonAccessor.CreateInstance() BLToolkit эмитит сборку, где определяет наследника от PersonAccessor и реализует абстрактные методы, ссылку на экземпляр этого наследника и возвращает CreateInstance(). Если посмотреть на реализацию этого класса (допустим, при помощи Reflector), то мы увидим примерно следующее:

[BLToolkitGenerated]
public class PersonAccessor : DataAccessorTest.PersonAccessor
{
    DataAccessorTest.Person person = null;
    DbManager dbManager = this.GetDbManager();
    try
    {
        Type type = typeof(DataAccessorTest.Person);
        IDbDataParameter[] parameters = new IDbDataParameter[]
            {
                dbManager.Parameter(
                    this.GetQueryParameterName(dbManager, "personId"),
                    personId) };

        person = (DataAccessorTest.Person) dbManager
            .SetCommand("SELECT p.PersonId, p.FirstName, p.SecondName, p.MiddleName, p.Gender FROM Person WHERE p.PersonId = @PersonId", this.PrepareParameters(dbManager, parameters))
            .ExecuteObject(type);
    }
    finally
    {
        this.Dispose(dbManager);
    }
    return person;
}

Если не вдаваться в детали, то BLToolkit за нас написал примерно следующее:

    using (DbManager db = new DbManager())
    {
        return db
            .SetCommand@"
                         SELECT
                             p.PersonId,
                             p.FirstName,
                             p.SecondName,
                             p.MiddleName,
                             p.Gender
                         FROM Person p
                         WHERE p.PersonId = @PersonId",
                db.Parameter("@PersonId", personId)
            .ExecuteObject(typeof(Person));
    }

Иными словами, при помощи DataAccessor можно задекларировать какой код нам нужен для работы с данными, и по этой декларации BLToolkit сгенерирует за программиста этот код. Декларация происходит методом объявления наследника от DataAccessor и объявления в нем методов, каждая часть объявления метода имеет свое значение, а именно:

  • Возвращаемое значение.
  • Имя метода.
  • Параметры.
  • Атрибуты, примененные к методу и его параметрам.

Возвращаемое значение ¶

По типу возвращаемого значения определяется метод DbManager, который следует вызвать:

Тип | Метод
IDataReader | ExecuteDataReader
Наследник от DataSet | ExecuteDataSet
Наследник от DataTable | ExecuteDataTable
Наследник от IList | ExecuteList или ExecuteScalarList
Наследник от IDictionary | ExecuteDictionary или ExecuteScalarDictionary
void | ExecuteNonQuery
ValueType (int, string, byte[]) | ExecuteScalar
Иное | ExecuteObject

Имя метода ¶

По умолчанию DataAccessor использует хранимые процедуры. По имени метода определяется имя хранимой процедуры. Нотация следующая: [Имя типа]_[Имя Процедуры]

public abstract List<Person> SelectAll() // вызовет Person_SelectAll
public abstract void Insert(Person p) // вызовет Person_Insert

Переопределить подобное поведение можно написав своего наследника от DataAccessor и перегрузив в нем метод GetDefaultSpName.

Параметры ¶

Параметры используются как параметры запросов. Для параметра запроса используется имя параметра функции. Переопределить данное поведение можно, перегрузив функции GetQueryParameterName и/или GetSpParameterName.
Если в качестве параметра передан не примитивный тип, то параметры из него будут созданы при помощи метода DbManager.CreateParameters(…).
Если в качестве параметра будет передан экземпляр DbManager, то он будет использован для выполнения запроса.

Атрибуты ¶

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

ActionNameAttribute – позволяет задать имя действия для хранимой процедуры.
ActionSprocNameAttribute – позволяет ассоциировать метод аксессора с хранимой процедурой.
SprocNameAttribute – позволяет явно задать имя хранимой процедуры.
Рассмотрим несколько примеров (все методы в результате вызывают хранимку Person_SelectAll):

[SprocActionName("GetPersons", "Person_SelectAll")]
public abstract class PersonAccessor : DataAccessor<Person, PersonAccessor>
{
    public abstract List<Person> SelectAll();

    [Action("SelectAll")]
    public abstract List<Person> GetAll();

    [SprocName("Person_SelectAll")]
    public abstract List<Person> LoadPersons();

    public abstract List<Person> GetPersons();
}

DiscoverParametersAttribute – при использовании хранимых процедур через DataAccessor по умолчанию полагается, что имена параметров функции совпадают с именем параметров хранимки, в таком случае порядок параметров в функции не имеет значения. Если же к функции применен данный атрибут BLToolkit получит информацию о параметрах хранимой процедуры и применит ее к параметрам метода по порядку, в данном случае имена параметров будут проигнорированы.

[TestFixture]
public class DiscoverParameters
{
    public abstract class PersonAccessor : DataAccessor
    {
        [DiscoverParameters]
        public abstract Person SelectByName(string anyParameterName, string rParameterName);
    }

    [Test]
    public void Test()
    {
        PersonAccessor pa = DataAccessor.CreateInstance<PersonAccessor>();
        Person         p  = pa.SelectByName("Tester", "Testerson");

        Assert.AreEqual(2, p.ID);
    }
}

SqlQueryAttribute – говорит методу, что следует выполнить указанный SQL запрос. Выше уже приводился стандартный метод использования данного атрибута. Кроме того, у атрибута есть свойство IsDynamic?. Если данное свойство выставлено в true то для получения кода запроса используется метод атрибута GetSqlText?(…).
Рассмотрим пример:

public class TestQueryAttribute : SqlQueryAttribute
{
        public TestQueryAttribute()
        {
                IsDynamic = true;
        }

        private string _oracleText;
        public  string  OracleText
        {
                get { return _oracleText;  }
                set { _oracleText = value; }
        }

        public override string GetSqlText(DataAccessor accessor, DbManager dbManager)
        {
                switch (dbManager.DataProvider.Name)
                {
                        case "Sql"   :
                        case "SqlCe" : return SqlText;

                        case "Oracle":
                        case "ODP"   : return OracleText ?? SqlText;
                }

                throw new ApplicationException(string.Format("Unknown data ider '{0}'", dbManager.DataProvider.Name));
        }
}

public abstract class PersonAccessor : DataAccessor<Person, Person>
{
        [TestQuery(
                SqlText    = "SELECT * FROM Person WHERE LastName = @lastName",
                OracleText = "SELECT * FROM Person WHERE LastName = :lastName")]
        public abstract List<Person> SelectByLastName(string lastName);
}

В данном примере, в зависимости от имени используемого DataProvider-а выполняются разные запросы.

FormatAttribute – применяется к параметру, указывает, что данный параметр используется для генерации текста SQL запроса или имени хранимой процедуры.

public abstract class PersonAccessor : DataAccessor
{
    [SqlQuery("SELECT TOP {0} * FROM Person")]
    public abstract List<Person> GetPersonList([Format] int top);
}

[Test]
public void Test()
{
    PersonAccessor pa   = DataAccessor.CreateInstance<PersonAccessor>();
    List<Person>   list = pa.GetPersonList(2); // SELECT TOP 2 * FROM Person

    Assert.That(list,       Is.Not.Null);
    Assert.That(list.Count, Is.LessThanOrEqualTo(2));
}

ParamNameAttribute – позволяет явно задать имя параметра.
ParamDbTypeAttribute – позволяет явно задать тип параметра.
ParamSizeAttribute – позволяет явно задать размер параметра.
ParamNullValueAttribute – позволяет указать какое значение параметра считать за NULL.

public abstract class TestAccessor : DataAccessor
{

    [SqlQuery("SELECT * FROM Person WHERE PersonID = @personId")]
    public abstract Person SelectByKey([ParamName("personId")]int id);

    // при id == 1 значение параметра будет заменено на NULL
    public abstract Person SelectByKey([ParamNullValue(1)] int id);

    [SqlQuery("SELECT {0} = {1} FROM Person WHERE PersonID = 1")]
    public abstract void SelectJohn(
        [ParamSize(50), ParamDbType(DbType.String)] out string name,
        [Format] string paramName,
        [Format] string fieldName);
}

[Test]
public void AccessorTest()
{
    using (DbManager db = new DbManager())
    {
        TestAccessor ta = DataAccessor.CreateInstance<TestAccessor>(db);

        string actualName;

        // SELECT @name  = FirstName FROM Person WHERE PersonID = 1
        // полученое значение будет возвращено в параметр name
        ta.SelectJohn(out actualName, "@name", "FirstName");

        Assert.AreEqual("John", actualName);
    }
}

DirectionAttribute – позволяет для параметра явно задать его направление.
DestinationAttribute – указывает, что в данный параметр следует отмапить выбранные значения.

public abstract class PersonAccessor : DataAccessor<Person, PersonAccessor>
{
    [SqlQuery("SELECT * FROM Person")]
    public abstract void SelectAll([Destination]List<Person> list);

    // CREATE Procedure Person_Insert_OutputParameter
    //   @FirstName  nvarchar(50),
    //   @LastName   nvarchar(50),
    //   @MiddleName nvarchar(50),
    //   @Gender     char(1),
    //   @PersonID   int output
    //   AS
    //
    //     INSERT INTO Person
    //       ( LastName,  FirstName,  MiddleName,  Gender)
    //     VALUES
    //       (@LastName, @FirstName, @MiddleName, @Gender)
    //
    //     SET @PersonID = Cast(SCOPE_IDENTITY() as int)
    [SprocName("Person_Insert_OutputParameter")]
    public abstract void Insert([Direction.Output("PersonId")] Person p);
}

IndexAttribute – позволяет указать индекс для словаря (по умолчанию используется первичный ключ):

public abstract class PersonAccessor : DataAccessor<Person>
{
    // Для ключа словаря будет использован первичный ключ объекта Person,
    // т.е. поля, помеченные атрибутом PrimaryKey
    [ActionName("SelectAll")]
    public abstract Dictionary<int,Person> GetPersonDictionary1();

    // Явно задаем индекс. ID – поле класса Person.
    //
    [ActionName("SelectAll")]
    [Index("ID")]
    public abstract Dictionary<int,Person> GetPersonDictionary2();

    // Явно задаем индекс.
    // "@PersonID"- поле полученного в результате выборки кортежа!.
    // Важно: собачка - '@' заставляет BLToolkit для индекса
    // брать значения из полей кортежа!
    //
    [ActionName("SelectAll")]
    [Index("@PersonID")]
    public abstract Dictionary<int,Person> GetPersonDictionary3();

    // Будет вычитан словарь со скалярнми величинами.
    //
    [SqlQuery("SELECT PersonID, FirstName FROM Person")]
    [Index("PersonID")]
    [ScalarFieldName("FirstName")]
    public abstract Dictionary<int,string> GetPersonNameDictionary();
}

ObjectTypeAttribute – явно задает тип возвращаемого абстрактным методом объекта.
ActualTypeAttribute – явно задает тип возвращаемого абстрактным методом объекта, имеет более низкий приоритет чем ObjectType.

public interface IName
{
    string Name { get; }
}

public class NameBase : IName
{
    private string _name;
    public  string  Name { get { return _name; } set { _name = value; } }
}

public class Name1 : NameBase {}
public class Name2 : NameBase {}

[ActualType(typeof(IName), typeof(Name1))]
public abstract class TestAccessor : DataAccessor
{
    // Вернет объект класса Name1
    [SqlQuery("SELECT 'John' as Name")]
    public abstract IName GetName1();

    // Вернет объект класса Name2
    [SqlQuery("SELECT 'John' as Name"), ObjectType(typeof(Name2))]
    public abstract IName GetName2();

    [SqlQuery("SELECT 'John' as Name")]
    public abstract IList<IName> GetName1List();

    [SqlQuery("SELECT 'John' as Name"), ObjectType(typeof(Name2))]
    public abstract IList<IName> GetName2List();

    [SqlQuery("SELECT 1 as ID, 'John' as Name"), Index("@ID")]
    public abstract IDictionary<int, IName> GetName1Dictionary();

    [SqlQuery("SELECT 1 as ID, 'John' as Name"), Index("@ID"),
     ObjectType(typeof(Name2))]
    public abstract IDictionary<int, IName> GetName2Dictionary();
}

[ObjectType(typeof(Person)]
public abstract class PersonAccessor : DataAccessor
{
    public abstract ArrayList SelectAll();

    public abstract object SelectByKey(int personId);
}

Рекомендации по использванию ¶

Реализация методов ручками ¶

Эмит кода, это конечно хорошо, но переодически возникает необходимость сделать метод руками, в таком случае рекомендуется делать это так:

public class MyAccessor : DataAccessor
{
    public List<Person> SelectAll()
    {
        DbManager db = GetDbManager();
        try
        {
            return SelectAll(db);
        }
        finally
        {
            Dispose(db);
        }
    }

    public SelectAll(DbManager db)
    {
        return db.SetCommand("SELECT * FROM Person").ExecuteList<Person>();
    }
}

Это типовой шаблон реализации методов как для всех наследников DataAccessorBase, коими являются как DataAccessor так и SqlQuery. Ключевыми являются использование функций GetDbManager() и Dispose(DbManager db). Первый возвращает эеземпляр DbManager, переданный в конструктор, если передавался, иначе новый экзкмпляр. Второй освобождает экземпляр DbManager, в случае если оный не был передан через конструктор.

Генерация SQL запросов в абстрактном аксессоре ¶

Абстрактные аксессоры не поддерживают генерацию SQL запросов, однако, при необходимости можно реализовать своего наследника, допустим, следующего вида:

public abstract class MyAccessorBase<T, TA> : DataAccessor<T, TA> where TA : DataAccessor<T>
{
        SqlQuery<T> _query = new SqlQuery<T>();

        private delegate R Func<P1, P2, R>(P1 par1, P2 par2);
        private delegate R Func<P1, R>(P1 par1);

        private R Exec<R, P>(Func<DbManager, P, R> op, P obj)
        {
                DbManager db = GetDbManager();
                try
                {
                        return op(db, obj);
                }
                finally
                {
                        Dispose(db);
                }
        }

        private R Exec<R>(Func<DbManager, R> op)
        {
                DbManager db = GetDbManager();
                try
                {
                        return op(db);
                }
                finally
                {
                        Dispose(db);
                }
        }

        public virtual int  Insert(T obj)
        {
                return Exec<int, T>(Insert, obj);
        }

        public virtual int Insert(DbManager db, T obj)
        {
                return _query.Insert(db, obj);
        }

        public virtual int Update(T obj)
        {
                return Exec<int, T>(Update, obj);
        }

        public virtual int Update(DbManager db, T obj)
        {
                return _query.Update(db, obj);
        }

        public virtual int Delete(T obj)
        {
                return Exec<int, T>(Delete, obj);
        }

        public virtual int Delete(DbManager db, T obj)
        {
                return _query.Delete(db, obj);
        }

        public virtual int DeleteByKey(object[] keys)
        {
                return Exec<int, object[]>(DeleteByKey, keys);
        }

        public virtual int DeleteByKey(DbManager db, object[] keys)
        {
                return _query.DeleteByKey(db, keys);
        }

        public virtual List<T> SelectAll()
        {
                return Exec<List<T>>(SelectAll);
        }

        public virtual List<T> SelectAll(DbManager db)
        {
                return _query.SelectAll(db);
        }

        public virtual T SelectByKey(object[] keys)
        {
                return Exec<T, object[]>(SelectByKey, keys);
        }

        public virtual T SelectByKey(DbManager db, object[] keys)
        {
                return _query.SelectByKey(db, keys);
        }
}

Полезные ссылки ¶

  • Описание DataAccess на англ. языке
  • Как использовать генерацию вызова сохранённых процедур

Download in other formats:

  • Plain Text

Powered by Trac 0.11.6


ComplexMapping – BLToolkit

  • Roadmap
  • Browse Source
  • View Tickets
  • Search

Context Navigation

  • Start Page
  • Index
  • History
  • Last Change

Иерархия объектов, Order-Details ¶

Кроме всего прочего BLToolkit позволяет осуществлять маппинг сложных объектов, типа Order-Details.
Реализовано это при помощи MappingSchema.MapResultSets(...)
Пример использования

Декларативный маппинг ¶

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

      sets[0].AddRelation(sets[1], "ParentID", "ParentID", "Children");
      sets[1].AddRelation(sets[0], "ParentID", "ParentID", "Parent");
      sets[1].AddRelation(sets[2], "ChildID",  "ChildID",  "Grandchildren");
      sets[2].AddRelation(sets[1], "ChildID",  "ChildID",  "Child");

Общие правила следующие:

  • Для отношений "один-ко-многим" необходимо явно указывать тип подчиненного объекта. Для отношений "один-к-одному" тип объекта берется из типа свойстав\поля (если не задан явно).
  • Если яано не заданы индексы (ключевые поля для мастера и слейва) то они определяются из полей, помеченных как PrimaryKey. Для "один-ко-многим" берется первичный ключ мастера, для "один-к-одному" - слейва. Если явно не задан ключ для мастера, то считается, что он совпадает с ключем для слейва. В качестве индексов указываются алиасы для маппинга.

Атрибуты ¶

RelationAttribute

  • Destination - тип подчиненного объекта, необходимо указывть для полей\свойств, унаследованных от IEnumerable ("один-ко-многим").
  • MasterIndex(1, 2, 3) SlaveIndex(1, 2, 3) - позволяют явно задавать индексы мастера и слейва, соотвественно. Всего можно указать до 3 ключевых полей для каждого индекса (надеюсь, никому не повезет на столько, что понадобятся 4 :) ).

Пример использования (аналогичен приведенному выше):

        public class Parent
        {
            [MapField("ParentID"), PrimaryKey]
            public int ID;

            [Relation(typeof(Child))]
            public List<Child> Children = new List<Child>();
        }

        [MapField("ParentID", "Parent.ID")]
        public class Child
        {
            [MapField("ChildID"), PrimaryKey]
            public int ID;

            [Relation]
            public Parent Parent = new Parent();

            [Relation(typeof(Grandchild))]
            public List<Grandchild> Grandchildren = new List<Grandchild>();
        }

        [MapField("ChildID", "Child.ID")]
        public class Grandchild
        {
            [MapField("GrandchildID"), PrimaryKey]
            public int ID;

            [Relation]
            public Child Child = new Child();
        }
        [Test]
        public void Test2()
        {
            List<Parent>   parents = new List<Parent>();
            MapResultSet[] sets    = new MapResultSet[3];

            sets[0] = new MapResultSet(typeof(Parent), parents);
            sets[1] = new MapResultSet(typeof(Child));
            sets[2] = new MapResultSet(typeof(Grandchild));

            using (DbManager db = new DbManager())
            {
                db
                    .SetCommand(TestQuery)
                    .ExecuteResultSet(sets);
            }

            Assert.IsNotEmpty(parents);
        //...
        }

XML ¶

Так же аналогично можно задать через XML расширение (пример приведен ниже).
Важно:

  • В приведенном примере, для типа ChildEx специально не указан PrimaryKey - для того, что бы продемонстрировать то, как вручную задавать индексы. Количество элементов MasterIndex и SlaveIndex - не ограничено (в отличии от использования RelationAttribute).
  • DestinationType - вообще задается в виде так называемого AssemblyQualifiedName (тип с указанием сборки). Если сборка, где расположен подчиненный тип, совпадает со сборкой мастера, то имя сборки можно опустить. В примере эно показано на: ParentEx.Children - тип указан с именем сборки; ChildEx.Grandchildren - имя сборки опущено.

RelationExtension.xml:

<?xml version="1.0" encoding="utf-8" ?>
<Types
    xmlns   = "urn:schemas-bltoolkit-net:typeext"
    xmlns:m = "urn:schemas-bltoolkit-net:mapping">

    <Type Name="HowTo.Data.ComplexMapping+ParentEx">
        <Member Name="ID" MapField="ParentID" PrimaryKey="1"/>
        <Member Name="Children">
            <Relation DestinationType = "HowTo.Data.ComplexMapping+ChildEx, HowTo"/>
        </Member>
    </Type>

    <Type Name="HowTo.Data.ComplexMapping+ChildEx">
        <Member Name ="ID" MapField="ChildID"/>
        <Member Name ="Parent">
            <Relation/>
        </Member>
        <m:MapField OrigName="Parent.ID" MapName="ParentID"/>
        <Member Name ="Grandchildren">
            <Relation DestinationType="HowTo.Data.ComplexMapping+GrandchildEx">
                <SlaveIndex  Name="ChildID"/>
                <MasterIndex Name="ChildID"/>
            </Relation>
        </Member>
    </Type>

    <Type Name="HowTo.Data.ComplexMapping+GrandchildEx">
        <Member Name="ID" MapField="GrandchildId" PrimaryKey="1"/>
        <m:MapField OrigName="Child.ID" MapName="ChildID"/>
        <Member Name="Child">
            <Relation>
                <SlaveIndex  Name="ChildID"/>
                <MasterIndex Name="ChildID"/>
            </Relation>
        </Member>
    </Type>

</Types>

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

                public class ParentEx
                {
                        public int           ID;
                        public List<ChildEx> Children = new List<ChildEx>();
                }

                public class ChildEx
                {
                        public int                ID;
                        public ParentEx           Parent        = new ParentEx();
                        public List<GrandchildEx> Grandchildren = new List<GrandchildEx>();
                }

                public class GrandchildEx
                {
                        public int     ID;
                        public ChildEx Child = new ChildEx();
                }

                [Test]
                public void Test3()
                {
                        List<ParentEx> parents = new List<ParentEx>();
                        MapResultSet[] sets    = new MapResultSet[3];

                        sets[0] = new MapResultSet(typeof(ParentEx), parents);
                        sets[1] = new MapResultSet(typeof(ChildEx));
                        sets[2] = new MapResultSet(typeof(GrandchildEx));

                        MappingSchema ms = new MappingSchema();
                        ms.Extensions = TypeExtension.GetExtensions("RelationExtension.xml");

                        using (DbManager db = new DbManager())
                        {
                                db.MappingSchema = ms;

                                db
                                        .SetCommand(TestQuery)
                                        .ExecuteResultSet(sets);
                        }

                        Assert.IsNotEmpty(parents);
                //...
                }

Download in other formats:

  • Plain Text


EditableObjects – BLToolkit

  • Search

Context Navigation

  • Start Page
  • Index
  • History
  • Last Change

  1. EditableObjects, создание объектов бизнес-логики
    1. Использование абстрактных классов
      1. Семейство функций TypeAccessor.CreateInstance
      2. Атрибуты
    2. EditableObjects
      1. В рукаве ничего нет и шляпа пустая
      2. EditableObjectHolder
    3. EditableArrayList, EditableList<T>
    4. Полезные ссылки

EditableObjects, создание объектов бизнес-логики ¶

Использование абстрактных классов ¶

В своих недрах BLT достаточно широко использует методику генерации абстрактных классов. Выглядит это так: программист пишет абстрактный класс, таким образом декларируя свои намеренья о том, как этот класс должен работать. После чего BLT по этой декларации на лету генерирует то, что программисту было лень писать руками...
Зачем это надо? Ну, для начала это облегчает труд разработчика, снимая с него необходимость рутинного вбивания кода (для примера можно посмотреть на DataAccessor). Далее, дает возможность использовать аспекты. Внутри BLT это так же пользуется для повышения производительности (фактически внутри BLT генерирует очень много маленьких классиков заточенных на решение конкретных задач). Недостатком можно считать издержки первого вызова - дополнительные временные затраты на эмит сборок с реализацией абстрактных классов (хотя, данная проблема может быть снята, допустим, утилитой BLTGen).
Итак, кроме механизмов для доступа к данным и отображения данных на бизес-объекты, BLT предоставляет разработчику так же инструментарий для создания иерархии этих самых бизнес объектов.
В простейшем варианте можно сэкономить кучу времени на создании абстрактных свойств. Приведу пример:

public class Person
{
    private int _id;
    public  int Id { get { return _id; } set { _id = value; } }

    private string _firstName;
    public  string FirstName { get { return _firstName; } set { _firstName = value; } }

    // далее следует определение еще кучи свойств…
}

public void SomeMethod()
{
    Person p = new Person();
    //...
}

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

public abstract class Person
{
    public abstract int Id { get; set; }

    public abstract string FirstName { get; set; }

    // далее следует определение еще кучи свойств…
}

public void SomeMethod()
{
    Person p = TypeAccessor.CreateInstance<Person>();
    //...
}

В чем разница? В том, что на набивку второго примера я потратил как минимум вдвое меньше времени (ну ладно… потратил бы если бы не использовал копипаст). Это раз. И для создания экземпляра класса используется семейство функций TypeAccessor.CreateInstance(). Это два. На втором моменте остановимся подробней.

Семейство функций TypeAccessor.CreateInstance ¶

public static object CreateInstance(Type type)
public static object CreateInstance(Type type, InitContext context)
public static object CreateInstanceEx(Type type)
public static object CreateInstanceEx(Type type, InitContext context)

public static T CreateInstance<T>()
public static T CreateInstance<T>(InitContext context)
public static T CreateInstanceEx<T>()
public static T CreateInstanceEx<T>(InitContext context)

Данные функции создают экземпляры объектов заданного типа (параметры type и <T>). В случае, если требуемый тип является абстрактным, эмитится сборка с реализацией данного типа и возвращается ссылка на объект сэмиченного типа (разумеется, только при первом вызове, потом используется уже сгенерированная сборка), иначе просто создается экземпляр объекта и возвращается на него ссылка.
Параметр InitContext context – задает контекст инициализации. Если задан этот параметр, и класс реализует конструктор, принимающий единственный параметр типа InitContext, то для создания объекта будет использован этот конструктор. InitContext, через свойство Parameters, позволяет передать в конструктор дополнительные данные.
Подсемейство CreateInstanceEx – отличается тем, что если с данным типом ассоциирована фабрика объектов, то для создания экземпляра будет использована данная фабрика, иначе вызовется соответствующий CreateInstance ( пример использования фабрики объектов).
Внутри BLT для создания объектов пользуется услугами TypeAccessor.

Атрибуты ¶

  • ParameterAttribute – позволяет задать значение по умолчанию для свойства.
  • NoInstanceAttribute – указывает что для данного свойства не следует создавать экзкмпляр объекта. По умолчанию создается объект (должен реализовывать конструктор по умолчанию, т.е. конструктор без параметров).
  • LazyInstanceAttribute – экземпляр объекта будет создан при первом обращении к свойству.
public abstract class TestObject
{
    public abstract int IntValue1 { get; set; }
    [Parameter(50)]
    public abstract int IntValue2 { get; set; }

    public abstract object NotNullValue { get; set; }
    [NoInstance]
    public abstract object NullValue { get; set; }

    [LazyInstance]
    public abstract object LazyValue { get; set; }
}

[Test]
public abstract Test()
{
    TestObject o = TypeAccessor.CreateInstance<TestObject>();

    Assert.IsNotNull(o);

    Assert,AreEqual(0,  o.IntValue1);
    Assert.AreEqual(50, o.IntValue2);

    Assert.IsNotNull(o.NotNullValue);
    Assert.IsNull(o.NullValue);

    Assert.IsNotNull(o.LazyValue);
}

EditableObjects ¶

В жизни так случается, что пользователи программ изменяют разного рода данные, и иногда сохраняют эти изменения. Чаще они пугаются того, что только что наменяли, и быстренько хотят вернуть все как было. Такое необычное поведение пользователей заставляет разработчика корпеть над тем, что бы только что внесенные изменения можно было легко отменить. Пространство имен BLToolkit.EditableObjects содержит набор классов, которые помогут вам в сделать свои объекты редактируемыми – т.е. наделят их возможностью откатывать состояние объекта до эталонного. Для этого служат следующие классы:

  • EditabeObject, EditableObject<T> - для объектов бизнес-логики.
  • EditableListArray, EditableList<T> - для списков.

Вам достанутся все прелести следующих интерфейсов (для объектов):

Namespace | Interface
System.ComponentModel | IEditableObject
System.ComponentModel | INotifyPropertyChanged
System.ComponentModel | ICustomTypeDescriptor
System | ICloneable
BLToolkit.ComponentModel | INotifyObjectEdit
BLToolkit.EditableObjects | IEditable
BLToolkit.Validation | IValidatable
BLToolkit.Mapping | ISupportMapping

И следующих для списков:

Namespace | Interface
System.ComponentModel | ITypedList
System.ComponentModel | IBindingList
System.ComponentModel | IBindingListView
System.ComponentModel | ICancelAddNew
BLToolkit.ComponentModel | ISortable
BLToolkit.EditableObjects | IEditable

И достанется вам все это почти бесплатно – BLToolkit за вас сгенерирует весь необходимый код. Вам останется только объявить абстрактный класс и унаследовать его от EditableObject, либо в случае списка просто унаследоваться от EditableArrayList или просто использовать его экземпляр.

public abstract class TestObject : EditableObject<TestObject>
{
    public abstract string FirstName { get; set; }
    public abstract string LastName  { get; set; }
}

[Test]
public void Test()
{
    // Создание экземпляра редактируемого объекта.
    //
    TestObject obj = TestObject.CreateInstance();

    // Установка значений.
    //
    obj.FirstName = "Tester";
    obj.LastName  = "Testerson";

    // Свойство IsDirty говорит нам о том, что состояние объекта было изменено
    Assert.IsTrue(obj.IsDirty);

    // Вызов AcceptChanges фикирует измененное состояние
    obj.AcceptChanges();

    // Теперь объект стал «чистым».
    // Т.е. его состояние было зафиксировано и с тех пор он не менялся.
    Assert.AreEqual("Tester", obj.FirstName);
    Assert.IsFalse(obj.IsDirty);

    // Поаробуем изменить значение поля
    obj.FirstName = "Developer";

    // Объект – «грязный».
    Assert.IsTrue(obj.IsDirty);

    // Вызов RejectChanges приводит к откату внесенных изменений.
    obj.RejectChanges();

    // Объект снова находится в том состоянии в котором был зафиксирован.
    Assert.AreEqual("Tester", obj.FirstName);
    Assert.IsFalse(obj.IsDirty);
}

Ключевыми моментами в данном примере являются следующие:

  • TestObject – абстрактный и унаследован от EditableObject.
  • Свойства TestObject.FirstName и TestObject.LastName – абстрактные.

Важно отметить, что при выборке объектов унаследованных от EditableObject из базы, они получаются «чистыми». Если быть точным, то по окончанию мапинга на EditableObject для объекта вызывается ISupportMapping.EndMapping(…) который в свою очередь вызывает AcceptChanges(). Так же на время маппинга для EditableObject «отключается» нотификация об изменении свойств, а по окончанию вызывается событие OnPropertyChanged с пустым именем свойства.

В рукаве ничего нет и шляпа пустая ¶

Как известно, чудес на свете не бывает, и раз объект обладает неким поведением, то кто-то это поведение написал. Данный случай не является исключением – код, необходимый для поддержки возможности запомнить чистое состояние, определить, что объект был изменен и откатить изменения сгенерировал BLToolkit. Как и в случае с абстрактными дата аксессорами, BLToolkit знает, что для наследников EditableObject код генерировать необходимо по определенным правилам. Из всех этих правил, самым важным и полезным к знанию и пониманию является то, как, собственно поддерживается механизм «редактируемости». Дело в том, что значение каждого абстрактного поля «упаковывается» в определенную структуру\класс, которые собственно и содержат оригинальное и измененное значение этого поля. Скалярные поля упаковываются в наследников generic структуры EditableValue<T>, полный код можно посмотреть в исходниках, здесь я приведу только его часть:

public struct EditableValue<T>: IEditable, IMemberwiseEditable, PrintDebugState
{
    private T _original;
    private T _current;

    public EditableValue(T value)
    {
        _original = value;
        _current  = value;
    }

    [GetValue, SetValue]
    public T Value
    {
        get { return _current;  }
        set { _current = value; }
    }

    public bool IsDirty
    {
        get
        {
            object o = _original;
            object c = _current;

            return o == null? c != null: o.Equals(c) == false;
        }
    }
}

Как видим, структура реализует интерфейсы IEditable и IMemberwiseEditable. Т.е. в итоге, наш EditableObject будет состоять из набора маленьких редактируемых структур и объектов, которые то и хранят оригинальное и измененное состояние поля (свойство IsDitry я привел просто для примера). Отдельно следует отметить свойство Value и то, что оно помечено атрибутами GetValue и SetValue. Эти атрибуты говорят о том, что реализация геттера и сеттера свойств в генерируемых объектах должна быть делегирована этому методу\свойству класса-обертки. Таким образом, описанный выше класс TestObject в сгенерированном виде будет выглядеть примерно следующим образом (приведена только часть сгенерированного кода):

public sealed class TestObject
{
    private EditableValue<string> _firstName;
    private EditableValue<string> _lastName;

    void IEditable.AcceptChanges()
    {
        this._firstName.AcceptChanges();
        this._lastName.AcceptChanges();
    }

    public bool IsDirty
    {
        get
        {
            bool isDirty = false;
            isDirty = this._firstName.IsDirty;
            if (!isDirty)
            {
                isDirty = this._lastName.IsDirty;
            }
            return isDirty;
        }
    }

    public string FirstName
    {
        get { return _firstName.Value;  }
        set { _firstName.Value = value; }
    }
}

Как я уже писал, скалярные поля упаковываются в наследников EditableValue<T>, а вот наследники EditableObject будут упакованы в EditableObjectHolder (подробнее об этом нюансе я напишу ниже, тут есть некоторые подводные камни). Вообще для управления того, куда и что упаковывать служат два атрибута:

  • InstanceType – данный атрибут применяется на свойство класса.
  • GlobalInstanceType – данный атрибут применяется на класс, и задает соответствие типа свойства к типу объекта обертки.

Выглядит это примерно так:

// укажем что все свойства типа int нербходимо обернуть в EditableValue<int>
[GlobalInstanceType(typeof(int), typeof(EditableValue<int>))]
public abstract class MyEditableObject : EditableObject
{
    // будет упакован в EditableValue<int>
    public abstract int IntValue1 { get; set; }

    [InstanceType(typeof(MyIntWrapper))] // будет упакован в MyIntWrapper
    public abstract int IntValue2 { get; set; }
}

Если посмотреть, как объявлен EditableObject в библиотеке, то мы увидим, что на классе висит несколько десятков атрибутов GlobalInstanceType, указывающих обертки для скалярных и ряда других типов.

EditableObjectHolder ¶

Как я уже говорил выше, значения полей с типами наследниками от EditableObject по умолчанию упаковываются в EditableObjectHolder, в связи с чем вам следует знать ряд нюансов:

  • Если вложенный объект грязный (допустим, изменено одно из его полей), то объект его содержащий так же считается грязным.
  • Вызов Accept\RegectChanges приводит к соответствующим вызовам во вложенном объекте.
  • EditableObjectHolder подписывается на события OnPropertyChanged объекта, который в него завернут. При изменении свойств вложенного объекта райзится событие для объекта в который он вложен.

В связи с чем могут возникать некоторые сложности:

public abstract class TestObject : EditableObject<TestObject>
{
    public abstract TestCategory Category { get; set; }
}

public abstract class TestCategory : EditableObject<TestCategory>
{
    public abstract TestObject InnerObject { get; set; }
}

public abstract class Parent : EditableObject<Parent>
{
    public abstract Child Child { get; set; }
}

public abstract class Child : EditableObject<Child>
{
    public abstract Parent Parent { get; set; }
}

public void InfiniteRecursion()
{
    Parent p = Parent.CreateInstance();
    Child  c = Child .CreateInstance();

    p.AcceptChanges(); // StakOverfolwException
}

static TestCategory _category = TestCategory.CreateInstance();
public void MemoryLeak()
{
    TestObject o = TestObject.CreateInstance();

    o.Category = _category; // утечка памяти
}
  • Функция InfiniteRecursion – приводит к бесконечной рекурсии и как следствие переполнению стека. Схема тут очень простая: p.AcceptChanges() -> c.AcceptChanges() -> p.AcceptChanges() -> … -> StakOverfolwException.
  • А вот функция MemoryLeak приведет к утечке памяти. Дело в том, что объект _category получит неявную ссылку на созданный в методе экземпляр TestObject через обработчик события OnPropertyChanged. Таким образом, созданный экземпляр TestObject попадет в Gen 2 и будет собран GC не ранее, чем будет уничтожен объект _category. Этот момент особенно неприятен тем, что различного рода небольшие справочники, которые редко меняются и на которые ссылаются другие объекты, очень хочется закешировать, что бы не дергать лишний раз базу. И вот тут мы и можем натолкнуться на утечку памяти в промышленных масштабах.

Разумеется, такое поведение не всегда приемлемо и удобно. В таком случае, вы можете сделать свой класс обертку который будет работать так как вам необходимо. И указать что необходимо использовать его атрибутами GlobalInstaceType или InstanceType.

EditableArrayList, EditableList<T> ¶

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

  • EditableArrayList предоставляет еще два дополнительных списка – DelItems и NewItems – для удаленных и новых элементов, соответственно. Данные списки чистятся по Accept/RejectChanges.
  • Если внутри списка лежат наследники IEditable, то их свойство IsDirty будет учтено при получении IsDirty списка. Т.е. даже если в список ничего не добавляли и не удаляли, но внутренние элементы менялись то IsDirty == true.
  • Accept/RejectChanges будут так же вызваны для всех наследников IEditable, находящихся в списке.

Полезные ссылки ¶

  • EditableObjects - документация на англ.
  • Как использовать фабрику объектов для чтения объектов разных типов из базы данных
  • Использование internal типов
  • XML сериализация абстрактных типов

Download in other formats:

  • Plain Text


Aspects – BLToolkit

  • Index
  • History
  • Last Change

  1. Аспекты
    1. Async
    2. Cache, ClearCache и InstanceCache
    3. Counter
    4. Log
    5. Mixin
    6. NotNull
    7. Overload

Аспекты ¶

Аспекты в BLToolkit реализуют парадигму Аспектно-ориентированного Программирования.

Async ¶

Позволяет асинхронно, т.е. не дожидаясь завершения, вызвать нужный метод. Пример:

public abstract SomeAccessor : DataAccessor
{
    public abstract int GetMatchedRecordsCount(string criteria);

    [Async] public abstract IAsyncResult BeginGetMatchedRecordsCount(string criteria);
    [Async] public abstract int EndGetMatchedRecordsCount(IAsyncResult ar);
}

Здесь GetMatchedRecordsCount это обычный, синхронный метод, а BeginGetMatchedRecordsCount и EndGetMatchedRecordsCount это два вспомогательных метода, позволяющих вызывать GetMatchedRecordsCount не дожидаясь его завершения. Пример использования:

IAsyncResult ar = ta.BeginGetMatchedRecordsCount("Diagnosis='Hallucination with Paranoid Bugs'");

// ...

if (ar.IsCompleted)
  totalRecords = ta.EndGetMatchedRecordsCount(ar);

Примечание: "Досрочный" вызов ta.EndGetMatchedRecordsCount?(ar) должен ждать завершения, если асинхронный вызов ещё не прекратился и немедленно возвращать результат, если вызов завершился.

Вариант с обратным вызовом:

public abstract SomeAccessor : DataAccessor
{
    public abstract int GetMatchedRecordsCount(string criteria);

    [Async] public abstract IAsyncResult BeginGetMatchedRecordsCount(string criteria, AsyncCallback callback, object state);
    [Async] public abstract int EndGetMatchedRecordsCount(IAsyncResult ar);
}

private void MyCallBack(IAsyncResult ar)
{
    SomeAccessor ta = (SomeAccessor) ar.AsyncState;
    Console.WriteLine("Callback");

    // End метод обязательно нужно вызывать, иначе никак не получить результат.
    // А если результат имеет тип void, то всё равно нужно вызывать EndMethod, иначе будет утечка ресурсов.
    //
    totalRecords = ta.EndGetMatchedRecordsCount(ar);
}

ta.BeginGetMatchedRecordsCount("FirstName='John'", MyCallBack, ta);

Cache, ClearCache и InstanceCache ¶

Позволяет кешировать результаты вызова метода. Например, метод:

[Cache]
public virtual int CachedMethod(int p1, int p2)
{
  return p1 + p2;
}

Будучи дважды вызван с параметрами p1=1 и p2=2 дважды вернёт число 3, но первый раз результат будет честно вычислен, а во второй раз будет возвращён результат предыдущего вызова. Параметры атрибута CacheAttibute:

  • MaxCacheTime, MaxSeconds и MaxMinutes задают время жизни сохранённых значений в миллисекундах, секундах и минутах соответственно.
  • IsWeak позволяет использовать слабую ссылку для сохранения результатов.

Так же можно заменить стандартный механизм кеширования на свой собственный:

public abstract class CustomTestClass
{
  [Cache(typeof(CustomCacheAspect), MaxCacheTime = 500)]
  public abstract int Test(int i1, int i2);
}

public class CustomCacheAspect : CacheAspect
{
  private static IDictionary _methodcache = new Hashtable();
  public  static IDictionary  MethodCache
  {
    get { return _methodcache; }
  }

  protected override CacheAspectItem CreateCacheItem(InterceptCallInfo info)
  {
    return new MyCacheAspectItem();
  }

  protected override IDictionary CreateCache()
  {
    return MethodCache;
  }
}

Cache задаёт глобальный кеш для всех экземпляров некоторого типа, InstanceCache соответственно создаёт отдельный кеш для каждого экземпляра. Принудительно сбросить кеш можно при помощи аспекта ClearCache:

public abstract class TestClass
{
  [Cache]
  public abstarct int CachedMethod();

  [Cache]
  public abstarct int CachedMethod(int p1, int p2);

  // Очищает кеш метода CachedMethod с двумя параметрами типа int.
  //
  [ClearCache("CachedMethod", typeof(int), typeof(int))]
  public abstract void ClearCache2();

  // Очищает кеш метода CachedMethod без параметров.
  //
  [ClearCache("CachedMethod", new Type[0])]
  public abstract void ClearCache0();

  // Очищает кеш обоих методов CachedMethod
  //
  [ClearCache("CachedMethod")]
  public abstract void ClearCache0();

Так же можно явно вызвать один из методов CacheAspect.ClearCache.

Counter ¶

Позволяет считать количество вызовов метода.

Log ¶

Позволяет вести журнал вызовов метода.

Mixin ¶

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

public interface ITestInterface
{
  int TestMethod(int value);
}

public class TestInterfaceImpl : ITestInterface
{
  public int TestMethod(int value) { return value; }
}

[Mixin(typeof(ITestInterface), "_testInterface")]
public abstract class TestClass
{
  public TestClass()
  {
    _testInterface = new TestInterfaceImpl();
  }

  protected object _testInterface;
}

После создания объекта типа TestClass при помощи TypeAccessor.CreateInstance у экземпляров типа TestClass невесть откуда появляется реализация интерфейса ITestInterface. На самом деле объекты типа TestClass просто делегируют вызовы в объект типа TestInterfaceImpl.

Иногда возникает необходимость вмешаться в процесс делегирования. Для этой цели служит атрибут MixinOverrideAttribute:

public interface ITestInterface
{
  void Method1();
  void Method2();
  void Method3();
}

public class TestInterfaceImpl : ITestInterface
{
  public void Method1() { }
  public void Method2() { }
  public void Method3() { }
}

[Mixin(typeof(ITestInterface), "_testInterface")]
public abstract class TestClass
{
  protected object _testInterface = new TestInterfaceImpl();

  [MixinOverride]
  protected void Method2()
  {
    // Какая-то предобработка.
    //
    int a = 2 + 2;

    // Опционально возможен вызов "базовой" реализации.
    //
    _testInterface.Method2();

    // Какая-то постобработка.
    //
    a /= 2;
  }
}

NotNull ¶

Применяется для параметров и добавляет проверку на null:

public abstract class TestObject
{
  public virtual void Foo1(string str1, [NotNull] string str2, string str3) {}
}

Объекты типа TestObject, созданные при помощи TypeAccessor.CreateInstance будут кидать исключение ArgumentNullException при попытке передать пустой указатель в качестве параметра str2.

Overload ¶

Упрощает перегрузку методов

public abstract class TestObject
{
  public void Foo(string str1, string str2, string str3) {}

  [Overload]
  public absract void Foo(string str1, string str3, string str4, string str5) { /* Вызовет Foo(str1, null, str3)*/}
}

Все параметры, у которых совпадают имена и типы (но не порядковый номер), передаются перегружаемому методу. "Лишние" параметры игнорируются, недостающие заменяются пустышками.

Download in other formats:

  • Plain Text


DuckTyping – BLToolkit

  • Index
  • History
  • Last Change

  1. Утиная типизация
    1. Токая настройка

Утиная типизация ¶

В c# 4.0 можно писать вот такой код:

public void DoFoo(object o)
{
  dynamic
  {
    o.Foo();
  }
}

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

public void DoFoo(object o)
{
  o.GetType().InvokeMember("Foo", BindingFlags.Public | BindingFlags.Instance | BindingFlags.InvokeMethod, null, o, null);
}

Вызов будет осуществлён напрямую, без использования медленной рефлексии.

Всё почти то же самое можно получить в c# 2.0 и даже 1.0, если воспользоваться Утиной типизацией. В случае использования BLTookit код получается такой:

public interface ICanFoo
{
  public void Foo();
}

// C# 2.0
//
public void DoFoo(object o)
{
  DuckTyping.Implement<ICanFoo>(o).Foo();
}

// C# 1.0
//
public void DoFooNoGenerics(object o)
{
  ICanFoo fooDuck = (ICanFoo)DuckTyping.Implement(typeof(ICanFoo), o);
  fooDuck.Foo();
}

Кода немного больше, но он и выразительней. Сначала явно и однозначно описывается интерфейс, который хотелось бы использовать:

public interface ICanFoo
{
  public void Foo();
}

Затем объект неизвестного типа приводится к этому интерфейсу и вызывается уже метод этого интерфейса.

  DuckTyping.Implement<ICanFoo>(o).Foo();

Каким образом какой-то объект, вдруг начинает реализовывать наш интерфейс? А вот каким. Если этот объект явно или неявно реализует нужный интерфейс, то DuckTyping.Implement просто возвращает ссылку на тот же самый объект. Это будет равнозначно простому приведению типа. Обычно ситуация обратная. Объекты из сборок третей и четвёртой фирмы ничего не знают о нашем интерфейсе. Но, тем не менее, у этих объектов имеется публичный метод Foo без параметров. В этом случае можно по-быстрому создать объект переходник и вернуть его в качестве полноценной реализации интерфейса ICanFoo:

public sealed class DuckThatCanFooForSomeType : ICanFoo
{
  SomeType _o;

  public DuckThatCanFooForSomeType(object o)
  {
    _o = (SomeType)o;
  }

  public void Foo()
  {
    _o.Foo();
  }
}

// ...
if (o is ICanFoo)
  return (CanFoo)o;

return new DuckThatCanFooForSomeType(o);

Тип объекта o всегда можно у него спросить при помощи метода Object.GetType() и специально для этого типа создать объект, реализующий нужный нам интерфейс и делегирующий его вызовы объекту этого конкретного типа.

Токая настройка ¶

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

Для более тонкой настройки принудительно реализуемого объектами интерфейса предназначен атрибут MustImplementAttribute:

public class MustImplementAttribute : Attribute
{
  public MustImplementAttribute(bool implement, bool throwException, string exceptionMessage);
}

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

Комбинация параметров implement и throwException даёт четыре сценария работы отсутствующего метода:

  • implement = false и throwException = false Отсутствующий метод попросту не вызывается. Возвращаемое значение и выходные параметры инициализируются значениями по умолчанию.
  • implement = false и throwException = true Отсутствующий метод кидает исключение NotImplementedException при вызове (поведение по-умолчанию).
  • implement = true и throwException = false DuckTyping.Implement возвращает пустой указатель; это аналог оператора as.
  • implement = true и throwException = true DuckTyping.Implement кидает исключение TypeBuilderException; это аналог оператора приведения типа.

Параметр exceptionMessage позволяет явно задать текст исключения NotImplementedException.

Поле DuckTyping.AllowStaticMembers задаёт политику использования статических методов для утиной типизации. По-умолчанию статические методы не используются.

Download in other formats:

  • Plain Text


DataProvider – BLToolkit

  • Index
  • History
  • Last Change

  1. Использование разных БД
    1. AccessDataProvider
    2. DB2DataProvider
    3. FdpDataProvider
    4. InformixDataProvider
    5. MonoSqliteDataProvider
    6. MySqlDataProvider
    7. OleDbDataProvider
    8. OdbcDataProvider
    9. OdpDataProvider
    10. OleDbDataProvider
    11. OracleDataProvider
    12. PostgreSQLDataProvider
    13. SqlCeDataProvider
    14. SqlDataProvider
    15. SQLiteDataProvider
    16. SybaseAdoDataProvider
    17. SybaseDataProvider

Использование разных БД ¶

см. Введение в BLToolkit

Часть провайдеров входит в стандартную сборку. Это провайдеры, которые не требуют специальных библиотек. Все провайдеры можно найти в папке Source\Data\DataProvider.
Некоторые провайдеры имеют конфигурационные параметры. Обычно провайдер может быть настроен через app.config. Пример:

<configuration>
    <configSections>
        <section name="bltoolkit" type="BLToolkit.Configuration.BLToolkitSection, BLToolkit.3"/>
    </configSections>

    <bltoolkit defaultConfiguration="MySql">
        <dataProviders>
            <add type="BLToolkit.Data.DataProvider.MySqlDataProvider, BLToolkit.3" default="true" />
            <add name="mySql" type="BLToolkit.Data.DataProvider.MySqlDataProvider, BLToolkit.3"

                    parameterPrefix           = "p_"
                    parameterSymbolConfig     = "NewStyle"
                    parameterSymbol           = "?"
                    tryConvertParameterSymbol = "true"
                    convertParameterSymbols   = "@"

                   />
        </dataProviders>
    </bltoolkit>
<!--
....
--!>
</configuration>

AccessDataProvider ¶

Включен в стандартную сборку. Унаследован от OleDbDataProvider.

DB2DataProvider ¶

Не включен в стандартную сборку. Использует IBM DB2.NET Data Provider. Redist\Ibm\IBM.Data.DB2.dll

FdpDataProvider ¶

Не включен в стандартную сборку. Использует FirebirdClient http://sourceforge.net/project/showfiles.php?group_id=9028&package_id=62107
Особенности провайдера (все настройки могут быть заданы через app.config):

  • Реализован в соответствии со схемой совместимости типов: http://www.firebirdsql.org/manual/migration-mssql-data-types.html Однако, для типа Binary используется BLOB, а не CHAR.
  • Позволяет эмулировать InOut параметры. Т.к. Firebird не поддерживает InOut параметры, для их эмуляции используются специальные префиксы InOut параметр должен быть определен в секции RETURNS(), так же должен быть определен его двойник в секции входных параметров. Имя двойника должно состоять из префикса и имени параметра. Префикс задается настройкой InOutInputParameterPrefix (по умолчанию "_in"). Использование эмуляции определяется настройкой IsInOutParameterEmulation (true by default).
  • Позволяет эмулировать.ReturnValue параметр. Параметр считается ReturnValue если его имя совпадает с заданным настройкой ReturnParameterName (RETURN_VALUE – по умолчанию). Использование определяется настройкой IsReturnValueEmulation.

InformixDataProvider ¶

Не включен в стандартную сборку. Использует IBM Informix(c) .NET Provider. Redist\Ibm\IBM.Data.Informix.dll

MonoSqliteDataProvider ¶

Не входит в стандартную сборку. Использует System.Data.SQLite http://sqlite.phxsoftware.com/

MySqlDataProvider ¶

Не входит в стандартную сборку. Использует MySql .Net Connector. http://dev.mysql.com/downloads/connector/net/
Провайдер имет следующие настройки (могут быть настроены через app.config):

  • ParameterSymbol - MySql .Net Connector может использовать '?' и '@' - в качестве символов, определяющих параметр запроса. Данный параметр позволяет указать какой именно использовать.
  • TryConvertParameterSymbol - определяет приобразовывать ли символы параметров к заданому ParameterSymbol. Символы которые преобразуются задаются настройкой ConvertParameterSymbols. По умолчанию true.
  • ConvertParameterSymbols - символы для преобразования в именах параметрах. См. http://rsdn.ru/forum/message/3390747.1.aspx По умолчанию '@'.
  • ParameterPrefix - префикс для имен параметров хранимых процедур. По усолчанию - пустой.
  • ParameterSymbolConfig - доступен –только через app.config. Задает "стиль" использования символов параметров. Может принимать значения "NewStyle" и "OldStyle"

Имеет 2 настроечных метода:

public static void ConfigureOldStyle()
{
    ParameterSymbol           = '?';
    ConvertParameterSymbols   = new List<char>(new char[] { '@' };
    TryConvertParameterSymbol = true;
}

public static void ConfigureNewStyle()
{
    ParameterSymbol           = '@';
    ConvertParameterSymbols   = null;
    TryConvertParameterSymbol = false;
}

OleDbDataProvider ¶

Включен в стандартную сборку.

OdbcDataProvider ¶

Включен в стандартную сборку.

OdpDataProvider ¶

Не включен в стандартную сборку. Использует Odp.Net Data Provider. http://www.oracle.com/technology/tech/windows/odpnet/index.html Особенности провайдера:

  • Для имен параметров хранимых процедур используется префикс, заданный настройкой ParameterPrefix. По умолчанию P. Может быть задан через app.config.

OleDbDataProvider ¶

Входит в стандартную сборку.

OracleDataProvider ¶

Входит в стандартную сборку.

PostgreSQLDataProvider ¶

Не входит в стандартную сборку. Использует Npgsql. http://pgfoundry.org/projects/npgsql

SqlCeDataProvider ¶

Не входит в стандартную сборку. Использует System.Data.SqlServerCe?.dll http://www.microsoft.com/sql/editions/compact/default.mspx

SqlDataProvider ¶

Входит в стандартную сборку.

SQLiteDataProvider ¶

Не входит в стандартную сборку. Использует System.Data.SQLite.dll http://sqlite.phxsoftware.com/

SybaseAdoDataProvider ¶

Не входит в стандартную поставку. Унаследован от OleDbDataProvider.

SybaseDataProvider ¶

Не входит в стандартную сборку. Использует Sybase.Data.AseClient

Download in other formats:

  • Plain Text


Linq with BLToolkit

Автор: Игорь Ткачёв
Опубликовано: 15.02.2010
Исправлено: 16.02.2010
Версия текста: 1.0

Введение
Подготовка к работе
Подключение провайдера базы данных
Создание модели данных
Идеология и модель программирования
Стандартные Linq запросы
Первое знакомство
Запрос к двум таблицам
Реализация стандартных возможностей
Ассоциации
Наследование
Стандартные функции .NET Framework
Дополнительные возможности BLToolkit
Select
DML операции
SQL функции
Расширение BLToolkit
SQL функции
Подстановка методов
Повторное использование выражений
Производительность
Проблемы производительности в Linq
CompiledQuery
Заключение

Демонстрационный проект

Введение

Сегодня Linq уже не нуждается в особом представлении и рекомендациях. Эта технология с каждым днём получает всё больше распространение и всё сильнее завоёвывает признание разработчиков. BLToolkit так же не остался в стороне и предлагает свой Linq-provider для доступа к базам данным. Целью данного документа как раз является предоставление информации о поддержки Linq в BLToolkit.

Подготовка к работе

Подключение провайдера базы данных

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

  • DB2
  • Informix
  • Firebird
  • Oracle
  • Microsoft Access
  • MS SQL 2005
  • MS SQL 2008
  • MySql
  • PostgreSQL
  • SqlCe
  • SQLite
  • Sybase ASE

По умолчанию библиотека поддерживает только три провайдера: MS SQL 2008, MS SQL 2005 и Microsoft Access. Связано это с тем, что эти провайдеры входят в стандартную поставку .NET, а для работоспособности остальных необходимо устанавливать дополнительные компоненты от производителей баз данных.

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

  1. Добавьте ссылки на соответствующие библиотеки от производителей баз данных в ваш проект.
  2. Добавьте соответствующий файл с реализацией провайдера в проект. Исходный код всех провайдеров находится в каталоге Data\DataProvider проекта BLToolkit или в том же каталоге архива http://bltoolkit.net/download/bltoolkit_bin.zip, если вы используете скомпилированную версию BLToolkit.
  3. Зарегистрируйте BLToolkit провайдер в своём проекте одним из следующих способов:
using System;

using NUnit.Framework;

using BLToolkit.Data;
using BLToolkit.Data.DataProvider;

namespace HowTo.Data.DataProvider
{
    [TestFixture]
    public class AddDataProvider
    {
        const string connectionString =
            "Data Source=DBHost;Port=5000;Database=BLToolkitData;Uid=sa";

        [Test]
        public void Test()
        {
            // 3rd party data provider registration.
            //
            DbManager.AddDataProvider(new SybaseDataProvider());

            // It can be configured by App.config.
            // We use this way for the demo purpose only.
            //
            DbManager.AddConnectionString(
                "Sybase",          // Provider name
                "Sybase",          // Configuration
                connectionString); // Connection string

            using (var db = new DbManager("Sybase"))
            {
            }
        }
    }
}

Либо с помощью файла конфигурации:

<?xml version="1.0"?>
<configuration>
    <configSections>
        <section name="bltoolkit" type="BLToolkit.Configuration.BLToolkitSection, BLToolkit.3"/>
    </configSections>
    <bltoolkit>
        <dataProviders>
            <add type="BLToolkit.Data.DataProvider.SybaseDataProvider, MyProjectName" />
        </dataProviders>
    </bltoolkit>
    <connectionStrings>
        <add name="Sybase" connectionString="Data Source=DBHost;Port=5000;Database=BLToolkitData;Uid=sa" providerName="Sybase.Data.AseClient"/>
    </connectionStrings>
</configuration>

Подробнее о способах конфигурации BLToolkit см. http://bltoolkit.net/doc/Data/index.htm.

Создание модели данных

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

  • С помощью специальной утилиты генерации классов для BLToolkit от Андрея Смирнова.
  • С помощью шаблона MyGeneration от Andy77.
  • С помощью дизайнера модели данных Linq to SQL.
  • Написанием классов модели данных вручную.

Мы для примеров будем использовать стандартного подопытного кролика от Microsoft - базу данных Northwind. Скрипт создания этой базы данных и модель данных для неё вы сможете найти в демонстрационном проекте этой статьи. Ниже для затравки приведён один из классов этой модели:

[TableName("Categories")]
public class Category
{
    [PrimaryKey, Identity] public int    CategoryID;
    [NotNull]              public string CategoryName;
                           public string Description;
                           public Binary Picture;

    [Association(ThisKey="CategoryID", OtherKey="CategoryID")]
    public List<Product> Products;
}

Кроме классов модели данных нам понадобится класс, объединяющий в себе все таблицы модели.

class NorthwindDB : DbManager
{
    public NorthwindDB() : base("Northwind")
    {
    }

    public Table<Northwind.Category> Category { get { return GetTable<Northwind.Category>(); } }
    // ...
}

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

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

Идеология и модель программирования

BLToolkit не является ORM в классическом понимании этого определения. Классические ORM решают по сути две задачи, предоставляемые в одном флаконе: Data Mapping и Entity Services. Первая задача - это как раз то, чем изначально должны заниматься ORM системы - перекладыванием данных из БД в объекты и обратно. Вторая, Entity Services - набор сервисов управления объектной моделью как то: обеспечение ссылочной целостности, кэширование объектов, отслеживание изменений, валидация объектов и т.п. В последнее время такие ORM стало принято называть Heavy ORM (тяжёлые ORM), а инструменты предоставляемые только лишь Data Mapping - Lightweight ORM (лёгкие ORM). BLToolkit относится ко второй категории. Поэтому вы не увидите в BLToolkit:

  • операций по автоматическому извлечению вложенных списков объектов и ленивой загрузке;
  • автоматического кэширования объектов;
  • контекста данных (наподобие Linq to SQL DataContext), который отслеживает изменения и занимается ссылочной целостностью, прочитанных из базы данных объектов;
  • операции SubmitChanges для контекста данных.

Вместо этого вам будут доступны:

  • операции DML (Insert/Update/Delete) в таком виде, в котором они присутствуют в SQL;
  • максимально приближённая к SQL модель программирования, включающая в том числе SELECT без источника данных (например, для генерации вызова SELECT CURRENT_TIMESTAMP);
  • возможность описания модели данных с помощью исключительно интерфейсов;
  • механизмы повторного использования выражений и подзапросов.

Другими словами, BLToolkit не будет от вас пытаться скрыть работу с базой данных за объектной моделью приложения, а как раз наоборот, предоставит максимально полные и естественные средства для работы с вашей БД.

Стандартные Linq запросы

Первое знакомство

  • Наш первый тест будет ничем не примечателен. Здесь мы всего лишь берём таблицу Employees и выполняем к ней запрос.
static void FirstTest()
{
    using (var db = new NorthwindDB())
    {
        var query = db.Employee;

        foreach (var employee in query)
        {
            Console.WriteLine("{0} {1}", employee.EmployeeID, employee.FirstName);
        }
    }
}

Что может быть проще? Тем не менее, давайте посмотрим на то, что у нас получилось под микроскопом. Поставим точку останова внутри цикла и взглянем на содержимое переменных query и db в отладчике.

Прежде всего нас будет интересовать свойство SqlText переменной query. Откроем Text visualizer и посмотрим содержимое SqlText.

Это текст запроса сгенерированный BLToolkit. В первой строчке закоментированы текущая строка конфигурации, имя провайдера данных и имя SQL провайдера.

Но, к сожалению, свойство SqlText доступно только, если мы работаем с переменной типа IQueriable<T> как в предыдущем случае. Это не всегда возможно, запрос в следующем примере возвращает нам значение типа int.

static void CountTest()
{
    using (var db = new NorthwindDB())
    {
        int count = db.Employee.Count();

        Console.WriteLine(count);
    }
}

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

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

static void Main()
{
    DbManager.TraceSwitch = new TraceSwitch("DbManager", "DbManager trace switch", "Info");

    FirstTest();
    CountTest();
}

Естественно, этот режим доступен только при использовании отладочной версии BLToolkit.

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

static void SingleTableTest()
{
    using (var db = new NorthwindDB())
    {
        var query =
            from e in db.Employee
            where e.EmployeeID > 5
            orderby e.LastName, e.FirstName
            select e;

        foreach (var employee in query)
        {
            Console.WriteLine("{0} {1}, {2}", employee.EmployeeID, employee.LastName, employee.FirstName);
        }
    }
}
ПРИМЕЧАНИЕ

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

Генерируемый SQL:

SELECT
    [e].[EmployeeID],
    [e].[LastName],
    [e].[FirstName],
    [e].[Title],
    [e].[TitleOfCourtesy],
    [e].[BirthDate],
    [e].[HireDate],
    [e].[Address],
    [e].[City],
    [e].[Region],
    [e].[PostalCode],
    [e].[Country],
    [e].[HomePhone],
    [e].[Extension],
    [e].[Notes],
    [e].[ReportsTo],
    [e].[PhotoPath]
FROM
    [Employees] [e]
WHERE
    [e].[EmployeeID] > 5
ORDER BY
    [e].[LastName],
    [e].[FirstName]

Запрос к двум таблицам

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

Select Many

"Старый" стиль Join.

from c in db.Category
from p in db.Product
where p.CategoryID == c.CategoryID
select new
{
    c.CategoryName,
    p.ProductName
};

SQL:

SELECT
    [c].[CategoryName],
    [t1].[ProductName]
FROM
    [Categories] [c], [Products] [t1]
WHERE
    [t1].[CategoryID] = [c].[CategoryID]

Inner Join

"Новый" Join.

from p in db.Product
join c in db.Category on p.CategoryID equals c.CategoryID
select new
{
    c.CategoryName,
    p.ProductName
};

SQL:

SELECT
    [t1].[CategoryName],
    [p].[ProductName]
FROM
    [Products] [p]
        INNER JOIN [Categories] [t1] ON [p].[CategoryID] = [t1].[CategoryID]

Left Join

И напоследок вот такой некрасивый Left Join.

from p in db.Product
join c in db.Category on p.CategoryID equals c.CategoryID into g
from c in g.DefaultIfEmpty()
select new
{
    c.CategoryName,
    p.ProductName
};

SQL:

SELECT
    [t1].[CategoryName],
    [p].[ProductName]
FROM
    [Products] [p]
        LEFT JOIN [Categories] [t1] ON [p].[CategoryID] = [t1].[CategoryID]

Реализация стандартных возможностей

Ассоциации

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

Ниже приведена таблица Product и её ассоциации.

[TableName("Products")]
public abstract class Product
{
    [PrimaryKey, Identity] public int      ProductID;
    [NotNull]              public string   ProductName;
                           public int?     SupplierID;
                           public int?     CategoryID;
                           public string   QuantityPerUnit;
                           public decimal? UnitPrice;
                           public short?   UnitsInStock;
                           public short?   UnitsOnOrder;
                           public short?   ReorderLevel;
                           public bool     Discontinued;

    [Association(ThisKey="ProductID",  OtherKey="ProductID")]
    public List<OrderDetail> OrderDetails;

    [Association(ThisKey="CategoryID", OtherKey="CategoryID", CanBeNull=false)]
    public Category Category;

    [Association(ThisKey="SupplierID", OtherKey="SupplierID", CanBeNull=false)]
    public Supplier Supplier;
}

Что нужно знать об ассоциациях?

  • Член класса, к которому применён атрибут ассоциации может иметь либо списочный тип, либо являться ссылкой на объект. Списочный тип означает связь один ко многим. Т.е. в нашем случае один Product и много Order Details. Просто ссылка на объект означает связь один к одному. Либо много к одному, что для нас уже не важно, т.к. на генерацию SQL это никак не влияет.
  • Сама связь задаётся парой свойств атрибута ThisKey/OtherKey. Свойство ThisKey перечисляет через запятую поля самой сущности в которой объявлена ассоциация. Свойство OtherKey - соответствующие поля связанной сущности. Если имена полей сущности и исходной таблицы базы данных отличаются, то в этих свойствах указываются имена полей сущности.
  • Свойство CanBeNull указывает BLToolkit какую связь генерировать: Inner Join или Left Join. Будьте внимательны с этим свойством. Left Join не по делу может весьма существенно повысить время выполнения запроса. К тому же BLToolkit может производить дополнительные оптимизации для связей один к одному и убирать лишние связи из запроса, если это Inner Join. В общем случае правило использования этого атрибута такое: для связей один ко многим CanBeNull может быть равно false только в случае, если мастер запись не может появиться в базе банных без дочерних (например, Order -> OrderDetails); для связей один к одному чаще всего CanBeNull должно быть false, но это нужно указать явно.
  • Члены класса, к которым применена ассоциация не заполняются автоматически при чтении объекта из базы данных целиком.

Теперь посмотрим что у нас получилось:

from p in db.Product
select new
{
    p.Category.CategoryName,
    p.ProductName
};

Явное указание связи с таблицей Category исчезло, но генерируемый SQL остался прежним:

SELECT
    [t1].[CategoryName],
    [p].[ProductName]
FROM
    [Products] [p]
        INNER JOIN [Categories] [t1] ON [p].[CategoryID] = [t1].[CategoryID]

Давайте попробуем поменять у ассоциации Category значение свойства CanBeNull на true и посмотрим на результат:

SELECT
    [t1].[CategoryName],
    [p].[ProductName]
FROM
    [Products] [p]
        LEFT JOIN [Categories] [t1] ON [p].[CategoryID] = [t1].[CategoryID]

Как вы уже догадались, теперь у нас получился Left Join.

Использовать списочные ассоциации можно, например, следующим образом:

from p in db.Product
select new
{
    p.OrderDetails.Count,
    p.ProductName
};

SQL:

SELECT
    (
        SELECT
            Count(*)
        FROM
            [Order Details] [t1]
        WHERE
            [p].[ProductID] = [t1].[ProductID]
    ) as [c1],
    [p].[ProductName]
FROM
    [Products] [p]

Как было сказано выше связанные сущности не заполняются автоматически при создании объекта, но при желании это можно сделать вручную:

from o in db.Order
select new Northwind.Order
{
    OrderID  = o.OrderID,
    Customer = o.Customer
};

SQL:

SELECT
    [o].[OrderID],
    [t1].[CustomerID],
    [t1].[CompanyName],
    [t1].[ContactName],
    [t1].[ContactTitle],
    [t1].[Address],
    [t1].[City],
    [t1].[Region],
    [t1].[PostalCode],
    [t1].[Country],
    [t1].[Phone],
    [t1].[Fax]
FROM
    [Orders] [o]
        INNER JOIN [Customers] [t1] ON [o].[CustomerID] = [t1].[CustomerID]

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

from o in db.OrderDetail
select new
{
    o.Product.ProductName,
    o.Order.OrderID,
    o.Order.Employee.ReportsToEmployee.Region
};

SQL:

SELECT
    [t1].[ProductName],
    [o].[OrderID],
    [t2].[Region]
FROM
    [Order Details] [o]
        INNER JOIN [Products] [t1] ON [o].[ProductID] = [t1].[ProductID]
        INNER JOIN [Orders] [t4]
            LEFT JOIN [Employees] [t3]
                LEFT JOIN [Employees] [t2] ON [t3].[ReportsTo] = [t2].[EmployeeID]
            ON [t4].[EmployeeID] = [t3].[EmployeeID]
        ON [o].[OrderID] = [t4].[OrderID]

Обратите внимание на то, что у нас для разных таблиц используются разные типы Join. Это результат применения параметра CanBeNull.

Ещё одним интересным моментом является использования в Select поля OrderID из таблицы OrderDetails, хотя в самом Linq запросе используется поле таблицы Orders. Это как раз та самая оптимизация, которую выполняет BLToolkit для убирания лишних связей. Давайте немного изменим наш пример:

from o in db.OrderDetail
select new
{
    o.Product.ProductName,
    o.Order.OrderID,
    //o.Order.Employee.ReportsToEmployee.Region
};

SQL:

SELECT
    [t1].[ProductName],
    [o].[OrderID]
FROM
    [Order Details] [o]
        INNER JOIN [Products] [t1] ON [o].[ProductID] = [t1].[ProductID]

Так как таблица Order для доступа к Employee нам теперь не нужна, а использование OrderID из OrderDetails не меняет логики запроса, то связь с Order тоже исчезла.

Кроме всего прочего однотипные ассоциативные объекты разных сущностей можно сравнивать:

from o in db.Order
from t in db.EmployeeTerritory
where o.Employee == t.Employee
select new
{
    o.OrderID,
    o.EmployeeID,
    t.TerritoryID
};

SQL:

SELECT
    [o].[OrderID],
    [o].[EmployeeID],
    [t1].[TerritoryID]
FROM
    [Orders] [o], [EmployeeTerritories] [t1]
WHERE
    [o].[EmployeeID] = [t1].[EmployeeID]

И использовать для группировки:

from p in db.Product
group p by p.Category into g
where g.Count() == 12
select g.Key.CategoryName;

SQL:

SELECT
    [t1].[CategoryName]
FROM
    [Products] [p]
        INNER JOIN [Categories] [t1] ON [p].[CategoryID] = [t1].[CategoryID]
GROUP BY
    [t1].[CategoryID],
    [t1].[CategoryName]
HAVING
    Count(*) = 12

Для сравнения и группировки по объектам BLToolkit использует информацию о первичных ключах объекта. Если первичный ключ не задан, то BLToolkit генерирует код, сравнивающий все поля объекта друг с другом. Это, во-первых, не эффективно, а, во-вторых, не всегда возможно, т.к. не все типы в SQL можно сравнивать.

В качестве эксперимента давайте попробуем убрать у сущности Employee первичный ключ и выполним предыдущий запрос ещё раз. Полученный SQL:

SELECT
    [o].[OrderID],
    [o].[EmployeeID],
    [t3].[TerritoryID]
FROM
    [Orders] [o]
        LEFT JOIN [Employees] [t1] ON [o].[EmployeeID] = [t1].[EmployeeID],
    [EmployeeTerritories] [t3]
        LEFT JOIN [Employees] [t2] ON [t3].[EmployeeID] = [t2].[EmployeeID]
WHERE
    [o].[EmployeeID] = [t3].[EmployeeID] AND
    [t1].[LastName] = [t2].[LastName] AND
    [t1].[FirstName] = [t2].[FirstName] AND
    [t1].[Title] = [t2].[Title] AND
    [t1].[TitleOfCourtesy] = [t2].[TitleOfCourtesy] AND
    ([t1].[BirthDate] IS NULL AND [t2].[BirthDate] IS NULL OR [t1].[BirthDate] IS NOT NULL AND [t2].[BirthDate] IS NOT NULL AND [t1].[BirthDate] = [t2].[BirthDate]) AND
    ([t1].[HireDate] IS NULL AND [t2].[HireDate] IS NULL OR [t1].[HireDate] IS NOT NULL AND [t2].[HireDate] IS NOT NULL AND [t1].[HireDate] = [t2].[HireDate]) AND
    [t1].[Address] = [t2].[Address] AND
    [t1].[City] = [t2].[City] AND
    [t1].[Region] = [t2].[Region] AND
    [t1].[PostalCode] = [t2].[PostalCode] AND
    [t1].[Country] = [t2].[Country] AND
    [t1].[HomePhone] = [t2].[HomePhone] AND
    [t1].[Extension] = [t2].[Extension] AND
    [t1].[Notes] = [t2].[Notes] AND
    ([t1].[ReportsTo] IS NULL AND [t2].[ReportsTo] IS NULL OR [t1].[ReportsTo] IS NOT NULL AND [t2].[ReportsTo] IS NOT NULL AND [t1].[ReportsTo] = [t2].[ReportsTo]) AND
    [t1].[PhotoPath] = [t2].[PhotoPath]

Мало того, что на этот запрос без слёз смотреть нельзя, так он ещё и не работает, так как среди полей таблицы Employees есть поля с типом ntext, которые нельзя сравнивать.

СОВЕТ

Будьте внимательны при описании модели данных. BLToolkit использует метаинформацию о ваших сущностях для оптимизации запросов и её отсутствие может привести к построению менее оптимальных запросов.

Наследование

Поддержка наследования в BLToolkit выполнена по образу и подобию наследования в Linq To SQL. Для организации наследования требуется специальная разметка сущностей как в следующем коде:

[TableName("Products")]
[InheritanceMapping(Code="True",  Type=typeof(DiscontinuedProduct))]
[InheritanceMapping(Code="False", Type=typeof(ActiveProduct))]
public abstract class Product
{
    [PrimaryKey, Identity]                      public int      ProductID;
    [NotNull]                                   public string   ProductName;
                                                public int?     SupplierID;
                                                public int?     CategoryID;
                                                public string   QuantityPerUnit;
                                                public decimal? UnitPrice;
                                                public short?   UnitsInStock;
                                                public short?   UnitsOnOrder;
                                                public short?   ReorderLevel;
    [MapField(IsInheritanceDiscriminator=true)] public bool     Discontinued;
}

public class ActiveProduct : Product
{
}

public class DiscontinuedProduct : Product
{
}

Здесь поле Discontinued играет роль дискриминатора - признака, по которому объекты делятся на те или иные типы. Атрибут InheritanceMapping позволяет сопоставить значение этого поля и тип, которому это поле соответствует. В этом примере типы ActiveProduct и DiscontinuedProduct не имеют специфичных для типа полей, но в принципе это не запрещено.

Более подробную информацию можно найти по следующей ссылке - Linq To SQL Inheritance.

Ниже приведены несколько примеров использования наследования.

from p in db.DiscontinuedProduct
select p;

SQL:

SELECT
    [p].[ProductID],
    [p].[ProductName],
    [p].[SupplierID],
    [p].[CategoryID],
    [p].[QuantityPerUnit],
    [p].[UnitPrice],
    [p].[UnitsInStock],
    [p].[UnitsOnOrder],
    [p].[ReorderLevel],
    [p].[Discontinued]
FROM
    [Products] [p]
WHERE
    [p].[Discontinued] = 'True'

В точности такой же SQL будет сгенерирован и для следующего теста:

from c in db.Product
where c is Northwind.DiscontinuedProduct
select c;

Стандартные функции .NET Framework

BLToolkit поддерживает преобразование в SQL около четырёхсот стандартных функций .NET Framework. Сюда входят функции работы со строками, датами, математические функции, функции преобразования и конвертирования скалярных типов данных. Если какая-либо функция не имеет прямого аналога в SQL, то она заменяется на специфичный для конкретного сервера базы данных алгоритм. Ниже приведено несколько примеров.

Здесь всё просто. Свойство Length имеет прямую реализацию в SQL:

from c in db.Customer
where c.ContactName.Length > 5
select c.ContactName;

SQL:

SELECT
    [p].[ContactName]
FROM
    [Customers] [p]
WHERE
    Len([p].[ContactName]) > 5

Здесь функция Compare преобразуется в соответствующую конструкцию SQL:

from c in db.Customer
where c.ContactName.CompareTo("John") > 0
select c.ContactName;

SQL:

SELECT
    [p].[ContactName]
FROM
    [Customers] [p]
WHERE
    [p].[ContactName] > 'John'

Следующий запрос реализует на SQL функцию эквивалентную функции Math.Round в .NET. Дело в том, что эта функция по умолчанию округляет число к ближайшему чётному, поэтому мы может наблюдать такую необычную реализацию.

from o in db.Order
where Math.Round(o.Freight) >= 10
select o.Freight;

SQL:

SELECT
    [o].[Freight]
FROM
    [Orders] [o]
WHERE
    CASE
        WHEN [o].[Freight] - Floor([o].[Freight]) = 0.5 AND Floor([o].[Freight]) % 2 = 0
            THEN Floor([o].[Freight])
        ELSE Round([o].[Freight], 0)
    END >= 10

Выглядит это всё пока вполне безобидно, но здесь кроется одна неприятная деталь. Давайте немного усложним наш запрос:

from o in db.Order
where Math.Round(o.OrderDetails.Sum(d => d.Quantity * d.UnitPrice)) >= 10
select o.Freight;

SQL:

SELECT
    [o].[Freight]
FROM
    [Orders] [o]
        INNER JOIN [Order Details] [t1] ON [o].[OrderID] = [t1].[OrderID]
WHERE
    CASE
        WHEN (
            SELECT
                Sum(Convert(Decimal(5,0), [t2].[Quantity]) * [t2].[UnitPrice])
            FROM
                [Order Details] [t2]
            WHERE
                [o].[OrderID] = [t2].[OrderID]
        ) - Floor((
            SELECT
                Sum(Convert(Decimal(5,0), [t3].[Quantity]) * [t3].[UnitPrice])
            FROM
                [Order Details] [t3]
            WHERE
                [o].[OrderID] = [t3].[OrderID]
        )) = 0.5 AND Floor((
            SELECT
                Sum(Convert(Decimal(5,0), [t4].[Quantity]) * [t4].[UnitPrice])
            FROM
                [Order Details] [t4]
            WHERE
                [o].[OrderID] = [t4].[OrderID]
        )) % 2 = 0
            THEN Floor((
            SELECT
                Sum(Convert(Decimal(5,0), [t5].[Quantity]) * [t5].[UnitPrice])
            FROM
                [Order Details] [t5]
            WHERE
                [o].[OrderID] = [t5].[OrderID]
        ))
        ELSE Round((
            SELECT
                Sum(Convert(Decimal(5,0), [t6].[Quantity]) * [t6].[UnitPrice])
            FROM
                [Order Details] [t6]
            WHERE
                [o].[OrderID] = [t6].[OrderID]
        ), 0)
    END >= 10

Результат уже не столь ожидаем. Поэтому такой запрос лучше переписать следующим образом:

from o in db.Order
let sum = o.OrderDetails.Sum(d => d.Quantity * d.UnitPrice)
where Math.Round(sum) >= 10
select o.Freight;

SQL:

SELECT
    [o1].[Freight] as [Freight1]
FROM
    (
        SELECT
            (
                SELECT
                    Sum(Convert(Decimal(5,0), [t1].[Quantity]) * [t1].[UnitPrice])
                FROM
                    [Order Details] [t1]
                WHERE
                    [o].[OrderID] = [t1].[OrderID]
            ) as [sum1],
            [o].[Freight]
        FROM
            [Orders] [o]
                INNER JOIN [Order Details] [t2] ON [o].[OrderID] = [t2].[OrderID]
    ) [o1]
WHERE
    CASE
        WHEN [o1].[sum1] - Floor([o1].[sum1]) = 0.5 AND Floor([o1].[sum1]) % 2 = 0
            THEN Floor([o1].[sum1])
        ELSE Round([o1].[sum1], 0)
    END >= 10

Так гораздо лучше. Фактически предыдущий запрос эквивалентен следующему:

from o in
    from o in db.Order
    select new
    {
        sum = o.OrderDetails.Sum(d => d.Quantity * d.UnitPrice),
        o
    }
where  Math.Round(o.sum) >= 10
select o.o.Freight;

Конструкция let заворачивается ещё в один Select, для которого BLToolkit создаёт подзапрос. Но подзапросы создаются не для всякого Select. Так, например, для следующего кода подзапрос создан не будет:

from o in db.Order
let sum = o.Freight
where  Math.Round(sum) >= 10
select o.Freight;

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

Дополнительные возможности BLToolkit

Кроме поддержки стандартных возможностей Linq, BLToolkit реализует ряд дополнительных расширений.

Select

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

SELECT
    CURRENT_TIMESTAMP as [c1]

Такая функциональность в BLToolkit реализуется следующим образом:

static void SimpleSelectTest()
{
    using (var db = new NorthwindDB())
    {
        var value = db.Select(() => Sql.CurrentTimestamp);

        Console.WriteLine(value);
    }
}

DML операции

Все существующие Linq провайдеры реализуют операции манипулирования данными (Insert, Update, Delete) посредством Entity Services, что вызывает много нареканий среди разработчиков по многим причинам. BLToolkit предлагает полный набор операций DML, включая операции манипулирования множеством записей одним запросом.

Insert

Следующий метод предназначен для добавления новой записи в таблицу БД.

db.Employee.Insert(() => new Northwind.Employee
{
    FirstName = "John",
    LastName  = "Shepard",
    Title     = "Spectre",
    HireDate  = Sql.CurrentTimestamp
});

SQL:

INSERT INTO [Employees]
(
    [FirstName],
    [LastName],
    [Title],
    [HireDate]
)
VALUES
(
    'John',
    'Shepard',
    'Spectre',
    CURRENT_TIMESTAMP
)

К сожалению, таким способом нельзя работать с объектами, у которых нет конструктора по умолчанию или есть свойства только для чтения. Поэтому для таких объектов BLToolkit предлагает альтернативный Insert:

db
    .Into(db.Employee)
        .Value(e => e.FirstName, "John")
        .Value(e => e.LastName,  "Shepard")
        .Value(e => e.Title,     "Spectre")
        .Value(e => e.HireDate,  () => Sql.CurrentTimestamp)
    .Insert();

SQL для данного запроса будет аналогичен предыдущему.

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

Следующая группа функций позволяет работать с множеством записей:

db.Region
    .Where(r => r.RegionID > 2)
    .Insert(db.Region, r => new Northwind.Region()
    {
        RegionID          = r.RegionID + 100,
        RegionDescription = "Copy Of " + r.RegionDescription
    });

И альтернативный вариант:

db.Region
    .Where(r => r.RegionID > 2)
    .Into(db.Region)
        .Value(_ => _.RegionID,          r => r.RegionID + 100)
        .Value(_ => _.RegionDescription, r => "Copy Of " + r.RegionDescription)
    .Insert();

SQL:

INSERT INTO [Region]
(
    [RegionID],
    [RegionDescription]
)
SELECT
    [r].[RegionID] + 100,
    'Copy Of ' + [r].[RegionDescription]
FROM
    [Region] [r]
WHERE
    [r].[RegionID] > 2

InsertWithIdentity

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

var value = db.Employee.InsertWithIdentity(() => new Northwind.Employee
{
    FirstName = "John",
    LastName  = "Shepard",
    Title     = "Spectre",
    HireDate  = Sql.CurrentTimestamp
});

var value =
    db
        .Into(db.Employee)
            .Value(e => e.FirstName, "John")
            .Value(e => e.LastName,  "Shepard")
            .Value(e => e.Title,     () => "Spectre")
            .Value(e => e.HireDate,  () => Sql.CurrentTimestamp)
        .InsertWithIdentity();

SQL:

INSERT INTO [Employees]
(
    [FirstName],
    [LastName],
    [Title],
    [HireDate]
)
VALUES
(
    'John',
    'Shepard',
    'Spectre',
    CURRENT_TIMESTAMP
)

SELECT SCOPE_IDENTITY()

Update

Следующие три метода позволяют обновлять одну или более записей в таблице.

Обновление с предикатом:

db.Employee
    .Update(
        e => e.Title == "Spectre",
        e => new Northwind.Employee
        {
            Title = "Commander"
        });

Обновление с подзапросом:

db.Employee
    .Where(e => e.Title == "Spectre")
    .Update(e => new Northwind.Employee
    {
        Title = "Commander"
    });

Обновление свойства только для чтения:

db.Employee
    .Where(e => e.Title == "Spectre")
    .Set(e => e.Title, "Commander")
    .Update();

Для всех этих запросов генерируется следующий SQL:

UPDATE
    [e]
SET
    [Title] = 'Commander'
FROM
    [Employees] [e]
WHERE
    [e].[Title] = 'Spectre'

Ещё одна операция обновления, в которой участвует одно из полей самой таблицы:

db.Employee
  .Where(e => e.Title == "Spectre")
  .Set(e => e.HireDate, e => e.HireDate.Value.AddDays(10))
  .Update();

SQL:

UPDATE
  [e]
SET
  [HireDate] = DateAdd(Day, 10, [e].[HireDate])
FROM
  [Employees] [e]
WHERE
  [e].[Title] = 'Spectre'

Delete

И в завершении операция удаления.

Удаление с предикатом:

db.Employee.Delete(e => e.Title == "Spectre");

Удаление с подзапросом:

db.Employee
    .Where(e => e.Title == "Spectre")
    .Delete();

SQL:

DELETE [e]
FROM
    [Employees] [e]
WHERE
    [e].[Title] = 'Spectre'

SQL функции

Наряду со стандартными функциями .NET Framework, BLToolkit позволяет использовать SQL функции, которые предоставляются базами данных. Два следующих примера аналогичны примерам, которые мы рассмотрели выше для стандартных функций .NET Framework, но вместо них они используют функции SQL.

Использование функции вычисления длины строки:

from c in db.Customer
where Sql.Length(c.ContactName) > 5
select c.ContactName;

SQL:

SELECT
    [c].[ContactName]
FROM
    [Customers] [c]
WHERE
    Len([c].[ContactName]) > 5

Функция округления:

from o in db.Order
where  Sql.Round(o.Freight) >= 10
select o.Freight;

SQL:

SELECT
    [o].[Freight]
FROM
    [Orders] [o]
WHERE
    Round([o].[Freight], 0) >= 10

Обратите внимание, в этот раз функция округления вызывается напрямую так как она присутствует в SQL.

Расширение BLToolkit

SQL функции

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

[SqlFunction]
[SqlFunction("Access",    "Len")]
[SqlFunction("Firebird",  "Char_Length")]
[SqlFunction("MsSql2005", "Len")]
[SqlFunction("MsSql2008", "Len")]
[SqlFunction("SqlCe",     "Len")]
[SqlFunction("Sybase",    "Len")]
public static int? Length(string str)
{
    return str == null ? null : (int?)str.Length;
}

Таким образом, SQL функция - это обычный метод, помеченный атрибутом SqlFunction. Атрибут SqlFunction включает следующие свойства:

  • Name - имя SQL функции. Если имя не задано, то в качестве него используется имя метода.
  • SqlProvider - имя SQL провайдера. Если среди атрибутов нет атрибута с искомым провайдером, то берётся атрибут без указания провайдера.
  • ArgIndices - массив, позволяющий ограничить или указать порядок следования аргументов SQL функции относительно метода. Например, это бывает полезным когда одна и та же функция для разных баз данных имеет не только разные имена, но и порядок следования аргументов.
  • ServerSideOnly - флаг, запрещающий выполнять функцию на клиенте. Всегда приводит к генерации SQL.

Последний параметр нуждается в особом объяснении. Дело в том, что BLToolkit пытается минимизировать нагрузку на сервер БД и при возможности переложить выполнение определённых функций на клиента. Но это поведение не всегда бывает полезно. Если SQL функция отмечена флагом ServerSideOnly, то функция в любом случае транслируется в SQL.

Так же BLToolkit содержит специальную функцию, которую можно применять чтобы заставить преобразовывать в SQL те выражения, которые изначально этого не предполагали. Рассмотрим пример всё с той же функцией вычисления длины строки:

from c in db.Customer
select Sql.Length(c.ContactName);

SQL:

SELECT
    [c].[ContactName]
FROM
    [Customers] [c]

В этом случае строка передается клиенту, где уже и вычисляется её длина.

В следующем примере мы используем функцию Sql.AsSql, чтобы предотвратить такое поведение:

from c in db.Customer
select Sql.AsSql(Sql.Length(c.ContactName));

SQL:

SELECT
    Len([c].[ContactName]) as [c1]
FROM
    [Customers] [c]

Вы наверняка уже догадались как должна выглядеть такая функция:

[SqlExpression("{0}", 0, ServerSideOnly=true)]
public static T AsSql<T>(T obj)
{
    return obj;
}

Примечательным здесь является то, что в данном случае мы используем не SqlFunction атрибут, а похожий атрибут SqlExpression. Он во многом аналогичен SqlFunction, но принимает в качестве параметра форматную строку, с помощью которой строит SQL выражение. Аргументами форматной строки являются параметры метода, а, если мы имеем дело с обобщённым методом или классом, то и обобщённые параметры. Таким образом функции Sql.AsSql будет передано два параметра: obj и T. Но так как параметр T нас в данном случае не интересует, то второй параметр атрибута SqlExpression в нашем примере как раз говорит BLToolkit, что использовать следует только один параметр с индексом 0.

Стоит так же упомянуть ещё один атрибут - SqlProperty. Фактически это просто функция без параметров. Такое свойство Sql.CurrentTimestamp мы уже встречали раньше. Вот его реализация:

[SqlProperty("CURRENT_TIMESTAMP",   ServerSideOnly = true)]
[SqlProperty("Informix", "CURRENT", ServerSideOnly = true)]
[SqlProperty("Access",   "Now",     ServerSideOnly = true)]
[SqlFunction("SqlCe",    "GetDate", ServerSideOnly = true)]
[SqlFunction("Sybase",   "GetDate", ServerSideOnly = true)]
public static DateTime CurrentTimestamp
{
    get { throw new LinqException("The 'CurrentTimestamp' is server side only property."); }
}

Атрибуты SqlFunction, SqlExpression и SqlProperty можно применять одновременно к одному методу для разных провайдеров. Вот, например, как выглядит метод TrimLeft:

[SqlExpression("DB2", "Strip({0}, L, {1})")]
[SqlFunction  (       "LTrim")]
public static string TrimLeft(string str, char? ch)
{
    return str == null || ch == null ? null : str.TrimStart(ch.Value);
}

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

Подстановка методов

SQL функции очень важные и полезные примитивы, но, к сожалению, создание более менее сложных алгоритмов, не предусмотренных SQL серверами, на них крайне затруднительны. Возьмём к примеру уже виденную нами функцию Math.Round. Её реализация на C# с использованием SQL примитивов выглядит следующим образом:

static decimal? RoundToEven(decimal? value)
{
    return
        value - Sql.Floor(value) == 0.5m && Sql.Floor(value) % 2 == 0?
            Sql.Floor(value) :
            Sql.Round(value);
}

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

Для реализации таких функций BLToolkit предлагает другой механизм.

Прежде всего нам понадобится метод, который мы будем вызывать в наших Linq запросах. Приведённый выше RoundToEven вполне подойдёт. Далее нам нужно зарегистрировать эту функцию в BLToolkit и привязать к ней необходимое выражение. Делается это следующим образом:

static void RoundToEvenTest()
{
    Expressions.MapMember<decimal?,decimal?>(
        value => RoundToEven(value),
        value =>
            value - Sql.Floor(value) == 0.5m && Sql.Floor(value) % 2 == 0?
                Sql.Floor(value) :
                Sql.Round(value));

    using (var db = new NorthwindDB())
    {
        var query =
            from o in db.Order
            let sum = o.OrderDetails.Sum(d => d.Quantity * d.UnitPrice)
            where RoundToEven(sum) >= 10
            select o.Freight;

        foreach (var item in query)
        {
            Console.WriteLine(item);
        }
    }
}

Первым аргументом метода MapMember может быть либо тип MemberInfo, либо лямбда, из которой этот MemberInfo можно легко получить. Второй параметр - это выражение, которое будет подставлено вместо первого метода, когда BLToolkit встретит такой метод в Linq выражении.

Поддержка всех стандартных функций .NET Framework в BLToolkit реализована именно таким способом.

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

Повторное использование выражений

Приведённый выше способ хорош для реализации глобальных, универсальных функций. Но иногда требуется повторно использовать Linq выражения для конкретной бизнес логики. Для этого в BLToolkit имеется похожий механизм, позволяющей локально объявлять методы и выражения для подстановки.

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

from c in db.Customer
select new
{
    sum1 = c.Orders.Count(o => o.ShipRegion == "SP"),
    sum2 = c.Orders.Count(o => o.ShipRegion == "NM")
};

Это можно сделать следующим образом:

[MethodExpression("OrderCountExpression")]
static int OrderCount(Northwind.Customer customer, string region)
{
    throw new NotImplementedException();
}

static Expression<Func<Northwind.Customer,string,int>> OrderCountExpression()
{
    return (customer, region) => customer.Orders.Count(o => o.ShipRegion == region);
}

static void MethodExpressionTest()
{
    using (var db = new NorthwindDB())
    {
        var query =
            from c in db.Customer
            select new
            {
                sum1 = OrderCount(c, "SP"),
                sum2 = OrderCount(c, "NM")
            };

        foreach (var item in query)
        {
            Console.WriteLine(item);
        }
    }
}

Здесь OrderCount является методом, который мы будем использовать в нашем Linq выражении и заменять его другим подстановочным выражением. Каким именно выражением его заменять определяется атрибутом MethodExpression. Этот атрибут задаёт метод, который нам и будет возвращать результирующее выражение. В нашем примере это метод OrderCountExpression.

Ниже приведён результирующий SQL:

SELECT
    (
        SELECT
            Count(*)
        FROM
            [Orders] [o]
        WHERE
            [c].[CustomerID] = [o].[CustomerID] AND [o].[ShipRegion] = 'SP'
    ) as [c1],
    (
        SELECT
            Count(*)
        FROM
            [Orders] [o1]
        WHERE
            [c].[CustomerID] = [o1].[CustomerID] AND [o1].[ShipRegion] = 'NM'
    ) as [c2]
FROM
    [Customers] [c]

Производительность

Проблемы производительности в Linq

Возможность создания Linq провайдеров, транслирующих Linq запросы в SQL и прочие форматы несомненно является большим достижением господствующих в индустрии технологий. Но какова плата за такие достижения? В статье Building a LINQ Provider кратко описывается процесс создания провайдера подобного Linq To SQL. Кроме всего прочего эта статья описывает путь, который проходит Linq выражение от структуры, созданной компилятором, до превращения её в SQL или другой формат.

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

Можно было бы кэшировать результаты парсинга, но кроме того, что мы каждый раз имеем дело с вновь созданной структурой, она может ещё и отличаться. Отличия могут возникать, во-первых, из-за возможности создавать такую структуру по частям, как, например, в следующем примере:

var query =
    from e in db.Employee
    select e;

if (a > 0)
{
    query =
        from e in query
        where e.EmployeeID > id
        select e;
}

Во-вторых, значения передаваемых параметров, таких как параметр id в примере выше, встраиваются непосредственно в Expression Tree, делая эту структуру зависящей от значений параметров.

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

Если обратиться к тестам сайта ORMBattle.net, например к этой диаграмме, то можно легко убедиться в справедливости данного предположения. Ниже приведена таблица с цифрами из диаграммы, которые нам будут интересны.

 | BLT | EF | ES | DO | L2S | SQL | Unit
LINQ Query | 8749 | 450 | 933 | 1559 | 835 | n/a | queries/s
Compiled LINQ Query | 13222 | 6501 | 2395 | 8046 | 9114 | n/a | queries/s
Native Query | 17187 | 9728 | 8465 | 10335 | n/a | 18645 | queries/s

Здесь по горизонтали:

  • LINQ Query - это Linq запросы, реализуемые разными провайдерами.
  • Compiled Linq Query - скомпилированные Linq запросы. О них речь пойдёт ниже.
  • Native Query - запросы к БД без применения Linq.

По вертикали перечислены различные Linq провайдеры, предпоследняя колонка - чистый SQL, последняя - единица измерения (запросов в секунду). Максимально достижимый эталон выделен жирным - это SQL / Native Query.

Сравнивая эталон с цифрами из строки LINQ Query можно заметить, что для некоторых провайдеров замедление клиентского кода, при использовании Linq запросов, может составлять десятки! раз.

Как BLToolkit борется с этой ситуацией при условии, что алгоритм парсинга Expression Tree в BLToolkit ничуть не менее сложный и ресурсоёмкий? Мы помним, что кэшировать Expression Tree нельзя, но элементарная логика утверждает, что если очень хочется, то можно.

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

CompiledQuery

Для решения вышеописанной проблемы многие провайдеры, включая BLToolkit, предлагают так называемые Compiled Query - компиляцию Linq запросов в лямбды, которые в дальнейшем можно использовать без повторного парсинга. Выглядит это следующим образом:

static Func<NorthwindDB,int,IEnumerable<Northwind.Employee>> _query =
    CompiledQuery.Compile<NorthwindDB,int,IEnumerable<Northwind.Employee>>((db, n) =>
        from e in db.Employee
        where e.EmployeeID > n
        orderby e.LastName, e.FirstName
        select e
    );

static void CompiledQueryTest()
{
    using (var db = new NorthwindDB())
    {
        var query = _query(db, 5);

        foreach (var item in query)
        {
            Console.WriteLine(item);
        }
    }
}

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

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

Заключение

В конце хочется поблагодарить всех, кто принимал участие в обсуждении и тестировании Linq провайдера BLToolkit. Отдельная благодарность всему проекту RSDN@Home, в очередной раз ставшему полигоном новых идей, и лично Андрею Корявченко за позитивную критику, выявленные ошибки и множество интересных идей.

Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
<<Показать меню Сообщений 21 [image] Оценка 1595 [image] [image] Оценить