Les bonnes techniques de programmation pour l'ERP Dynamics NAV

| Alain Krikilion | traduit par jpelaho |

Traduction de l'article How to work with record-variables (version 2)? qui présente quelques recettes pour améliorer votre façon d'utiliser les variables de type « record » avec l'environnement de développement C/SIDE.

Commentez

- Titre : Les bonnes techniques de programmation pour l'ERP Dynamics NAV

- Auteurs : Alain Krikilion, traduit par jpelaho

- Parution : 23 janvier 2012

- Mise à jour : 23 février 2012

- Licence : Copyright ® 2009 Alain Krikilion. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- 1. Introduction

- 2. Aperçu de quelques commandes

- 3. Lectures de données sans verrou

- 3-A. Vous devez vérifier l'existence d'au moins un enregistrement dans votre jeu de données, mais vous ne souhaitez pas utiliser les données de ce dernier s'il existe

- 3-B. Vous voulez extraire le premier/dernier enregistrement (s'il existe), mais pas plus

- 3-C. Vous souhaitez extraire toutes les lignes d'un jeu de données dans l'ordre ascendant

- 3-D. Vous souhaitez extraire toutes les lignes d'un jeu de données dans l'ordre descendant

- 4. Opérations de mise à jour d'enregistrements

- 4-A. Vous ne devez modifier qu'un seul enregistrement

- 4-B. Vous devez récupérer un enregistrement et éventuellement le mettre à jour

- 4-C. Vous devez extraire un ensemble d'enregistrements et tous les modifier

- 4-D. Vous devez extraire un ensemble d'enregistrements et en modifier certains

- 5. Autres opérations

- 5-A. Utilisation de l'instruction FILTERGROUPS

- 5-B. Lecture/Insertion/Mise à jour/Suppression d'enregistrements dans une autre société

- 5-C. Vous souhaitez savoir la quantité totale en stock d'un article donné dans un magasin donné

- 5-D. Vous souhaitez calculer la somme du champ « montant » en groupant par « Gen. Bus. Posting Group » (groupe de comptabilisation marché) et « Gen. Prod. Posting Group » (groupe de comptabilisation produit) de la table 15 : « G/L Entry » (écriture comptable).

- 6. Remerciements

1 - Introduction

J'ai remarqué plusieurs fois que la plupart des gens utilisent les instructions d'extraction des données à partir de la base d'une façon peu performante. Ou alors saisissent des instructions non nécessaires qui rendent le code confus.

Ceci m'a poussé à écrire cet article afin d'expliquer à l'aide de quelques exemples comment utiliser (ou pas) ces différentes instructions et à quel moment le faire (ou pas).

La plupart des éléments traités ici supposent que le serveur de base de données utilisé est SQL Server. Mais certains sont aussi valables pour le serveur de base de données natif C/SIDE.

2 - Aperçu de quelques commandes

Les instructions CLEAR, INIT et RESET.

Commençons par présenter les différences entre ces trois instructions.

I. TheTable.RESET : utilisée pour supprimer tous les filtres (inclus les filtres indiqués dans les instructions FILTERGROUP) d'une variable de type record. Cette instruction réinitialise aussi la commande SETCURRENTKEY afin qu'elle utilise la clé primaire comme clé active pour les recherches.

Ne jamais utiliser cette instruction pour initialiser les champs d'une variable de type record. Ceci n'est pas son rôle.

a) TRÈS MAUVAIS : si une variable de type record a des valeurs dans ses champs, l'instruction RESET ne les initialisera pas.

TheTable.RESET;
TheTable."Primary Key" := Some Value;
TheTable."Field N" := Some Value;
TheTable.INSERT(FALSE);

b) CORRECT :

CLEAR(TheTable); // initialise aussi les valeurs des champs constituant la clé primaire
TheTable."Primary Key" := Some Value;
TheTable."Field N" := Some Value;
TheTable.INSERT(FALSE);

II. TheTable.INIT : cette instruction initialise (avec la valeur par défaut) tous les champs d'une variable de type record sauf les champs constituant la clé primaire. Elle n'affecte pas les filtres (FILTERS) et ne modifie pas la clé active (CURRENTKEY). Donc si vous souhaitez ajouter un nouvel enregistrement sans modifier les valeurs des champs constituant la clé primaire, cette instruction convient parfaitement.

