Descubre la nueva Dynamics AX 2012 R3 Entity Store

En prácticamente todas las instalaciones de Microsoft Dynamics AX, en cualquiera de sus versiones, nos vemos en la necesidad de exponer ciertos datos de la aplicación para que otros sistemas puedan utilizarlos. Casi siempre, aunque no únicamente, herramientas de reporting y Business Intelligence. Para conseguirlo diseñamos datawarehouses más o menos completos y procesos ETL para transformar la base de datos transaccional y normalizada de Microsoft Dynamics AX en una fuente de datos más apropiada para la lectura eficiente de la información.

Entity Store - Exposing entities

Para evitarnos esta tarea extra y permitirnos centrar nuestro tiempo en la solución final, se ha publicado una nueva funcionalidad disponible para Microsoft Dynamics AX 2012 R3 llamada Entity Store, que nos va a permitir exponer las entidades de Microsoft Dynamics AX y utilizar todas las novedades del nuevo Microsoft SQL Server 2016 como los nuevos índices columnares en memoria y herramientas diseñadas para optimizar el reporting y permitir una explotación en “casi” tiempo real (near-real time).

Entity Store - Power BI

Este almacén de entidades es una nueva base de datos independiente de la transaccional por lo que, aparte de permitirnos una optimización y escalado independiente, nos va a permitir funcionar siempre con la nueva versión de SQL sin tener que migrar también el servidor transaccional de AX, facilitando la adopción de las últimas novedades. Actualmente es compatible con SQL Server 2014 y 2016 así como Azure SQL.

La herramienta se integra y configura en la aplicación (actualmente como un hotfix independiente, pero formará parte del próximo parche acumulativo para AX 2012 R3) y utiliza el Data Import/Export Framework (DIEF) para coordinar la actualización de las entidades, que pueden configurarse a cierta frecuencia de forma independiente.

Esta es una primera versión para introducir la funcionalidad y proveer las bases de su funcionamiento, pero Microsoft ya ha anunciado que están invirtiendo en este Data Entity por lo que podemos esperar novedades en el futuro, ya que esta promete ser la puerta de integración de nuestros sistemas con el resto de tecnologías de Microsoft referentes al reporting y business intelligence, que están avanzando mucho de la mano de Azure y SQL Server.

Por ejemplo, Microsoft ya nos sugiere que estas entidades están preparadas para trabajar con Azure Machine Learning, Power BI, Azure Data Factory y herramientas clásicas de BI (como data warehouse clásico, data lake para AX, …) y Big Data (Hadoop). Todas estas herramientas que ya están disponibles en Azure (Azure SQL, SQL-DW, Azure Data Factory, HDInsight, Azure Data Lake, Azure Data Lake Analysis Services, y Machine Learning) son parte también del Cortana Intelligence Suite (CIS), por lo que este nuevo almacén de datos consolidado es sólo el principio 😉

Toda la información disponible en el siguiente white paper de Microsoft:

El parche se puede descargar desde LCS buscando “Entity store” o KB3147499.

* Las imágenes de este post están extraídas del blog Dynamics AX Business Intelligence!!

Fixing DB synchronization problems in Microsoft Dynamics AX 2012 [EN]

Sometimes when synchronizing the database in Microsoft Dynamics AX 2012 we get some nonsense error messages. In my example, I’m getting an error stating that it can’t change the data type in a table field. But this field doesn’t exists on this table, same error says that is trying to convert a data field into a field of another table… weird.

ndb-sync-001

This is an indicator that we are facing an ID problem with the table and/or the table fields. Unfortunately, we don’t have the proper tools to diagnose and fix this kind of problems, so we need to use dirty tricks like fixing the conflict directly in the database.

Of course, what I’m describing here is not recommended at all and it comes with a huge risk of loosing data and giving more problems that the ones is trying to fix if you don’t do it with care. Always test this in a dedicated test system and, if possible, try to avoid it.

Continue Reading…

Problemas de sincronización en Microsoft Dynamics AX 2012

A veces al sincronizar la base de datos de Microsoft Dynamics AX 2012 obtenemos errores sin sentido. En mi ejemplo, estoy recibiendo un error indicando que no se puede cambiar el tipo de dato de un campo en una tabla. Pero ese campo ni siquiera existe en esa tabla, el mismo mensaje de error indica que intenta convertir un campo de una tabla en otro campo de otra tabla… absurdo.

ndb-sync-001

Este es un signo claro de que un conflicto de IDs en la tabla o en los campos está ocurriendo. Desafortunadamente no tenemos herramientas apropiada en AX para gestionar estos conflictos, así que voy a recurrir directamente a la base de datos.

Me encanta el olor a UPDATE SQLDICTIONARY por las mañanas

Por supuesto, todo lo que voy a contar no está para nada recomendado y conlleva un grave riesgo de pérdida de datos y de generar más problemas de los que se intenta solucionar con ellos si no se hace con cuidado. Probarlo siempre antes en entorno de test y, si es posible, intenta solucionarlo de otra forma.

