Pull to refresh

Text Template Transformation Toolkit (T4), часть 2: генераторы шаблонов

Reading time9 min
Views4.1K
Приветствую, Хабр!

Эта статья продолжит тему автоматической кодогенерации в Visual Studio с помощью T4 — Text Template Transformation Toolkit. Часть №1, чуть ранее опубликованная в этом блоге, описывала синтаксис T4 и элементарные принципы его использования. В этот же раз я решил подробнее осмотреть блог уважаемого Олега Сыча и ещё немного адаптировать к хабрааудитории некоторые из его наработок. В частности, сегодня обсудим следующие темы:
  • Создание повторно используемых и параметризируемых шаблонов
  • Создание генераторов шаблонов
  • Отладка, тестирование и расширение генераторов (ссылки)

Я не стал изобретать каких-то особенных примеров. История развития запроса с созданием хранимой процедуры, описанная Олегом, идеально подходит для иллюстрации проблемы, при которой могут потребоваться генераторы. Причем, что характерно, — не надуманной проблемы. Также статья придерживается принципа «меньше слов — больше кода».
Здесь и далее предполагается, что у вас установлена Visual Studio 2005/2008/2010, а также T4 Toolbox.


Параметризируемые шаблоны


Начальная стадия


Итак, у нас есть база данных. В этой базе данных есть таблица. А в таблице, в лучших традициях Кощея Бессмертного, есть строка, которую мы желаем удалить. Причём удалить не просто так, а свежесозданной хранимой процедурой. Пускай для простоты параметры, передающиеся в процедуру — все до единого поля таблицы, по которым и идентифицируется строки, подлежащие удалению. Набивать запрос для создания такой процедуры вручную — работа неблагодарная, поэтому мы применяем Т4 и создаём код, подобный приведённому ниже:

<#@template language=C#v3.5” #>
<#@output extension=SQL” #>
<#@assembly name=Microsoft.SqlServer.ConnectionInfo” #>
<#@assembly name=Microsoft.SqlServer.Smo” #>
<#@import namespace=Microsoft.SqlServer.Management.Smo” #><#
    Server server = new Server();
    Database database = new Database(server, “Northwind”);
    Table table = new Table(database, “Products”);
    table.Refresh();
#>
create procedure <#= table.Name #>_Delete
<#
    PushIndent(”\t”);
    foreach (Column column in table.Columns)
        if (column.InPrimaryKey)
            WriteLine(”@” + column.Name + ” ” + column.DataType.Name);
    PopIndent();
#>
as
    delete from
<#= table.Name #>
    where
<#
    PushIndent(”\t\t”);
    foreach (Column column in table.Columns)
        if (column.InPrimaryKey)
            WriteLine(column.Name + ” = @” + column.Name);
    PopIndent();
#>    

Здесь, чтобы получить информацию о полях таблицы из БД локального SQL-сервера, используется SQL Server Management Objects (SMO). После сохранения файла на выходе получим искомый запрос. Например, он может выглядеть так:

create procedure Products_Delete
    @ProductID int
as
    delete from
Products
    where ProductID = @ProductID

Параметризация


Первый же вопрос, который должен возникнуть в голове у человека, взглянувшего на этот код: почему названия БД и таблицы жёстко забиты в код? Получается, для создания каждого нового запроса программисту придётся залезать в шаблон и менять его вручную в нескольких местах?
На самом деле не обязательно. Достаточно воспользоваться другим способом генерации. Добавляем в проект новый файл, воспользовавшись теперь вариантом Template вместо File из T4 Toolbox, назовём его, к примеру, DeleteProcedureTemplate.tt. Как видно, среда автоматически создала заготовку параметризированного шаблона, то есть файла, который мы потом будем включать в другие шаблоны с целью использовать его в обобщённом виде.

<#+
// <copyright file=”DeleteProcedureTemplate.tt” company=”Your Company”>
//  Copyright © Your Company. All Rights Reserved.
// </copyright>

public class DeleteProcedureTemplate : Template
{
    
protected override void RenderCore()
    {

    }
}
#>

Не пытайтесь найти в пространстве имён Microsoft.VisualStudio.TextTemplating класс Template: его там нет. Это абстрактный класс, определённый в T4Toolbox, а файл T4Toolbox.tt непосредственно в самих параметризированных шаблонах подключать нет смысла. Поэтому при каждом сохранении DeleteProcedureTemplate.tt Студия попытается обработать его, сгенерировать выходной файл, потерпит крах и уведомит вас об этом ошибкой. Это неприятное поведение можно легко убрать, заглянув в окно Properties для нашего редактируемого файла и установив там свойство Custom Tool в пустую строку. Теперь попытки неявной генерации не происходит.
Метод RenderCore() класса Template — основная точка работы параметризированного шаблона. Именно в нём происходит генерация той части текста, за которую и будет в конечном счёте отвечать наш шаблон в генераторе. Поэтому, не мудрствуя лукаво, просто перенесём в него уже готовый код.

<#@assembly name=Microsoft.SqlServer.ConnectionInfo” #>
<#@assembly name=Microsoft.SqlServer.Smo” #>
<#@import namespace=Microsoft.SqlServer.Management.Smo” #>
<#+
public class DeleteProcedureTemplate : Template
{
    public string DatabaseName;
    public string TableName;

    protected override void RenderCore()
    {
        Server server = new Server();
        Database database = new Database(server, DatabaseName);
        Table table = new Table(database, TableName);
        table.Refresh();
#>
create procedure <#= table.Name #>_Delete
<#+
        PushIndent(”\t”);
        foreach (Column column in table.Columns)
        {
            if (column.InPrimaryKey)
                WriteLine(”@” + column.Name + ” ” + column.DataType.Name);
        }
        PopIndent();
#>
as
    delete from
<#= table.Name #>
    where
<#+
        PushIndent(”\t\t”);
        foreach (Column column in table.Columns)
        {
            if (column.InPrimaryKey)
                WriteLine(column.Name + ” = @” + column.Name);
        }
        PopIndent();
    }
}
#>

