Article
· Sept 15, 2023 6m de lecture

Comment exécuter du code au moment de la compilation avec les macros.

Salut les développeurs,

Dans cet article, je vais vous montrer comment exécuter du code au moment de la compilation avec les macros ObjectScript.

Voici un cas d'utilisation qui m'a récemment amené à utiliser cette fonctionnalité :

Dans le cadre d'une application médicale développée depuis plus de 20 ans, nous avons un grand nombre de paramètres. Bien que nous disposions de procédures pour documenter ces paramètres, il peut être utile d'avoir une vue rapide sur les paramètres réellement utilisés par le code de l'application.

Pour obtenir cette vue, nous pourrions effectuer une recherche dans le code avec des expressions régulières. Cependant, il serait plus pratique de disposer de tous les paramètres utilisés directement dans une table une fois la compilation terminée.

Commençons par créer une simple classe persistante pour stocker les paramètres:

Class dc.MyAppParam Extends %Persistent
{

Property Label As %String(MAXLEN = "");
Property Key As %String(MAXLEN = 256) [ Required ];
Property Value As %String(MAXLEN = "");
Index UniqueI On Key [ Unique ];
ClassMethod Get(
    Key As %String,
    DefaultValue As %String = "") As %String
{
    Quit:'..UniqueIExists(Key, .id) DefaultValue
    Quit ..ValueGetStored(id)
}

ClassMethod Set(
    Key As %String,
    Value As %String,
    Label As %String = "") As %Status
{
    If '..UniqueIExists(Key, .id) {
        Set param = ..%New(), param.Key = Key, param.Label = Label, param.Value = Value
        Quit param.%Save()
    }

    Set param = ..%OpenId(id), param.Value = Value
    Set:Label'="" param.Label = Label

    Quit param.%Save()
}
}

 

Définissons maintenant la macro, dans un cas d'utilisation classique, nous aurions pu faire:

#Def1Arg    GetAppParam(%args)  ##class(dc.MyAppParam).Get(%args)

Dans notre cas, nous ne voulons pas juste remplacer $$$GetAppParam par l'appel de méthode, mais aussi exécuter du code, nous utiliserons donc:

ROUTINE MyAppParam [Type=INC]

#Def1Arg    GetAppParam(%args)  ##expression($$Get^OnCompileParam(%args))

Comme on peut le voir la macro fait appel à Get^OnCompileParam, implémentons maintenant cette routine:

ROUTINE OnCompileParam

Get(Key,DefaultValue="")
    New (Key,DefaultValue)
    
    Set expression = "##expression(""##class(dc.MyAppParam).Get(""""%1"""",""""%1"""")"")"
    For replaceStr = Key, DefaultValue Set expression = $Replace(expression, "%1", replaceStr, , 1)
    Set routineDBRef = "^^"_$$GetDBRoutines()
    
    Lock +^[routineDBRef]MyAppParam(Key)
    If '$Data(^[routineDBRef]MyAppParam(Key), data) {
        Set ^[routineDBRef]MyAppParam(Key) = $ListBuild(Key,DefaultValue,1) 
        Lock -^[routineDBRef]MyAppParam(Key) 
        Quit expression
    }

    Set $List(data,2) = DefaultValue, $List(data,3) = 1 + $ListGet(data,3)
    Set ^[routineDBRef]MyAppParam(Key) = data
    Lock -^[routineDBRef]MyAppParam(Key)
    Quit expression

GetDBRoutines()
    New
    New $NAMESPACE Set ns = $Namespace, $Namespace = "%SYS"
    Do ##class(Config.Namespaces).Get(ns,.pNs), ##class(Config.Databases).Get(pNs("Routines"),.pDb)
    Set $Namespace = ns
    Quit pDb("Directory")

La routine effectue un set de MyAppParam(Key) dans la base de code pour chaque utilisation de la macro $$GetAppParam au moment de la compilation et retourne également une ##expression avec le code à exécuter au runtime.

Exemple d'utilisation: 

Include MyAppParam

Class dc.ParamUsage
{

ClassMethod TestGetAppParam() As %String
{
    Set x = $$$GetAppParam("test.key", "DefaultValue")
    Set x = $$$GetAppParam("test.key2", "DefaultValue")
}

}

Après compilation, nous retrouvons bien le remplacement des $$$GetAppParam par les appels à la méthode Get : 

ROUTINE dc.ParamUsage.1 [Type=INT,Generated]
 ;dc.ParamUsage.1
 ;Generated for class dc.ParamUsage.  Do NOT edit. 08/07/2023 10:33:17PM
 ;;59356D64;dc.ParamUsage
 ;
