Приветствую, Хабр!
Эта статья продолжит тему автоматической кодогенерации в Visual Studio с помощью T4 — Text Template Transformation Toolkit. Часть №1, чуть ранее опубликованная в этом блоге, описывала синтаксис T4 и элементарные принципы его использования. В этот же раз я решил подробнее осмотреть блог уважаемого Олега Сыча и ещё немного адаптировать к хабрааудитории некоторые из его наработок. В частности, сегодня обсудим следующие темы:
Я не стал изобретать каких-то особенных примеров. История развития запроса с созданием хранимой процедуры, описанная Олегом, идеально подходит для иллюстрации проблемы, при которой могут потребоваться генераторы. Причем, что характерно, — не надуманной проблемы. Также статья придерживается принципа «меньше слов — больше кода».
Здесь и далее предполагается, что у вас установлена Visual Studio 2005/2008/2010, а также T4 Toolbox.
Итак, у нас есть база данных. В этой базе данных есть таблица. А в таблице, в лучших традициях Кощея Бессмертного, есть строка, которую мы желаем удалить. Причём удалить не просто так, а свежесозданной хранимой процедурой. Пускай для простоты параметры, передающиеся в процедуру — все до единого поля таблицы, по которым и идентифицируется строки, подлежащие удалению. Набивать запрос для создания такой процедуры вручную — работа неблагодарная, поэтому мы применяем Т4 и создаём код, подобный приведённому ниже:
Здесь, чтобы получить информацию о полях таблицы из БД локального SQL-сервера, используется SQL Server Management Objects (SMO). После сохранения файла на выходе получим искомый запрос. Например, он может выглядеть так:
Первый же вопрос, который должен возникнуть в голове у человека, взглянувшего на этот код: почему названия БД и таблицы жёстко забиты в код? Получается, для создания каждого нового запроса программисту придётся залезать в шаблон и менять его вручную в нескольких местах?
На самом деле не обязательно. Достаточно воспользоваться другим способом генерации. Добавляем в проект новый файл, воспользовавшись теперь вариантом Template вместо File из T4 Toolbox, назовём его, к примеру, DeleteProcedureTemplate.tt. Как видно, среда автоматически создала заготовку параметризированного шаблона, то есть файла, который мы потом будем включать в другие шаблоны с целью использовать его в обобщённом виде.
Не пытайтесь найти в пространстве имён Microsoft.VisualStudio.TextTemplating класс Template: его там нет. Это абстрактный класс, определённый в T4Toolbox, а файл T4Toolbox.tt непосредственно в самих параметризированных шаблонах подключать нет смысла. Поэтому при каждом сохранении DeleteProcedureTemplate.tt Студия попытается обработать его, сгенерировать выходной файл, потерпит крах и уведомит вас об этом ошибкой. Это неприятное поведение можно легко убрать, заглянув в окно Properties для нашего редактируемого файла и установив там свойство Custom Tool в пустую строку. Теперь попытки неявной генерации не происходит.
Метод RenderCore() класса Template — основная точка работы параметризированного шаблона. Именно в нём происходит генерация той части текста, за которую и будет в конечном счёте отвечать наш шаблон в генераторе. Поэтому, не мудрствуя лукаво, просто перенесём в него уже готовый код.
Главное изменение, которому подвергся шаблон — это добавление в него открытых полей DatabaseName и TableName, которые, собственно, и выполняют функцию параметризации. Теперь файлы данных и логики разделены. Единственное, что осталось — воспользоваться директивой include и запускать попеременно один и тот же шаблон на разных БД и таблицах, как здесь:
При желании подобным же методом можно создать универсальный шаблон, создающий в зависимости от параметров хранимую процедуру на основе SELECT, INSERT и UPDATE, а не только DELETE. Код шаблона каждый может теперь составить самостоятельно.
Небольшое отступление в сторону. На первый взгляд весь описанный материал кажется элементарным. Да собственно, он таким и является, как и весь T4 сам по себе. Вопрос в другом: эти возможности включены в стандартную поставку, и подобной простой статьёй-компиляцией я хочу уберечь читателей (а читатели у Хабра разной степени опытности) от опасности нагородить собственных велосипедов. Описанные ниже генераторы шаблонов — ещё один из подобных подводных камней.
В параметризированных шаблонах всё ещё остаётся один неучтённый недочёт. Да, мы вынесли логику запуска шаблона с конкретными именами БД и таблицы в отдельный файл, но в случае работы с несколькими возможными БД и таблицами подобное решение есть замена шила на мыло. Программист всё ещё вынужден плодить отдельные файлики шаблона для каждой конкретной таблицы. Пускай эти файлы теперь заметно усохли в размере, однако проблему копипаста никто не отменял. В идеале хотелось бы за одно движение создать отдельный запросы к каждой таблице данной конкретной БД. Это возможно? Да.
Добавим в проект третий полезный тип шаблона, разработанный в рамках T4Toolbox — Generator. Генератор будет использовать наш параметризированный шаблон для того чтобы подставлять в него разные значения параметров (имён БД и таблицы) и направлять результат обработки в разные файлы. С последней целью в классе Template предусмотрен замечательный метод RenderToFile.
Итак, пусть файл генератора называется CrudProcedureGenerator.tt и дефолтная заготовка для него, как вы воочию можете убедиться, выглядит следующим образом:
Генератор сам не проводит никакой обработки текста T4, он лишь запускает по очереди другие, уже написанные базовые шаблоны. Поэтому его соответствующие основные методы вместо Render и RenderCore называются Run и RunCore соответственно. Подстроим же их для себя:
Здесь перебираются все таблицы в одной заданной БД и для каждой экземпляр класса DeleteProcedureTemplate создаёт отдельный уникальный выходной файл с запросом. Для полного счастья не хватает лишь задать БД и запустить полный цикл обработки:
Результат на ваших экранах:
Следуя обыкновенной логике, статью о генераторах стоило бы необходимо завершить заметками о том, как их отлаживать, тестировать и безболезненно модифицировать под расширенные нужды. К сожалению, такого количества лишнего программного кода эта одна хабрастатья просто не вынесет, а выносить его в третью часть тоже не хочется, материал получится бедный и оторванный от коллектива. Поэтому ограничусь советами, на основе которых, а также исходников из блога Олега Сыча, читатель сможет без каких-либо проблем использовать генераторы в жизни.
Handling errors in code generators
Unit testing code generators
Making code generators extensible
Эта статья продолжит тему автоматической кодогенерации в 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.5” hostspecific=”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.5” hostspecific=”True” debug=”True” #>
<#@ output extension=”txt” #>
<#@ include file=”T4Toolbox.tt” #>
<#@ include file=”CrudProcedureGenerator.tt” #>
<#
CrudProcedureGenerator generator = new CrudProcedureGenerator();
generator.DatabaseName = “Northwind”;
generator.Run();
#>
Результат на ваших экранах:
Постскриптум. Особенности использования генераторов
Следуя обыкновенной логике, статью о генераторах стоило бы необходимо завершить заметками о том, как их отлаживать, тестировать и безболезненно модифицировать под расширенные нужды. К сожалению, такого количества лишнего программного кода эта одна хабрастатья просто не вынесет, а выносить его в третью часть тоже не хочется, материал получится бедный и оторванный от коллектива. Поэтому ограничусь советами, на основе которых, а также исходников из блога Олега Сыча, читатель сможет без каких-либо проблем использовать генераторы в жизни.
- Для тестирования генераторов в T4 Toolbox предусмотрена своя отличная заготовочка под именем «Unit Test».
- Если необходимо модифицировать сердце генерации — код, создаваемый шаблоном, — незачем править непосредственно файл с его классом (в данном случае DeleteProcedureTemplate). Достаточно импортировать в генератор ещё один файлик, в котором описать наследника нашего шаблона с требуемыми коррективами.
- У класса Generator есть функция Validate, которая вызывается методом Run в первую очередь, перед тем как заняться непосредственно генерацией кода (RunCore). Её можно использовать для проверки входящих параметров генератора.
- Для рапорта об ошибках можно использовать методы Error и Warning, аналогичные рассмотренным в классе TextTransformation.
Handling errors in code generators
Unit testing code generators
Making code generators extensible