Главное изменение, которому подвергся шаблон — это добавление в него открытых полей DatabaseName и TableName, которые, собственно, и выполняют функцию параметризации. Теперь файлы данных и логики разделены. Единственное, что осталось — воспользоваться директивой include и запускать попеременно один и тот же шаблон на разных БД и таблицах, как здесь:

<#@ template language=”C#v3.5hostspecific=”True” #>
<#@
output extension=”sql” #>
<#@
include file=”T4Toolbox.tt” #>
<#@
include file=”DeleteProcedureTemplate.tt” #>
<#
    DeleteProcedureTemplate template =
new DeleteProcedureTemplate();
    template.DatabaseName =
“Northwind”;
    template.TableName =
“Products”;
    template.Render();
#>

При желании подобным же методом можно создать универсальный шаблон, создающий в зависимости от параметров хранимую процедуру на основе SELECT, INSERT и UPDATE, а не только DELETE. Код шаблона каждый может теперь составить самостоятельно.

Небольшое отступление в сторону. На первый взгляд весь описанный материал кажется элементарным. Да собственно, он таким и является, как и весь T4 сам по себе. Вопрос в другом: эти возможности включены в стандартную поставку, и подобной простой статьёй-компиляцией я хочу уберечь читателей (а читатели у Хабра разной степени опытности) от опасности нагородить собственных велосипедов. Описанные ниже генераторы шаблонов — ещё один из подобных подводных камней.

Генераторы шаблонов


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

Добавим в проект третий полезный тип шаблона, разработанный в рамках T4Toolbox — Generator. Генератор будет использовать наш параметризированный шаблон для того чтобы подставлять в него разные значения параметров (имён БД и таблицы) и направлять результат обработки в разные файлы. С последней целью в классе Template предусмотрен замечательный метод RenderToFile.

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

<#+
// <copyright file=”CrudProcedureGenerator.tt” company=”Your Company”>
//  Copyright © Your Company. All Rights Reserved.
// </copyright>

public class CrudProcedureGenerator : Generator
{
    
protected override void RunCore()
    {

    }
}
#>

Генератор сам не проводит никакой обработки текста T4, он лишь запускает по очереди другие, уже написанные базовые шаблоны. Поэтому его соответствующие основные методы вместо Render и RenderCore называются Run и RunCore соответственно. Подстроим же их для себя:

<#@ assembly name=”Microsoft.SqlServer.ConnectionInfo” #>
<#@
assembly name=”Microsoft.SqlServer.Smo” #>
<#@
import namespace=”Microsoft.SqlServer.Management.Smo” #>
<#@
include file=”DeleteProcedureTemplate.tt” #>
<#+
public class CrudProcedureGenerator : Generator
{
  
public string DatabaseName;
  
public DeleteProcedureTemplate DeleteTemplate = new DeleteProcedureTemplate();  

  
protected override void RunCore()
  {
    Server server =
new Server();
    Database database =
new Database(server, this.DatabaseName);
    database.Refresh();
    
foreach (Table table in database.Tables)
    {
      
this.DeleteTemplate.DatabaseName = this.DatabaseName;
      
this.DeleteTemplate.TableName = table.Name;
      
this.DeleteTemplate.RenderToFile(table.Name + “_Delete.sql”);
    }
  }
}
#>

Здесь перебираются все таблицы в одной заданной БД и для каждой экземпляр класса DeleteProcedureTemplate создаёт отдельный уникальный выходной файл с запросом. Для полного счастья не хватает лишь задать БД и запустить полный цикл обработки:

<#@ template language=”C#v3.5hostspecific=”Truedebug=”True” #>
<#@
output extension=”txt” #>
<#@
include file=”T4Toolbox.tt” #>
<#@
include file=”CrudProcedureGenerator.tt” #>
<#
    CrudProcedureGenerator generator =
new CrudProcedureGenerator();
    generator.DatabaseName =
“Northwind”;
    generator.Run();
#>

Результат на ваших экранах:


Постскриптум. Особенности использования генераторов


Следуя обыкновенной логике, статью о генераторах стоило бы необходимо завершить заметками о том, как их отлаживать, тестировать и безболезненно модифицировать под расширенные нужды. К сожалению, такого количества лишнего программного кода эта одна хабрастатья просто не вынесет, а выносить его в третью часть тоже не хочется, материал получится бедный и оторванный от коллектива. Поэтому ограничусь советами, на основе которых, а также исходников из блога Олега Сыча, читатель сможет без каких-либо проблем использовать генераторы в жизни.
  1. Для тестирования генераторов в T4 Toolbox предусмотрена своя отличная заготовочка под именем «Unit Test».
  2. Если необходимо модифицировать сердце генерации — код, создаваемый шаблоном, — незачем править непосредственно файл с его классом (в данном случае DeleteProcedureTemplate). Достаточно импортировать в генератор ещё один файлик, в котором описать наследника нашего шаблона с требуемыми коррективами.
  3. У класса Generator есть функция Validate, которая вызывается методом Run в первую очередь, перед тем как заняться непосредственно генерацией кода (RunCore). Её можно использовать для проверки входящих параметров генератора.
  4. Для рапорта об ошибках можно использовать методы Error и Warning, аналогичные рассмотренным в классе TextTransformation.
Ссылки по теме:
Handling errors in code generators
Unit testing code generators
Making code generators extensible
Tags:
Hubs:
+5
Comments4

Articles

Change theme settings