III. CLEAR(TheTable) : cette instruction combine l'action d'un RESET et d'un INIT et initialise les valeurs des champs constituant la clé primaire. J'utilise toujours cette instruction pour créer un nouvel enregistrement. Il s'agit de l'unique commande qui réinitialise la commande CHANGECOMPANY.

IV. GET : cette instruction est la meilleure si vous voulez extraire un enregistrement à l'aide de sa clé primaire. Vous pouvez aussi filtrer sur la clé primaire et utiliser l'instruction FINDFIRST. La commande SELECT envoyée au serveur SQL est la même mais l'instruction GET nécessite moins de code et est plus facile à lire. Aucune commande RESET, SETCURRENTKEY, SETRANGE n'est nécessaire, ces dernières instructions n'influencent pas le fonctionnement de cette commande.

a) MAUVAIS (à cause de la confusion créée) :

TheTable.RESET;
TheTable.SETCURRENTKEY(...)
TheTable.SETRANGE/SETFILTER
TheTable.GET(...);

b) CORRECT :

TheTable.GET(...);

Si vous souhaitez effectuer une commande GET sans générer une erreur même si l'enregistrement recherché n'existe pas, vous pouvez procéder de cette manière :

Version 1

CLEAR(TheTable);
IF TheTable.GET(....) THEN ;

Version 2

IF NOT TheTable.GET(....) THEN
CLEAR(TheTable);

Ces deux versions renverront un enregistrement avec des valeurs par défaut si elles ne trouvent pas l'enregistrement recherché. Aucune valeur de champ non enregistrée ne sera conservée sur la variable après l'exécution du code. Personnellement, je préfère la première instruction : j'initialise d'abord la variable, ensuite j'extrais l'enregistrement (s'il existe).

Les prochaines commandes nécessitent d'appliquer des filtres sur la variable de type record. Avant de les détailler, je voudrais commencer par vous donner une bonne technique de programmation qui s'applique à celles-ci : pour réaliser un code facilement maintenable, il est important de placer les instructions appliquant des filtres ensemble et après la commande RESET. Ceci vous permet de ne pas vous soucier si certains filtres ont été appliqués avant et s'ils doivent être supprimés ou pas. J'utilise toujours ces instructions ensemble, toute personne lisant mon code saura ainsi que je n'ai pas oublié d'appeler la commande SETCURRENTKEY.

TheTable.RESET;
TheTable.SETCURRENTKEY(...); //Même si vous allez utiliser la clé primaire comme clé active
TheTable.SETRANGE(...)ou TheTable.SETFILTER(....)

Quelques remarques :

(a) FIND,FINDSET: ces commandes ouvrent un curseur SQL Server. Utiliser un curseur est très coûteux en termes de performances, il vaut mieux toujours les éviter si cela est possible.

(b) Les exemples suivants présentent les commandes à utiliser si vous souhaitez extraire les données sans verrouiller les enregistrements. Ensuite je présenterai des exemples avec utilisation des verrous.

(c) FINDSET extrait les N premiers enregistrements d'une table en une passe. N est défini dans le champ « Jeu d'enregistrements » des propriétés de la base de données en suivant le menu : Fichier -> Base de données -> Modifier et en cliquant sur l'onglet « Avancé ».

V. FIND(‘-‘) : cette instruction ne doit plus être utilisée (sauf dans certaines situations que j'expliquerai plus tard), car il existe d'autres instructions plus optimisées comme FINDFIRST et FINDSET.

VI. FIND(‘+') : cette instruction ne doit plus être utilisée (sauf dans certaines situations que j'expliquerai plus tard), car il existe d'autres instructions plus optimisées comme FINDLAST.

VII. FIND ('=') : cette instruction me semble un peu bizarre. Elle agit la plupart du temps comme la commande GET afin d'extraire un enregistrement à l'aide de sa clé primaire. On l'appelle après le remplissage des valeurs des champs constituant la clé primaire. Tous les filtres appliqués sur la variable de type record sont pris en compte. Cette commande est utile si la variable Record contient déjà l'enregistrement que l'on recherche et on voudrait juste mettre à jour ses différents champs.

VIII. FIND ('>') / FIND('<') : cette commande s'appuie sur les valeurs des champs constituant la clé primaire pour extraire l'enregistrement suivant ou précédent. Tous les filtres appliqués sur la variable de type record sont pris en compte. Il vaut mieux utiliser l'instruction NEXT ou NEXT(-1).

IX. FIND ('=<>') : cette commande est la moins performante de toutes. Elle commence par exécuter l'instruction FIND(‘=') pour extraire l'enregistrement recherché, si ce dernier n'est pas trouvé, la commande FIND(‘<') est appelée, si rien n'est toujours trouvé, la commande FIND(‘>') est enfin appelée. À éviter absolument.

X. FINDFIRST : ceci est la commande à utiliser si on ne souhaite extraire que le premier enregistrement d'un jeu de données. Par contre, ne l'utilisez jamais si vous souhaitez parcourir les lignes du jeu de résultats obtenu.

XI. FINDLAST : ceci est la commande à utiliser si on ne souhaite extraire que le dernier enregistrement d'un jeu de données. Par contre, ne l'utilisez jamais si vous souhaitez parcourir les lignes du jeu de résultats obtenu.

XII. FINDSET : ceci est la commande à utiliser si vous souhaitez extraire un ensemble de lignes d'une table donnée. Elle récupère les N premières lignes en une seule passe. Si le nombre de lignes dans la table est plus petit que N, la commande va extraire toutes les lignes de la table. N est défini dans le champ « Jeu d'enregistrements » des propriétés de la base de données en suivant le menu : Fichier -> Base de données -> Modifier et en cliquant sur l'onglet « Avancé ».

XIII. ISEMPTY : ceci est la commande à utiliser si vous souhaitez vérifier si votre jeu de données comporte au moins une ligne ou pas.

XIV. COUNT : cette commande est utilisée si vous souhaitez avoir le nombre précis de lignes de votre jeu de données. Elle est plus lourde en termes de performances que la commande COUNTAPPROX.

XV. COUNTAPPROX : cette commande est à utiliser si vous souhaitez avoir un nombre approximatif de lignes de votre jeu de données. Elle peut servir pour mettre à jour le contrôle ProgressBar. Elle est moins gourmande en ressources que la commande COUNT.

3 - Lectures de données sans verrou

3-A - Vous devez vérifier l'existence d'au moins un enregistrement dans votre jeu de données, mais vous ne souhaitez pas utiliser les données de ce dernier s'il existe

a) TRÈS MAUVAIS: parce que la commande compte toutes les lignes.

IF TheTable.COUNT > 0 THEN ... //ou COUNTAPPROX

b) MAUVAIS : parce que la commande retourne un ensemble de lignes à destination du poste client.

IF TheTable.FINDSET THEN ...

c) (MOINS) MAUVAIS: jusqu'à l'apparition de certaines versions (je ne me souviens plus lesquelles), vous deviez procéder uniquement ainsi. Un curseur est ouvert au niveau de SQL Server.

