Generando código con AjGenesis usando archivos de mapeo de NHibernate

Published on Author lopezLeave a comment

En estos días, estuve trabajando en generar código de clases C#, usando como punto de partida los archivos .hbm, que se usan en NHibernate para especificar el mapeo de clases y tablas de bases relacionales. Como es usual, cuando encaro algo de generación de código uso AjGenesis, mi proyecto open source de generación de código (practico el “dog fooding” :-).

(English version of this post Generating code with AjGenesis using NHibernate hbm files)

Pueden bajar un ejemplo de lo que estoy haciendo de mi Skydrive:

Examples > AjGenesis > NHibernateMappingExample01.zip

(el código en desarrollo está en el trunk, en este change actual, en el directorio examples\NHibernateMappinp:

pero si quieren ir directamente al ejemplo, pueden bajárselo completo desde el Skydrive que mencioné, que incluye el ejecutable de AjGenesis de la versión en desarrollo, no necesitan compilar nada).

Luego de expandir el archivo del ejemplo, tendrán este contenido:

 

Para crear clases C#, pueden probar de ejecutar los comandos:

GenerateClasses AjFirstExample
GenerateClasses AjTest

Para crear un proyecto .NET con los archivos .cs y .hbm, y una solución, ejecutar:

GenerateProject AjFirstExample
GenerateProject AjTest

Los archivos generados, en ambos casos, quedan en el directorio Build.

Hay dos proyectos ejemplo que son AjFirstExample, con dos mapeos simples, y AjTest, que tiene mapeos más interesantes, con “bags” y relaciones “many to one”.

En el ejemplo, cada proyecto se describe con un simple archivo Project.xml:

<Project Name="AjTest">
</Project>

En cuanto necesite más información, lo agregaré ahí, o en tags meta de los propios archivos de mapeo y configuración.

Este es uno de los archivos de mapeo que sirven de modelo inicial para este ejemplo de generación, en Projects\AjTest\Mappings, Department.hbm:

<?xml version="1.0" encoding="utf-8" ?> 
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
  assembly="AjTest.Entities"
  namespace="AjTest.Entities"
  >
  <class name="Department" table="departments">
    <id name="Id" column="Id" type="Int32">
      <generator class="native"/>
    </id>
    <property name="Description" type="String"/>
    <bag name="Employees" lazy="true" inverse="true" cascade="all">
      <key column="IdDepartment"/>
      <one-to-many class="AjTest.Entities.Employee, AjTest.Entities"/>
    </bag>  
  </class>
</hibernate-mapping>

Este es el código generado para este mapeo, Department.generated.cs:

using System;
using System.Collections.Generic;
using Iesi.Collections.Generic;
namespace AjTest.Entities 
{
  public class Department {
    public int Id { get; set; }
    public string Description { get; set; }
    public IList<Employee> Employees { get; set; }
    public Department() 
    {
      this.Employees = new List<Employee>();
    }
  }
}

Veamos el proceso de generación. Este es el contenido de GenerateProject.cmd:

@echo off
set ProjectName=%1%
if "%1%"=="" set ProjectName=AjFirstExample
Bin\AjGenesis.Console.exe Projects\%ProjectName%\Project.xml Tasks\AddMappings.ajg Tasks\BuildCSharp.ajg
xcopy Libraries\*.* Build\%ProjectName%\CSharp\Src\Libraries /Y /Q

La línea más importante es la que invoca a AjGenesis.Console.exe. El contenido de Project.xml se carga en memoria. La tarea AddMapping.ajg se carga y ejecuta (está escrita en un lenguaje dinámico, afectuosamente llamado AjBasic), y luego, se procesa la tarea BuildCSharp.ajg. Veamos el código de AddMapping.ajg:

' Add mappings from directory if not specified in Project model
Include("Utilities/Utilities.tpl")
if not Project.Mappings then
  Project.Mappings = CreateList()
  
  di = new System.IO.DirectoryInfo("Projects/${Project.Name}/Mappings")
  
  for each fi in di.GetFiles("*.hbm.xml")
    filename = fi.Name
    Project.Mappings.Add(filename.Substring(0, filename.Length - 8))
  end for
end if

Encuentra y agrega los nombres de los archivos de mapeo contenidos en el directorio de Mapping, dentro del proyecto (noten que se pueden usar clases y objetos del framework .NET). Una tarea más interesante es la GenerateCSharp.ajg. Primero, carga la dll de NHibernate, para usar más adelante su parser de archivos hbm:

include "Utilities/Utilities.tpl"
include "Utilities/FileUtilities.tpl"
include "Utilities/TypeUtilities.tpl"
Include("Utilities/NHibernateUtilities.tpl")
include "Templates/CSharp/UtilitiesCs.tpl"
include "Templates/CSharp/CSharpFunctions.tpl"
AssemblyManager.LoadFrom("Libraries/NHibernate.dll")
parser = new NHibernate.Cfg.MappingSchema.MappingDocumentParser()

Crea objetos dinámicos, donde coloca información de la solución y el proyecto a crear:

if not Project.BuildDir then
  Project.BuildDir = "Build/${Project.Name}/CSharp"
