Les dangers de la méthode Empty() en réseau

La méthode empty() est souvent utilisée pour vider une table, comme son nom l'indique. On l'utilise également quelquefois pour supprimer un lot d'enregistrements précédemment filtrés ou une partie du modèle relationnel d'une fiche. Prenons l'exemple des tables Cde (Commandes) et CdeLig (Lignes de commandes), la première étant indexée sur le numéro de commande (champ NumCde) et la seconde sur le numéro de commande et sur le code produit (champs NumCde et CodeProduit).

 

1. Vidage complet des lignes de commande:

empty"(:monAlias:CdeLig.Db") Toutes les lignes sont supprimées

 

var
CdeligTC tcursor
  DYARAT DynArray[] AnyType
endvar
if not CdeLigTC.open(":monAlias:CdeLig.db") then
DYARAT["QteCde"] = "< 1 "
CdeLigTC.SetGenFilter(DYARAT)
CdeLigTC.empty()   les lignes dont la quantité est inférieure à 1 sont supprimées

3. Suppression d'un lot d'enregistrements suite à une sélection par setRange

var
CdeligTC tcursor
endvar
if not CdeLigTC.open(":monAlias:CdeLig.db") then
CdeLigTC.SeRange(NumCde.Value,NumCde.Value) ; Le champ NumCde est pris dans la fiche courante
CdeLigTC.empty()   les lignes de la commande visualisée sont supprimées

4. Suppression d'un lot d'enregistrement via un objet visuel de la fiche

On suppose que la fiche a comme table maître la table Cde et en table liée 1-n la table CdeLig.

Un cadre de table appelé CDELIG visualise sur la fiche la liste des lignes par commande.

 

CDELIG.empty() ; vide l'ensemble des lignes de commande

 

Toutes ses solutions marchent très bien lorsque vous testez sur votre poste, mais il faut savoir que quel que soit le contexte, la méthode Empty() pose un verrou si possible Exclusif ou au moins d'écriture sur la table.

 

Il s'en suit que lorsque vous êtes en réseau, dès lors qu'une mise à jour est en cours sur la table LigCom par un autre utilisateur, la méthode Empty va échouer avec une erreur table verrrouilée, même si l'enregistrement verrouillé n'appartient pas au jeu d'enregistrements à supprimer (par setgenfilter ou setrange).

Vérification:

Créer les tables CDE et CDELIG avec quelques enregistrements dans chaque table

Pour tester la méthode empty(), faites une fiche Cde.fsl avec un bouton comprenant dans sa méthode pushbutton l'instruction

if not CDELIG.empty() then errorshow() endif

Test: Vous lancez une autre session Paradox (sur un autre poste ou sur le même poste en indiquant un répertoire privé différent). Ouvrez la table CdeLig et commencer à modifier une ligne de la commande 1. Rapssez dans le Paradox avec la fiche et déclencher le bouton alors que vous êtes sur la commande 2. Le empty ne s'exécute pas et vous avez un message d'erreur vous précisant que la table est verrouillée.

Revenez dans la session ou la table est ouverte. Anuuler votre modification puis reprenez la fiche dans l'autre session et appuyez sur le bouton de suppression. L'empty se fait correctement

Les solutions préconisées

La première solution est de remplacer l'instruction empty() par une boucle de suppression, par exemple, dans le cas 4:

 

var tc tcursor endvar
tc.attach(CDELIG)
tc.edit()
while not tc.eot()
   deleteRecord()
endwhile
tc.close()

L'inconvénient de cette solution est que l'exécution de cette boucle va être lente si vous avez un grand nombre d'enregistrements à supprimer puisqu'on va avoir un verrouillage enregistrement par enregistrement dans tous les cas, et ce même si aucun autre utilisateur n'utilise la table.

 

Une autre solution consiste à tenter un verrouillage de la table cible de la suppression et à n'utiliser la solution précédente que dans le cas où ce verrouillage n'est pas possible. A noter qu'on tente d'abord un verrouillage de la table en exclusif (dans ce cas le vidage sera très rapide), puis un verrouillage en écriture (le vidage sera plus lent), avant de se rabattre sur la suppression enregistrement par enregistrement (solution très lente).

 

Pour cette solution nous créons la méthode suivante (que nous mettrons dans une librairie générale):


method uiEmpty_LO(ui UIOBJECT) Logical
var
tc tcursor
RetourLO Logical
endvar
RetourLO = True
try
tc.attach(ui)
if not tc.lock("Full") then
if not tc.lock("Write") then
          While not tc.eot()
             if not tc.deleteRecord() then
                RetourLO = False
                if not tc.nextrecord() then
                fail()
                endif
             endif
          endwhile
       else
       tc.empty()
          tc.unlock("Write")
       endif
    else
      tc.empty()
       tc.unlock("Full")
    endif
    tc.Home()
    UI.resync(tc)
onfail
errorLog(userError,
"Une erreur critique s'est produite dans SPEC01->uiEmpty_LO\\nUI : "+ui.Name)
errCopie()
   RetourLO = False
endtry
if tc.isAssigned() then tc.Close() endif
return RetourLO

endMethod

et dans la fiche appelante nous aurons:
;CDELIG.empty()     ancienne instruction
gLib.uiEmpty_LO(CDELIG)

Nous ferons une méthode similaire tcEmpty_LO pour vider des tcursors et ainsi disposerons de l'équivalent des instructions Empty() avec une optimisation d'exécution maximale.

Curiosité

Si vous faites le test précédent dans la session Paradox dans laquelle est ouverte la fiche, alors la méthode Empty() marche dans tous les cas. Pourquoi ?

Les cas où vous pouvez utiliser Empty() sans risque

Bien sûr, si votre application est toujours utilisée en mono-utilisateur, vous pouvez utiliser empty() sans risque. Il en est de même sur toutes les tables situées dans le répertoire privé de l'utilisateur.