IF TheTable.FIND('-') THEN ... // or FIND('+')

d) TOUJOURS) MAUVAIS : la commande ne fait pas appel à un curseur mais retourne toujours le premier enregistrement (s'il en existe au moins un).

IF TheTable.FINDFIRST THEN ... // or FINDLAST

e) CORRECT : la commande ne renverra aucune ligne. Elle retourne une valeur booléenne indiquant s'il existe une ligne ou pas dans le jeu de données.

IF NOT TheTable.ISEMPTY THEN ...

3-B - Vous voulez extraire le premier/dernier enregistrement (s'il existe), mais pas plus

a) TRÈS MAUVAIS: la commande retourne un ensemble de lignes.

If TheTable.FINDSET THEN ...
// Pour avoir le dernier enregistrement, vous auriez besoin d'appeler
//l'instruction ASCENDING(FALSE) avant d'exécuter la commande FINDSET
//(À voir plus bas dans ce document car c'est encore plus mauvais que « très mauvais »)

b) MAUVAIS : ceci ouvre un curseur au niveau du serveur SQL Server.

If TheTable.FIND('-') THEN ... // ou FIND('+')

c) CORRECT : la commande n'ouvre pas de curseur et retourne un seul enregistrement.

If TheTable.FINDFIRST THEN ... // ou FINDLAST

3-C - Vous souhaitez extraire toutes les lignes d'un jeu de données dans l'ordre ascendant