end if
message "Creating Directories..."
FileManager.CreateDirectory(Project.BuildDir)
FileManager.CreateDirectory("${Project.BuildDir}/Sql")
FileManager.CreateDirectory("${Project.BuildDir}/Src")
FileManager.CreateDirectory("${Project.BuildDir}/Src/Libraries")
message "Defining Solution and Projects..."
Project.Solution = CreateObject()
Project.Solution.Guid = "FAE04EC0-301F-11D3-BF4B-00C04F79EFBC"
Project.Solution.Projects = CreateList()
message "Defining Entities Project..."
PrjEntities = CreateObject()
PrjEntities.Includes = CreateList()
PrjEntities.Guid = CreateGuid()
PrjEntities.COMGuid = CreateGuid()
Project.Solution.Projects.Add(PrjEntities)
Project.Entities = CreateList()

Ahora, itera sobre cada archivo hbm, lo lee usando el parser del propio NHibernate, toma información sobre las clases a generar:

for each MappingName in Project.Mappings
  filename = "Projects/${Project.Name}/Mappings/${MappingName}.hbm.xml"
  mapping = parser.Parse(OpenAsStream(filename))
    
  for each hbmclass in mapping.Items where IsType(hbmclass, "HbmClass")
    Entity = CreateObject()
    
    Project.Entities.Add(Entity)
  
    Entity.ClassName = hbmclass.name
    Entity.Namespace = mapping.namespace
    
    ' Namespace as default project name for Entities Project
    if not PrjEntities.Name then
      PrjEntities.Name = mapping.namespace
      PrjEntities.Directory = "${Project.BuildDir}/Src/${PrjEntities.Name}"
      FileManager.CreateDirectory(PrjEntities.Directory)
    end if
    
    Entity.Properties = CreateList()
    
    if hbmclass.Id then
      Property = CreateObject()
      Property.Name = hbmclass.Id.name
      Property.Type = HbmTypeToCSharp(hbmclass.Id.type1, Entity.Namespace)
      Entity.Properties.Add(Property)
    end if
    
    for each item in hbmclass.Items
      if IsType(item, "HbmProperty") then
        Property = CreateObject()
        Property.Name = item.name
        Property.Type = HbmTypeToCSharp(item.type1, Entity.Namespace)
        Entity.Properties.Add(Property)
      end if
      
      if IsType(item, "HbmManyToOne") then
        Property = CreateObject()
        Property.Name = item.name
        Property.Type = HbmTypeToCSharp(item.class, Entity.Namespace)
        Entity.Properties.Add(Property)
      end if
      if IsType(item, "HbmSet") then
        Property = CreateObject()
        Property.Name = item.name
        Property.IsSet = true
        Property.Type = HbmTypeToCSharp(item.Item.class, Entity.Namespace)
        Entity.Properties.Add(Property)
      end if
      if IsType(item, "HbmBag") then
        Property = CreateObject()
        Property.Name = item.name
        Property.IsList = true
        Property.Type = HbmTypeToCSharp(item.Item.class, Entity.Namespace)
        Entity.Properties.Add(Property)
      end if
    end for    
  end for
end for

Pueden extender esta capacidades, procesando más tags (debería escribir un ejemplo usando los tags meta que puede contener el hbm; ya utilitarios de Java, como el venerable XDocLet, usaban esos tags para ayudarse en la generación de código, en Hibernate), y detectar más formas de mapeo de NHibernate. Ahora, pasa a la generación de código:

for each Entity in Project.Entities
  TransformerManager.Transform("Templates/CSharp/Entity.tpl", "${PrjEntities.Directory}/${Entity.ClassName}.generated.cs", Environment)
  PrjEntities.Includes.Add(CreateFileCs("${Entity.ClassName}.generated"))
end for

La tarea genera los archivos .cs, y también crea un archivo de solución y otro de proyecto C#, copiando y embebiendo los archivos de mapeo originales:

for each MappingName in Project.Mappings
  filename = "Projects/${Project.Name}/Mappings/${MappingName}.hbm.xml"
  targetfilename = "${PrjEntities.Directory}/${MappingName}.hbm.xml"
  System.IO.File.Copy(filename, targetfilename, true)
  PrjEntities.Includes.Add(CreateFileType(MappingName,"hbm.xml"))
end for
for each CsProject in Project.Solution.Projects where CsProject.ProjectType<>"Web"
  FileManager.CreateDirectory(CsProject.Directory)
  FileManager.CreateDirectory(CsProject.Directory & "/Properties")
  TransformerManager.Transform("Templates/CSharp/CsProject.tpl", "${CsProject.Directory}/${CsProject.Name}.csproj", Environment)
  TransformerManager.Transform("Templates/CSharp/AssemblyInfoCs.tpl", "${CsProject.Directory}/Properties/AssemblyInfo.cs", Environment)
end for
TransformerManager.Transform("Templates/Solution.tpl", "${Project.BuildDir}/Src/${Project.Name}.sln", Environment)

Esta es la solución generada:

Próximos pasos

Debería trabajar en algunos puntos:

– Generar una solución más completa, como en otros ejemplos de AjGenesis (con un proyecto de infraestructura, un proyecto de Service Layer o similar, una presentación web, etc…).

– Soportar más opciones de mapeo de NHibernate.

– Usar los tags meta.

Por ahora, pueden jugar con este ejemplo. Pueden cambiar los templates para generar más artefactos, por ejemplo, código VB.NET.

Gracias a @fabiomaulo por avisarmr de las capacidades públicas de parser de hbm dentro de NHibernate!

Nos leemos!

Angel “Java” Lopez

http://www.ajlopez.com

http://twitter.com/ajlopez

Leave a Reply

Your email address will not be published. Required fields are marked *