TestGetAppParam() methodimpl {
    Set x = ##class(dc.MyAppParam).Get("test.key","DefaultValue")
    Set x = ##class(dc.MyAppParam).Get("test.key2","DefaultValue") }

Le set est également bien présent dans la base de code:

IRISAPP>zw ^["^^"_$$GetDBRoutines^OnCompileParam()]MyAppParam
^["^^/usr/irissys/mgr/irisapp/data/"]MyAppParam("test.key")=$lb("test.key","DefaultValue",1)
^["^^/usr/irissys/mgr/irisapp/data/"]MyAppParam("test.key2")=$lb("test.key2","DefaultValue",1)

Effectuez maintenant au moins deux "Set" paramètres dans la table dc.MyAppParam, cela sera utile pour la suite:

Do ##class(dc.MyAppParam).Set("test.key", "value parameter", "For testing $$$GetAppParam")
Do ##class(dc.MyAppParam).Set("test.unused", "an unused param", "For testing unused param")

Au fil du temps, les développements, les améliorations, etc., peuvent entraîner des paramètres en base de données qui ne sont plus utilisés dans le code de l'application.

Avec le système mis en place, il est assez facile de vérifier si un paramètre en base de données est encore utilisé dans le code. Il suffirait par exemple d'ajouter une propriété booléenne calculée dans la classe dc.MyAppParam :

Property ExistsInCode As %Boolean [ Calculated, SqlComputeCode = { Set {*} = ''$Data(^["^^"_$$GetDBRoutines^OnCompileParam()]MyAppParam({Key}))}, SqlComputed, Transient ];
Method ExistsInCodeGet() As %Boolean [ CodeMode = expression ]
{
''$Data(^["^^"_$$GetDBRoutines^OnCompileParam()]MyAppParam(..Key))
}

 

SELECT
ID, Key, Label, Value, ExistsInCode
FROM dc.MyAppParam



Nous pouvons aussi faire l'inverse, lister tous les paramètres utilisés dans le code et vérifier si ils sont définis dans la table de paramétrage.  Cela va toutefois nécessité l'écriture d'une custom class query, mais rien de très compliqué:

Query CompiledParameter() As %Query(ROWSPEC = "Key:%String,DefaultValue:%String,ExistsInParamTable:%Boolean") [ SqlProc ]
{
}

ClassMethod CompiledParameterExecute(
	ByRef qHandle As %Binary,
	Filter As %DynamicObject) As %Status
{
    Set qHandle("Key") = "", qHandle("dbref") = "^^"_$$GetDBRoutines^OnCompileParam()
    Quit $$$OK
}

ClassMethod CompiledParameterFetch(
	ByRef qHandle As %Binary,
	ByRef Row As %List,
	ByRef AtEnd As %Boolean) As %Status [ PlaceAfter = CompiledParameterExecute ]
{
    Set qHandle("Key") = $Order(^[qHandle("dbref")]MyAppParam(qHandle("Key")), 1, data)

    If qHandle("Key") = "" Set AtEnd = $$$YES, Row = "" Quit $$$OK
    Set Row = $Lb($Lg(data,1),$Lg(data,2),..UniqueIExists(qHandle("Key"))), AtEnd = $$$NO
    Quit $$$OK
}

ClassMethod CompiledParameterClose(ByRef qHandle As %Binary) As %Status [ PlaceAfter = CompiledParameterExecute ]
{
	Kill qHandle Quit $$$OK
}
SELECT *
FROM dc.MyAppParam_CompiledParameter()

Et voilà, l'objectif est maintenant atteint!

Toutefois, il existe quelques limitations à cette solution.

Étant donné que le code est exécuté au moment de la compilation, vous ne pouvez utiliser que des constantes string. Il est donc impossible d'utiliser des variables en paramètre de $$$GetAppParam.

La routine OnCompileParam.mac doit être compilée avant les classes (et autres routines) qui l'utilisent.  Selon les cas, il peut être préférable de compiler toutes classes avant les routines et cela peut être problématique ici.  Pour résoudre ce problème, je vous propose une astuce qui consiste à utiliser une projection et une classe en System = 3:

Class dc.Priority [ System = 3 ]
{

Projection compilePriority As dc.Projection;
}

Class dc.Projection Extends %Projection.AbstractProjection [ System = 3 ]
{

ClassMethod CreateProjection(
    classname As %String,
    ByRef parameters As %String,
    modified As %String,
    qstruct) As %Status
{
    Set routine = "OnCompileParam.mac"
    Do:##class(%Library.Routine).Exists(routine) ##class(%Library.Routine).CompileList(routine,"c-d")
	QUIT $$$OK
}

}

Le mot clé System = 3 place la classe en haute priorité pour la compilation et la projection qu'elle contient forcera la compilation de la routine.

Un dernier point important à noter est le nettoyage de la global MyAppParam.

Dans le cas où le build de votre application se fait toujours sur une nouvelle base de code, il n'y a pas de problème. Cependant, si le build se fait sur la même base de code, il est nécessaire de vider la global MyAppParam avant de recompiler.

Pour cela, vous pouvez utiliser simplement la commande Kill :

Kill ^["^^<path_routine_db>"]MyAppParam

Vous pourrez retrouver tout le code cet article ce GitHub repository.

Merci pour votre lecture.
A bientôt!

Discussion (0)1
Connectez-vous ou inscrivez-vous pour continuer