a) TRÈS MAUVAIS : l'instruction FINDFIRST n'ouvre pas de curseur et retourne juste le premier enregistrement. La première fois que l'instruction NEXT est appelée, le système n'ayant pas de curseur pour parcourir le jeu de résultat en crée un.

IF TheTable.FINDFIRST THEN
REPEAT
...
UNTIL TheTable.NEXT = 0;

b) MAUVAIS : ceci vous renverra un seul enregistrement à la fois.

IF TheTable.FIND('-') THEN
REPEAT
...
UNTIL TheTable.NEXT = 0;

c) CORRECT : la commande vous renverra les N premiers enregistrements d'un seul coup et après les autres seront extraits un par un.

IF TheTable.FINDSET THEN
REPEAT
...
UNTIL TheTable.NEXT = 0;

3-D - Vous souhaitez extraire toutes les lignes d'un jeu de données dans l'ordre descendant

a) TRÈS MAUVAIS : l'instruction FINDFIRST n'ouvre pas de curseur et retourne juste le premier enregistrement. La première fois que l'instruction NEXT est appelée, le système n'ayant pas de curseur pour parcourir le jeu de résultat en crée un.

TheTable.ASCENDING(FALSE);
IF TheTable.FINDSET THEN
REPEAT
...
UNTIL TheTable.NEXT = 0;

b) CORRECT :

utilisez cette version si possible, elle emploie l'instruction FINDSET pour extraire plus rapidement les lignes et les enregistre dans une table temporaire. Ensuite elle parcourt la table temporaire dans un ordre descendant. Ceci peut être utile si le nombre de lignes est inférieur à N (le nombre de lignes retourné par la commande FINDSET). Cette commande ne doit pas être utilisée s'il y a trop d'enregistrements dans le jeu de données car ils seront tous enregistrés dans la table temporaire et donc sur le poste client.

IF TheTable.FINDSET THEN
REPEAT
//Ajout de lignes dans la table temporaire
tmpTheTable := TheTable;
tmpTheTable.INSERT(FALSE);
UNTIL TheTable.NEXT = 0;

//Parcours de la table temporaire
tmpTheTable.RESET;
tmpTheTable.ASCENDING(FALSE);
IF tmpTheTable.FIND('-') THEN
REPEAT
...
UNTIL tmpTheTable.NEXT = 0;

c) CORRECT :

TheTable.ASCENDING(FALSE);
IF TheTable.FIND('-') THEN
REPEAT
...
UNTIL TheTable.NEXT= 0;

4 - Opérations de mise à jour d'enregistrements

Pour commencer, quelques conseils sur la façon de mettre à jour les données dans la base.

1) Créez des transactions le plus tard possible dans le code (commencez par lire toutes les données nécessaires sans appliquer de verrou et ensuite effectuez les opérations de « mise à jour »).

2) Appliquez des verrous sur le moins d'enregistrements possible (si vous voulez changer une ligne dans une table qui en contient 10 millions, pourquoi verrouiller toute la table alors que vous pouvez verrouiller seulement un seul enregistrement ?).

3) Le code contenu dans vos transactions doit être le plus rapide possible (il est par exemple très mauvais de mettre une instruction SLEEP dans une transaction).

4) Le code contenu dans vos transactions doit être le plus court possible (OK, mais pas trop petit : si vous souhaitez mettre à jour 1000 lignes dans une table, n'appelez pas la commande COMMIT à chaque mise à jour).

5) Effectuez vos transactions de sorte que chacune d'elles contienne, le cas échéant et en un seul bloc, toutes les instructions de mise à jour prévues, sinon aucune.

LOCKTABLE(ou FINDSET(TRUE...)) : cette commande fonctionne différemment avec le serveur de base de données natif C/SIDE. Dans ce cas, toute la table est verrouillée. Avec le serveur SQL, à partir de ce point du code, tous les enregistrements extraits de la table seront verrouillés. Mais les enregistrements des autres tables ne le seront pas. Il est cependant possible que certains autres enregistrements soient verrouillés à cause du mécanisme de verrouillage de SQL Server sur lequel nous n'avons pas vraiment de contrôle (au moins à partir du langage C/AL).

4-A - Vous ne devez modifier qu'un seul enregistrement