Continue Reading…

[HOWTO] Tomar el control de un backup de AX 2012 mediante PowerShell

Hace unos días hacía un comentario en twitter acerca de las posibilidades de PowerShell que me trajo mucho feedback.

Supongo que hay mucho interés en conocer más sobre las bondades de PowerShell aplicadas a Microsoft Dynamics AX, y estoy preparando un artículo largo (probablemente una serie de varios artículos) sobre el tema que estarán terminados en las próximas semanas. PowerShell es una herramienta extremadamente potente y a la vez extraña. Los programadores habitualmente la consideran una herramienta de administración (es una consola) mientras que los sysadmin suelen verla como algo para programadores (es código). Curioso, ¿verdad?

Mientras termino el artículo, y para ir abriendo boca, un problema común en todas las versiones de AX es que, tras restaurar una copia de seguridad de AX de un entorno a otro, hay varias situaciones en las que no podremos acceder al sistema porque nuestro usuario no está dado de alta en esa aplicación. Casos típicos es que la copia es de un cliente lo restauramos en una máquina propia en otro dominio, o que nuestro usuario de desarrollo no está dado de alta en la aplicación de producción.

Sin importar mucho el por qué, la solución siempre ha sido ir a la base de datos manualmente, buscar en la tabla de usuarios y actualizar el SID del usuario administrador por el nuestro; o bien crear manualmente un nuevo usuario en la tabla. Esta es, de hecho, la única solución. Pero no siempre es fácil conocer nuestro SID y esta es una tarea que, por definición, podemos automatizar.

Para eso podemos utilizar un script PowerShell que podrá ser ejecutado cada vez que restauramos un backup, y nos permitirá tomar el control del usuario admin de ese entorno automáticamente (todo este código puede pegarse en un nuevo fichero con extensión .ps1 y utilizarse como un cmdlet):

[CmdletBinding()]
Param
(
[Parameter()]
[string] $dbInstance = "localhost",

[Parameter(Mandatory=$True)]
[string] $dbName,

[Parameter()]
[string] $adDomain = [Environment]::UserDomainName,

[Parameter()]
[string] $adUser = [Environment]::UserName
)

Import-Module sqlps -DisableNameChecking

$ntAccount = New-Object System.Security.Principal.NTAccount($adDomain, $adUser)
$adSID = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier])

$params = "adDomain = $adDomain",
"adUser = $adUser",
"adSID = $adSID"

Invoke-Sqlcmd -Query "
SELECT ID, NAME, SID, NETWORKDOMAIN, NETWORKALIAS
FROM [dbo].[USERINFO]
WHERE ID = 'admin';
"
-ServerInstance "$dbInstance" –Database "$dbName" | Format-Table

Invoke-Sqlcmd -Query "
UPDATE [dbo].[USERINFO]
SET NAME = 'Admin',
SID = '`$(adSID)',
NETWORKDOMAIN = '`$(adDomain)',
NETWORKALIAS = '`$(adUser)'
WHERE ID = 'admin';
"
-ServerInstance "$dbInstance" –Database "$dbName" -Variable $params

Invoke-Sqlcmd -Query "
SELECT ID, NAME, SID, NETWORKDOMAIN, NETWORKALIAS
FROM [dbo].[USERINFO]
WHERE ID = 'admin';
"
-ServerInstance "$dbInstance" –Database "$dbName" | Format-Table

El usuario nos permite pasar por parámetros los datos de nuestro usuario y la base de datos, y también asume valores por defecto. Si se utilizan los valores por defecto, el script utilizará el usuario actual. Después se utiliza el módulo PowerShell de SQL Server para lanzar el update necesario a la tabla, y nos mostrará por consola los datos antes y después de su ejecución para asegurarnos, o por si queremos guardarlo en un fichero de texto, por si acaso 😉

Este es el resultado:

Restore-AXAdminUser

Después de ejecutar el script ya puedo entrar en la aplicación correspondiente porque desde este momento mi usuario esta enlazado al usuario admin de esa aplicación.

Más sobre PowerShell en los próximos días! 🙂

Consultar roles de seguridad desde SQL en AX 2012

No es algo común pero en algún caso nos puede venir bien consultar qué roles de seguridad tiene un usuario consultando la base de datos. Lo ideal sería utilizar servicios web o algo parecido, utilizando un código X++ parecido al siguiente (código extraído del capítulo 10: “Licencia, Configuración y Seguridad” de mi libro):

static void QueryRoles(Args _args)
{
    SecurityRole            securityRole;
    SecurityUserRole        securityUserRole;

    while select securityUserRole
            where securityUserRole.User == 'admin'
        join securityRole
            where securityRole.RecId == securityUserRole.SecurityRole
    {
        info(strFmt("%1", SysLabel::labelId2String(securityRole.Name)));
    }
}