a) MAUVAIS: cette instruction va générer deux commandes SELECT au niveau de Serveur SQL. La première n'applique pas de verrou et est exécutée pour récupérer l'enregistrement recherché (instruction GET de la première ligne). La seconde applique un verrou exclusif et est exécutée pour la mise à jour de la ligne (instruction MODIFY). Ensuite la commande UPDATE est appelée. La deuxième commande SELECT est nécessaire pour verrouiller l'enregistrement, afin de s'assurer qu'aucune mise à jour ne peut se faire entre l'extraction de l'enregistrement (instruction GET) et sa mise à jour (instruction MODIFY).

//Vous n'avez pas de LOCKTABLE ici (un LOCKTABLE sur une autre table ne
//verrouillera pas la table courante)
TheTable.GET(...); // ou FINDFIRST,FINDLAST
TheTable."Some Field" := 'Some Value';
TheTable.MODIFY(FALSE);

b) CORRECT : l'instruction GET va générer une commande SELECT avec un verrou exclusif. L'instruction MODIFY va générer une commande UPDATE.

TheTable.LOCKTABLE;
TheTable.GET(...); // ou FINDFIRST,FINDLAST
TheTable."Some Field" := 'Some Value';
TheTable.MODIFY(FALSE);

4-B - Vous devez récupérer un enregistrement et éventuellement le mettre à jour

a) CORRECT : si les chances de mise à jour sont infimes, évitez de poser un verrou non nécessaire.

TheTable.GET(...); // ou FINDFIRST,FINDLAST
IF (to be changed) THEN BEGIN
TheTable."Some Field" := 'Some Value';
TheTable.MODIFY(FALSE);
END;

b) CORRECT : si les chances de mise à jour sont grandes, ceci permet d'éviter une deuxième commande SELECT.

TheTable.LOCKTABLE;
TheTable.GET(...); // ou also FINDFIRST,FINDLAST
IF (to be changed) THEN BEGIN
TheTable."Some Field" := 'Some Value';
TheTable.MODIFY(FALSE);
END;

4-C - Vous devez extraire un ensemble d'enregistrements et tous les modifier

a) TRÈS MAUVAIS: déjà vous devez utiliser un FINDSET (lire l'explication plus haut). Vous utilisez la même variable de type record pour lire et mettre à jour les données. Certaines lignes peuvent être omises, vous pouvez modifier des lignes plus d'une fois, ou même réaliser une boucle infinie. De plus vous n'avez pas verrouillé la table.

IF TheTable.FINDFIRST THEN // ou aussi FIND('-')
REPEAT
TheTable."Some Field" := 'Some Value';
TheTable.MODIFY(FALSE);
UNTIL TheTable.NEXT= 0;

b) MAUVAIS: vous n'avez pas verrouillé la table, une commande SELECT supplémentaire sera donc créée par l'instruction MODIFY.

IF TheTable.FINDSET THEN
REPEAT
TheTable2 := TheTable;
TheTable2."Some Field" := 'Some Value';
TheTable2.MODIFY(FALSE);
UNTIL TheTable.NEXT = 0;

c) CORRECT: vous appliquez un verrou sur la table à l'aide du premier paramètre à TRUE de la commande FINSET. Le deuxième paramètre (TRUE) n'est important que dans certaines circonstances mais je vous conseille de toujours l'utiliser.

IF TheTable.FINDSET(TRUE,TRUE) THEN
REPEAT
TheTable2 := TheTable;
TheTable2."Some Field" := 'Some Value';
TheTable2.MODIFY(FALSE);
UNTIL TheTable.NEXT= 0;

4-D - Vous devez extraire un ensemble d'enregistrements et en modifier certains

Les méthodes « TRÈS MAUVAIS » et « MAUVAIS » présentées précédemment s'appliquent aussi ici.

a) MAUVAIS: cette méthode était correcte pour le point précédent mais ne l'est plus ici parce qu'elle verrouille tous les enregistrements lus. Les verrous sont gourmands en termes d'utilisation de la mémoire et aussi en termes de performances. Ceci parce que les autres utilisateurs seront bloqués même s'ils veulent modifier d'autres enregistrements. C'est aussi une cause d'importants blocages.

IF TheTable.FINDSET(TRUE,TRUE) THEN
REPEAT
TheTable2 := TheTable;
TheTable2."Some Field" := 'Some Value';
TheTable2.MODIFY(FALSE);
UNTIL TheTable.NEXT= 0;

b) CORRECT: vous lisez d'abord les lignes et celles qui seront modifiées sont copiées dans une table temporaire sans l'application d'un quelconque verrou.

// Il n'y a pas de verrou sur la table "TheTable" !
IF TheTable.FINDSET THEN
REPEAT
// copie des données dans la table temporaire
IF (to be changed) THEN BEGIN
tmpTheTable := TheTable;
tmpTheTable.INSERT(FALSE);
END;
UNTIL TheTable.NEXT= 0;

//Appel de l'instruction LOCKTABLE. Ceci garantit que tous les enregistrements lus seront verrouillés
CLEAR(TheTable);
TheTable.LOCKTABLE;

Il existe deux méthodes pour parcourir les lignes.

Méthode 1

//Parcours de la table temporaire, le code utilise la version des enregistrements
//de la table temporaire pour mettre à jour les enregistrements correspondants
//de la table. Si un enregistrement est modifié entre-temps, une erreur est générée.
//Remarque : l'instruction MODIFY génère une commande SELECT avec un verrou exclusif
//avant l'instruction UPDATE. Ceci est nécessaire car Navision doit vérifier si la
//ligne n'a pas changé de version (voir le champ RowVersion)

tmpTheTable.RESET;
IF tmpTheTable.FINDSET THEN
REPEAT
TheTable := tmpTheTable;
TheTable."Some Field" := 'Some Value';
TheTable.MODIFY(FALSE);
UNTIL tmpTheTable.NEXT = 0;

Méthode 2

//Parcours de la table temporaire. Ce code extrait toujours des enregistrements en appliquant
//un verrou exclusif et ensuite effectue des mises à jour. Si l'enregistrement est modifié
//entre sa lecture et notre mise à jour, une erreur ne sera pas générée dans le MODIFY parce
//que la version de l'enregistrement extrait était la plus récente.
tmpTheTable.RESET;
IF tmpTheTable.FINDSET THEN
REPEAT
TheTable := tmpTheTable;
TheTable.FIND('=');

//Ou TheTable.GET(primary key). Les deux commandes sont possibles mais vous devez
//vérifier les filtres présents dans la table TheTable. La commande CLEAR(TheTable)
//appelée avant supprime tous ces filtres éventuels

TheTable."Some Field" := 'Some Value';
TheTable.MODIFY(FALSE);
UNTIL tmpTheTable.NEXT = 0;

5 - Autres opérations

5-A - Utilisation de l'instruction FILTERGROUPS

Cette commande permet d'appliquer plusieurs filtres sur le même champ et sert aussi à masquer certains filtres aux utilisateurs.

Commencez par utiliser les FILTERGROUPS de niveaux supérieurs ou égaux à 10. Les FILTERGROUPS de niveaux 0 à 6 sont réservés (voir la documentation de référence C/SIDE).

Les différents filtres appliqués sur le même champ se comportent comme si on les avait reliés avec l'opérateur logique « ET ».

Cas 1: vous voulez éviter qu'un utilisateur ne change un filtre.

TheTable.RESET;
TheTable.SETCURRENTKEY(....);
TheTable.FILTERGROUP(10); // application d'un filtergroup inaccessible pour l'utilisateur
TheTable.SETRANGE(....);
TheTable.FILTERGROUP(0); //application du filtergroup par défaut. L'utilisateur peut
//modifier ou supprimer ce filtre
FORM.RUNMODAL(0,TheTable); // L'utilisateur ne pourra pas mettre à jour les filtres

Cas 2 : vous devez appliquer plusieurs filtres au champ. Exemple : vous voulez extraire tous les enregistrements avec un certain champ commençant par ‘A', se terminant par un ‘B' et contenant un ‘C'.

Bien entendu, vous pouvez créer ce filtre avec une commande SETFILTER, mais cette dernière ne peut pas s'appliquer avec des requêtes plus complexes.

TheTable.RESET;
TheTable.SETCURRENTKEY(...);
TheTable.SETFILTER("The Field",'A*C*B');
IF TheTable.FINDSET THEN

Même exemple avec un FILTERGROUP

TheTable.RESET;
TheTable.SETCURRENTKEY(...);
TheTable.FILTERGROUP(10);
TheTable.SETFILTER("The Field",'A*');
TheTable.FILTERGROUP(11);
TheTable.SETFILTER("The Field",'*C*');
TheTable.FILTERGROUP(12);
TheTable.SETFILTER("The Field",'*B');
TheTable.FILTERGROUP(0);
IF TheTable.FINDSET THEN