Si insistimos en hacer esta consulta desde SQL Server nos vamos a llevar una sorpresa. La tabla SecurityUserRole que almacena los roles asignados a cada usuario existe en la base de datos transaccional de AX, pero la tabla SecurityRole, que almacena los roles en si, no existe. Sería lógico pensar que, a lo mejor, la tabla existe en la base de datos modelo (_model), pero tampoco la encontramos ahí.

Tras recurrir al truco que comentaba unos cuantos post atrás, nos encontramos con que la tabla está efectivamente en la base de datos modelo, pero abstraída tras un procedimiento almacenado, y tiene esta pinta:

SELECT T1.NAME, T1.ALLOWPASTRECORDS, ...
FROM [XXXX_model].[dbo].SECURITYROLE_INLINEFUNC(N'en_us') T1

Por tanto, jugando un poco con la consulta, podemos obtener el mismo resultado que consultando desde X++:

SecurityUserRole X++

Pero desde SQL Server, y por tanto desde cualquier aplicación externa:

SecurityUserRole SQL

Utilizar con cuidado 😉

 

Microsoft Dynamics AX 2012, tablas TempDB y Formularios

En versiones anteriores disponíamos de diferentes técnicas (no muy intuitivas) para utilizar tablas temporales en formularios. Estas mismas técnicas pueden utilizarse en AX 2012, donde además, tenemos diferentes tipos de estas tablas. En particular me interesan las tablas de tipo TempDB porque son las que mejor rendimiento obtienen, al estar gestionadas por el motor de Microsoft SQL Server. Este es uno de esos post que escribo para tener localizada la información y volver a ella en futuras referencias 🙂

Una versión muy sencilla de uso de estas tablas es similar a como lo hacíamos en la versión 2009, sustituyendo la función setTmpData por linkPhysicalTableInstance. De esta forma, creamos un formulario, añadimos la tabla temporal al origen de datos, etc. y finalmente el método init del formulario queda de esta forma:

public void init()
{
    // ...
   
    super();

    JAEETablaTmp.linkPhysicalTableInstance(JAEETablaTmp::createRecords(desde, hasta));
}

El origen de datos se enlaza con un buffer de la tabla temporal devuelto por la función que inserta los datos. De forma que esos datos que devuelve la función son los que se muestran en el formulario.

Una forma más avanzada de hacerlo, que ofrece más flexibilidad para actualizar los datos posteriormente, es usar una variable de la tabla temporal,que posteriormente podamos manipular. En este caso hay que tener en cuenta el ámbito (scope) de existencia de los datos en la tabla temporal, para lo cual debemos pasar la misma variable a las funciones que manipulen los datos, para no perderlos.

public class FormRun extends ObjectRun
{
    JAEETablaTmp   tmpTable;
}

public void init()
{
    // ...

    super();

    // Hay que pasar el buffer a la función para mantener el ámbito
    JAEETablaTmp::createRecords(tmpTable, desde, hasta);

    // ...

    // Enlazar el buffer con la tabla temporal
    JAEETablaTmp.linkPhysicalTableInstance(tmpTable);
}

Así podremos actualizar los datos posteriormente, en mi caso con la llamada a un botón en el formulario que obtiene parámetros de otros controles, de esta forma:

void clicked()
{
    JAEETablaTmp::createRecords(tmpTable, FromDate.dateValue(), ToDate.dateValue());
    JAEETablaTmp_ds.executeQuery();
}

Destacar cómo la variable temporal declarada en el formulario se pasa a la función que genera los datos para mantener siempre la referencia a la misma variable que está enlazada al origen de datos. La función gestiona los datos en esta misma variable, y la devuelve, de esta manera:

public static client JAEETablaTmp createRecords(
    JAEETablaTmp    _tmpBuffer,
    FromDate        _desde,
    ToDate          _hasta)
{
    // ...

    delete_from _tmpBuffer;

    while select DatePhysical, ItemId, sum(Qty)
        // ...
    {
        _tmpBuffer.clear();
        // ...
        _tmpBuffer.insert();
    }

    return _tmpBuffer;
}

Una particularidad que hay que tener en cuenta es que, en el momento de enlazar la tabla temporal con el origen de datos, la variable buffer no puede estar vacía, o el enlace no se realiza. Ésto no es ningún problema en el primer ejemplo, ya que simplemente no se mostrará ningún dato en el formulario; pero si utilizamos la segunda técnica y el enlace con la variable no se realiza correctamente,  posteriores actualizaciones de la variable no se verán reflejadas en el origen de datos, y tampoco se mostrará ningún error.

Una forma sencilla de evitar esto es insertar un registro vacío en la tabla antes de realizar el enlace con el origen de datos:

public void init()
{
    // ...

    super();

    // Hay que pasar el buffer a la función para mantener el ámbito
    JAEETablaTmp::createRecords(tmpTable, desde, hasta);

    // Si la tabla no tiene registros, la tabla no se enlaza!!
    if (!tmpTable)
        tmpTable.insert(); // IMPORTANTE!

    // Enlazar el buffer con la tabla temporal
    JAEETablaTmp.linkPhysicalTableInstance(tmpTable);
}

Espero que os sirva 😀