5-B - Lecture/Insertion/Mise à jour/Suppression d'enregistrements dans une autre société

Notez bien que seule la table concernée par la mise à jour sera dans une société différente, toutes les autres variables de type record présentes dans la définition de cette table ou alors à partir de la propriété TABLERELATION d'un de ses champs seront dans la société courante.

a) (TRÈS) MAUVAIS : vous ne devez jamais utiliser les triggers si vous voulez mettre à jour les données d'une table présente dans une autre société. Tout le code présent dans ces triggers s'exécutera dans la société courante.

CLEAR(TheTable);
TheTable.CHANGECOMPANY('Some Other Company');
TheTable.VALIDATE("Field 1",'Some Value');
TheTable.VALIDATE("Field 2",'Some Value');
TheTable.VALIDATE("Field 3",'Some Value');
TheTable.INSERT(TRUE);

b) CORRECT: pour insérer un enregistrement dans une autre société (MODIFY et DELETE fonctionnent de la même façon).

CLEAR(TheTable);
TheTable.CHANGECOMPANY('Some Other Company');
TheTable."Field 1" := 'Some Value';
TheTable."Field 2" := 'Some Value';
TheTable."Field 3" := 'Some Value';
TheTable.INSERT(FALSE);

c) CORRECT: pour insérer un enregistrement dans une autre société (MODIFY et DELETE fonctionnent de la même façon).

CLEAR(TheTable);
TheTable.CHANGECOMPANY('Some Other Company');
TheTable.GET(....);

d) CORRECT: lire/supprimer et mettre à jour un ou plusieurs enregistrements dans une autre société.

CLEAR(TheTable);
TheTable.CHANGECOMPANY('Some Other Company');
TheTable.SETRANGE("Field 1",'Some Value');
TheTable.FINDSET;
REPEAT
...
UNTIL TheTable.NEXT= 0;

//Ou

TheTable.DELETEALL(FALSE); // Sans triggers!!!!!

//Ou

TheTable.MODIFYALL(FALSE); // Sans triggers!!!!!

5-C - Vous souhaitez savoir la quantité totale en stock d'un article donné dans un magasin donné

Il existe deux façons de le faire, chacune des méthodes ayant ses avantages et ses inconvénients.

a) CORRECT : avantages : vous n'avez pas besoin de préciser l'index à utiliser pour effectuer la requête. Inconvénients : vous devez assigner une valeur à certains champs ou effectuer un GET et vous devez définir une valeur pour les FLOWFILTERS.

CLEAR(recItem);
recItem."No." := '1000'; // ou recItem.GET('1000');
recItem.SETRANGE("Location Filter",'BLU');
recItem.CALCFIELDS(Inventory);
MESSAGE('%1',recItem.Inventory);

b) CORRECT : avantages : vous n'effectuez que des filtres et pas d'affectations de valeurs. Inconvénients : vous devez indiquer la clé secondaire à utiliser par le champ de calcul.

recItemLedgerEntry.RESET;
recItemLedgerEntry.SETCURRENTKEY("Item no.","Location Code");
recItemLedgerEntry.SETRANGE("Item No."','1000');
recItemLedgerEntry.SETRANGE("Location Code",'BLU');
recItemLedgerEntry.CALCSUMS(Inventory);
MESSAGE('%1',recItemLedgerEntry.Quantity);

Si vous devez utiliser deux ou plusieurs variables de type record d'une même table, vous pouvez créer un tableau au lieu de créer deux variables. Les deux éléments obtenus se comporteront de façon indépendante comme si vous aviez créé deux variables.

recRecordVar1.RESET; => recRecordVar[1].RESET;
CLEAR(recRecordVar1); => CLEAR(recRecordVar[1]);
recRecordVar1.SETRANGE(...) => recRecordVar[1].SETRANGE(...)

Vous pouvez aussi utiliser une variable pour indiquer un élément du tableau.

Exemple:

intSomeInteger := 1;
recRecordVar1.RESET; => recRecordVar[ intSomeInteger ].RESET;

La technique du tableau peut aussi être utilisée pour des variables temporaires de type record. Les éléments du tableau fonctionneront toujours de façon indépendante comme pour une table normale. Mais il n'y a qu'une seule et unique table temporaire : ceci veut dire que si vous ajoutez un enregistrement à partir de element[1], ce dernier sera accessible par element[2]. Un exemple (ceci est le premier code avec la table temporaire donc elle ne contient aucun enregistrement).

CLEAR(recRecord[1]);
recRecord[1]."No." := '1');
recRecord[1].INSERT(FALSE);
recRecord[2].RESET;
recRecord[2].FINDFIRST;
MESSAGE('%1',recRecord[2]."No."); // la valeur de l'élément  1 sera affichée

5-D - Vous souhaitez calculer la somme du champ « montant » en groupant par « Gen. Bus. Posting Group » (groupe de comptabilisation marché) et « Gen. Prod. Posting Group » (groupe de comptabilisation produit) de la table 15 : « G/L Entry » (écriture comptable).

Avec une requête SQL vous aurez ceci :

SELECT  "Gen_ Bus_ Posting Group","Gen_ Prod_ Posting Group",SUM(Amount)
FROM dbo."CRONUS International Ltd_$G_L Entry"
GROUP  BY"Gen_ Bus_ Posting Group","Gen_ Prod_ Posting Group"
ORDER  BY"Gen_ Bus_ Posting Group","Gen_ Prod_ Posting Group"

Mais avec le langage C/AL, on ne peut pas écrire du code SQL (à moins d'utiliser ADO), donc il faut trouver une autre façon de procéder. Je vous conseille de procéder comme ci-dessous chaque fois que vous devez faire des sommes avec le langage C/AL.

recGLEntry.SETCURRENKEY(...); // Utiliser la clé appropriée pour l'exécution de la requête
recGLEntry.SETRANGE(.....); // appliquer les filtres
IF recGLEntry.FINDSET THEN
REPEAT

//"tmpGLEntry" table temporaire utilisée pour enregistrer les totaux
tmpGLEntry.RESET;

// Utiliser la clé appropriée pour les filtres
IF NOT tmpGLEntry.SETCURRENTKEY("Gen. Bus. Posting Group") THEN;
IF NOT tmpGLEntry.SETCURRENTKEY("Gen. Prod. Posting Group") THEN;

//filtre sur les lignes utilisées pour le regroupement
tmpGLEntry.SETRANGE("Gen. Bus. Posting Group",recGLEntry."Gen. Bus.Posting Group");
tmpGLEntry.SETRANGE("Gen. Prod. Posting Group",recGLEntry."Gen. Prod.Posting Group");

IF NOT tmpGLEntry.FINDFIRST THEN BEGIN
// L'enregistrement n'existe pas il faut le créer
// N'oubliez pas que vous devez utiliser une clé unique pour ajouter un enregistrement.
// Chaque "recGLEntry" est unique donc on peut insérer
// "recGLEntry" dans la table temporaire.
tmpGLEntry := recGLEntry;
tmpGLEntry.insert(FALSE);
END ELSE BEGIN
//l'enregistrement trouvé correspond à la combinaison voulue donc
//on ajoute son montant dans le champ de totalisation
tmpGLEntry.Amount+= recGLEntry.Amount;
tmpGLEntry.MODIFY(FALSE);
END;

UNTIL recGLEntry.NEXT= 0;

//Dans la table temporaire, j'ai les lignes contenant les totaux suivant les combinaisons
//que je voulais. Bien sûr si on veut ordonner la table, il faut disposer de la clé
//appropriée et avec Sql Server on peut mettre la propriété MaintainSQLIndex de cette clé à FALSE.
//Une autre façon de faire est de créer une nouvelle table avec tous les champs
//et clés nécessaires et l'utiliser comme une table temporaire.
//Vous n'avez pas besoin de licence pour une nouvelle table si vous ne
//l'utilisez que comme table temporaire

//Maintenant vous pouvez utiliser vos totaux dans la table temporaire
tmpGLEntry.RESET;
FORM.RUNMODAL(0,tmpGLEntry);

6 - Remerciements

Je tiens ici à remercier Alain Krikilion pour son aimable autorisation de traduire cet article.
Merci à djug et gorgonite pour leurs conseils.
Merci à ClaudeLELOUP et jacques_jean pour leur relecture attentive.