FirebirdSQL logo
 Firebird InterfacesÉcriture de plugins 

Modification des données par lots

Étant donné que Firebird 4.0 prend en charge l’exécution par lots d’instructions avec des paramètres d’entrée, cela signifie l’envoi de plus d’un ensemble de paramètres lors de l’exécution de l’instruction. L’interface de traitement par lots est conçue (principalement) pour répondre aux exigences de JDBC en matière de traitement par lots des instructions préparées, mais elle présente un certain nombre de différences majeures :

  • Comme toutes les opérations de données dans Firebird, elles sont orientées message, pas orientées champ ;

  • En tant qu’extension importante, l’interface de traitement par lots prend en charge l’utilisation intégrée d’objets blob (particulièrement efficace lorsque vous travaillez avec de petits objets blob).

  • La méthode execute() ne renvoie pas un simple tableau d’entiers, mais une interface spéciale appelée IBatchCompletionState, qui peut (en fonction des paramètres de la création du paquet) contenir à la fois des informations sur les mises à jour d’enregistrements et, en plus de l’indicateur d’erreur, des vecteurs d’état détaillés pour les messages à l’origine des erreurs d’exécution.

L’interface IBatch (tout comme IResultSet) peut être créée de deux manières, en utilisant l’interface IStatement ou IAttachment, qui appellent toutes deux la méthode createBatch() de l’interface correspondante.Dans le second cas, le texte de l’instruction SQL à exécuter dans le lot est passé directement à createBatch(). Le traitement par lots est configuré à l’aide du bloc Paramètres Batch, dont le format est plus ou moins similaire à celui de DPB v.2 – au début, il y a une balise (IBatch::CURRENT_VERSION), suivie d’un ensemble de clusters larges : une balise de 1 octet, une longueur de 4 octets, la valeur de la longueur d’octet spécifiée. Les balises possibles sont décrites dans l’interface de traitement par lots.Le moyen le plus simple (et recommandé) de créer un bloc de paramètres pour la création par lots est d’utiliser l’interface appropriée IXpbBuilder :

IXpbBuilder* pb = utl->getXpbBuilder(&status, IXpbBuilder::BATCH, NULL, 0);
pb->insertInt(&status, IBatch::RECORD_COUNTS, 1);

L’utilisation d’un tel bloc de paramètres indique au paquet de renvoyer le nombre d’enregistrements mis à jour pour chaque message.

Pour créer une interface batch avec les paramètres requis, passez le bloc de paramètres à l’appel createBatch() :

IBatch* batch = att->createBatch(&status, tra, 0, sqlStmtText, SQL_DIALECT_V6, NULL,
  pb->getBufferLength(&status), pb->getBuffer(&status));

Dans cet exemple, l’interface de traitement par lots est créée avec le format de message par défaut, car « NULL » est transmis à la place du format de métadonnées d’entrée.

Pour travailler avec l’interface de traitement par lots créée, nous devons connaître le format des messages qu’elle contient.Il peut être récupéré à l’aide de la méthode getMetadata() :

IMessageMetadata* meta = batch->getMetadata(&status);

Bien sûr, si vous avez passé votre propre format de message à un lot, vous pouvez simplement l’utiliser.

De plus, je suppose qu’il existe une fonction fillNextMessage(unsigned char* data, IMessageMetadata* metadata) et qu’elle peut remplir le tampon data selon le format passé metadata. Pour travailler avec les messages, nous avons besoin d’un tampon pour les données :

unsigned char* data = new unsigned char[meta->getMessageLength(&status)];

Maintenant, nous pouvons ajouter plusieurs messages avec des données remplies au lot :

fillNextMessage(data, meta);
batch->add(&status, 1, data);

fillNextMessage(data, meta);
batch->add(&status, 1, data);

Une autre façon de travailler avec les messages (à l’aide de la macro FB_MESSAGE) est présente dans l’exemple de l’interface de traitement par lots 11.batch.cpp.

Enfin, le lot devrait être exécuté :

IBatchCompletionState* cs = batch->execute(&status, tra);

Nous avons demandé un décompte du nombre d’enregistrements modifiés (insérés, mis à jour ou supprimés) pour chaque publication. Pour l’afficher, nous devons utiliser l’interface IBatchCompletionState.Déterminez le nombre total de messages traités par le lot (il peut être inférieur au nombre de messages envoyés au lot si une erreur s’est produite et que l’option permettant de renvoyer plusieurs erreurs pendant le traitement par lots n’a pas été activée) :

unsigned total = cs->getSize(&status);

Affichons maintenant l’état de chaque message :

for (unsigned p = 0; p < total; ++p) printf("Msg %u state %d\n", p, cs->getState(&status, p));

Lorsque vous avez terminé d’analyser l’état d’achèvement, n’oubliez pas de le supprimer :

cs->dispose();

Un exemple complet d’affichage du contenu IBatchCompletionState peut être trouvé dans la fonction print_cs() dans l’exemple 11.batch.cpp.

Si, pour une raison quelconque, vous souhaitez vider les tampons de traitement sans les exécuter (c’est-à-dire pour préparer le traitement d’un nouveau lot de messages), utilisez la méthode cancel() :

batch->cancel(&status);

Comme le reste de nos interfaces d’accès aux données, IBatch dispose d’une méthode spéciale pour la fermer :

batch->close(&status);

Au lieu de cela, vous pouvez utiliser l’appel standard release() si vous ne vous souciez pas des erreurs :

batch->release();

Ces techniques vous aident à mettre en œuvre tout ce dont vous avez besoin pour les opérations par lots de type JDBC avec des instructions préparées.

Note
Remarque

JDBC ne recommande pas d’utiliser des lots trop volumineux, par exemple, « Oracle recommande de maintenir la taille des lots entre 50 et 100 ».Firebird prend en charge des lots volumineux, mais dans tous les cas, il doit limiter la taille maximale des lots - voir TAG_BUFFER_BYTES_SIZE.Si la taille totale des messages dépasse cette limite, une erreur isc_batch_too_big est renvoyée.Notez qu’en raison de la mise en mémoire tampon des données de paquets lors de leur envoi sur le réseau, vous n’obtiendrez pas cette erreur immédiatement, mais uniquement lorsque vous viderez les tampons du client vers le serveur. Cela peut se produire à la fois dans les méthodes add() et execute()execute() qui effectue le vidage final du tampon. Si vous souhaitez toujours exécuter le lot avec les messages qui ont rempli le tampon, vous pouvez le faire (lorsque la fonction execute() a renvoyé une erreur, il suffit de la réessayer).Le nombre réel de messages traités sera renvoyé à IBatchCompletionState.La taille optimale du lot doit être trouvée pour chaque cas, mais il y a de fortes chances que si elle est supérieure à 1000, il est peu probable que vous obteniez une augmentation majeure des performances.

Vous pouvez ajouter plus d’un message par appel à un lot. Cela étant dit, n’oubliez pas que les messages doivent être correctement alignés pour que cette fonctionnalité fonctionne correctement. L’alignement requis et la taille du message aligné doivent être obtenus à partir de l’interface IMessageMetadata, par exemple :

unsigned aligned = meta->getAlignedLength(&status);

Plus tard, cette taille sera utile lors de la sélection et de l’utilisation d’un tableau de messages :

unsigned char* data = new unsigned char[aligned * N]; // N est le nombre de messages souhaité

for (int n = 0; n < N; ++n)
  fillNextMessage(&data[aligned * n], meta);

batch->add(&status, N, data);

Après cela, le lot peut être exécuté ou le lot suivant de messages peut y être ajouté.

Les objets blob sont généralement incompatibles avec les lots : batch est efficace lorsque vous devez transférer beaucoup de petites données vers le serveur en une seule étape, les champs blob sont traités comme des objets volumineux, et il n’est donc pas logique de les utiliser dans des lots en général.Mais dans la pratique, il arrive souvent que les BLOB ne soient pas trop volumineux – auquel cas l’utilisation de l’API BLOB traditionnelle (création d’un blob, envoi de segments vers le serveur, fermeture du blob, passage de l’ID BLOB dans un message) tue les performances, en particulier lorsqu’il est utilisé sur le WAN. Par conséquent, dans Firebird, le paquet prend en charge l’envoi de BLOB au serveur avec d’autres messages.Pour utiliser cette fonctionnalité, vous devez d’abord définir la stratégie d’utilisation d’objets blob pour le package que vous créez (en tant qu’option dans le bloc Paramètres) :

pb->insertInt(&status, IBatch::BLOB_IDS, IBatch::BLOB_IDS_ENGINE);

Dans cet exemple, les objets BLOB temporaires nécessaires pour maintenir la communication entre le BLOB et le message dans lequel ils sont utilisés seront générés par le moteur Firebird : il s’agit de l’utilisation la plus simple et la plus courante.Imaginez que le message soit décrit comme suit :

FB_MESSAGE(Msg, ThrowStatusWrapper,
  (FB_VARCHAR(5), id)
  (FB_VARCHAR(10), name)
  (FB_BLOB, desc)
) project(&status, master);

Dans ce cas, pour envoyer un message contenant un objet blob au serveur, vous pouvez faire quelque chose comme ceci :

project->id = ++idCounter;
project->name.set(currentName);

batch->addBlob(&status, descriptionSize, descriptionText, &project->desc);

batch->add(&status, 1, project.getData());

Si un objet blob est trop grand pour tenir dans votre mémoire tampon existante, vous pouvez utiliser la méthode appendBlobData() au lieu de redimentionner la mémoire tampon.Il ajoute plus de données au dernier BLOB ajouté.

batch->addBlob(&status, descriptionSize, descriptionText, &project->desc, bpbLength, bpb);

Après avoir ajouté la première partie du BLOB, récupérez la donnée suivante dans descriptionText, avec la taille descriptionSize puis :

batch->appendBlobData(&status, descriptionSize, descriptionText);

Cela peut être fait en boucle, mais veillez à ne pas trop remplir les tampons de lots internes : leur taille est contrôlée par le paramètre BUFFER_BYTES_SIZE lors de la création de l’interface IBatch, mais ne peut pas dépasser 256 Mo (16 Mo par défaut). Si vous avez besoin de traiter un blob aussi volumineux (par exemple, dans le contexte de nombreux petits blobs, ce qui peut expliquer l’utilisation du traitement par lots), utilisez simplement l’API standard IBlob et la méthode registerBlob de l’interface IBatch.

Un autre choix possible pour une stratégie BLOB est BLOB_IDS_USER. À première vue, l’utilisation ne change pas grand-chose — avant d’appeler addBlob(), l’identifiant correct et unique de chaque blob doit être placé dans la mémoire référencée par le dernier paramètre. Bien entendu, le même identifiant doit être transmis dans le message pour le BLOB.Étant donné que la génération d’objets blob par le moteur est très rapide, une telle politique peut sembler inutile, mais imaginez un cas où vous obtenez des objets blob et d’autres données dans des threads indépendants (par exemple, des blocs de fichiers) et où de bons ID sont déjà présents. Dans ce cas, l’utilisation de BLOB fournis par l’utilisateur peut grandement simplifier le code.

Note
Note

Contrairement aux objets blob créés à l’aide de la fonction createBlob() standard, les objets blob créés à l’aide de l’interface IBatch sont diffusés en continu par défaut, et non partitionnés. Les BLOB segmentés n’ont rien d’intéressant par rapport aux blobs en streaming et ne sont donc pas recommandés pour une utilisation dans les nouveaux développements. Nous ne prenons en charge ce format que pour des raisons de rétrocompatibilité. Si vous voulez vraiment des objets blob partitionnés, vous pouvez remplacer cette valeur par défaut en appelant :

batch->setDefaultBpb(&status, bpbLength, bpb);

Bien entendu, le BPB téléchargé peut contenir d’autres paramètres de création de BLOB. Comme vous l’avez peut-être déjà remarqué, vous pouvez également passer BPB directement à addBlob(), mais si la plupart des blobs que vous allez ajouter sont dans le même format différent du format par défaut, alors l’utilisation de setDefaultBpb() est un peu plus efficace.Pour en revenir aux blobs partitionnés, l’appel de addBlob() ajoutera la première partition à l’objet blob, les appels suivants à appendBlobData() ajouteront d’autres partitions. N’oubliez pas que la taille du segment est limitée à « 64 Ko – 1 », essayer de transférer plus de données en un seul appel provoquera une erreur.

L’étape suivante consiste à travailler avec des flux BLOB existants, qui utilisent la méthode addBlobStream().En l’utilisant, vous pouvez ajouter plus d’un BLOB à un lot en un seul appel. Un flux BLOB est une séquence de BLOB, chacun d’entre eux commençant par un en-tête BLOB. L’en-tête doit être correctement aligné — pour cela, il y a un appel spécial dans l’interface IBatch :

unsigned alignment = batch->getBlobAlignment(&status);

Il est supposé que tous les composants du flux BLOB dans le lot doivent être alignés au moins sur la limite d’alignement, y compris la taille des portions de flux passées à addBlobStream(), qui doit être un multiple de cet alignement.L’en-tête contient 3 champs : un objet BLOB de 8 octets (qui doit être différent de zéro), une taille totale de BLOB de 4 octets et une taille BPB de 4 octets.La taille totale d’un objet blob inclut le BPB à l’intérieur, ce qui signifie que vous pouvez toujours trouver l’objet blob suivant dans le flux dans un octet de taille d’objet blob après l’en-tête (y compris l’alignement). Le BPB (s’il est présent, c’est-à-dire si la taille du BPB n’est pas nul) est placé immédiatement après l’en-tête. Une fois que les données de l’objet blob BPB sont transférées, le format de l’objet blob varie selon que l’objet blob est diffusé en continu ou partitionné. Dans le cas d’un blob en streaming, il s’agit d’une simple séquence d’octets dont la taille est blob-size – BPB-size.Avec un blob partitionné, les choses sont un peu plus compliquées : les données de l’objet blob sont une collection de partitions, où chaque partition a le format suivant : la taille du segment est de 2 octets (elle doit être alignée sur la limite de IBatch::BLOB_SEGHDR_ALIGN), suivie des 2 octets du nombre d’octets qui y sont stockés.

Lorsqu’un BLOB est ajouté à un flux, sa taille n’est pas toujours connue à l’avance. Pour éviter d’avoir une mémoire tampon trop grande pour cet objet blob (n’oubliez pas que la taille doit être spécifiée dans l’en-tête BLOB avant les données de l’objet blob), vous pouvez utiliser un enregistrement de continuation BLOB. Dans l’en-tête BLOB, vous laissez la taille du BLOB avec la valeur connue lors de la création de l’en-tête, et ajoutez un enregistrement de continuation qui a exactement le même format que l’en-tête BLOB, mais ici l’ID BLOB doit être égal à zéro, et la taille du BPB doit toujours être également nulle. En règle générale, vous aurez besoin d’une entrée de continuation pour chaque appel à addBlobStream().

Cette dernière méthode, qui est utilisée pour travailler avec les BLOB, est distincte des trois premières, qui transmettent les données BLOB avec le reste des données du lot - elle doit s’enregistrer dans l’identifiant du package BLOB créé à l’aide de l’API BLOB standard.Cela peut être inévitable si vous souhaitez empaqueter un blob très volumineux. N’utilisez pas l’ID d’objet blob dans le lot, car cela entraînerait une erreur d’ID d’objet blob non valide lors de l’exécution du lot. Au lieu de cela, exécutez :

batch->registerBlob(&status, &realId, &msg->desc);

Si la politique BLOB amène le moteur Firebird à générer des BLOB, ce code est suffisant pour enregistrer correctement le BLOB existant dans le lot. Dans d`autres cas, vous devrez assigner l`identificateur correct (à partir du paquet POV) pour msg→desc.

Presque toutes les méthodes mentionnées sont utilisées dans 11.batch.cpp - utilisez-le pour voir un exemple en direct de traitement par lots dans firebird.

Quelques mots sur l’accès aux traitements par lots à partir de l’API ISC - vous pouvez exécuter une instruction ISC préparée en mode batch.Pour ce faire, deux nouvelles fonctions d’API ont été ajoutées, à savoir fb_get_transaction_interface et fb_get_statement_interface, qui permettent d’accéder à des interfaces pertinentes identiques aux descripteurs ISC existants. Un exemple de ceci est présent dans le 12.batch_isc.cpp.

Utilisation des événements

L’interface utilisateur de l’événement n’a pas été finalisé dans Firebird 4.0, nous nous attendons à ce qu’il y ait quelque chose de plus intéressant dans la prochaine version. Le support minimum existant est le suivant : IAttachment contient la méthode queEvents(), qui remplit presque les mêmes fonctions que l’appel isc_que_events().Au lieu de la paire de paramètres FPTR_EVENT_CALLBACK ast et void* arg nécessaires pour appeler le code utilisateur lorsqu’un événement se produit dans Firebird, l’interface de rappel IEventCallback est utilisée. Il s’agit d’une approche traditionnelle qui permet d’éviter les appels vide* dangereux dans une fonction personnalisée. Une autre différence importante est qu’au lieu d’un identificateur d’événement (une sorte de gestionnaire), cette fonction renvoie une référence à l’interface IEvents, qui a une méthode cancel() utilisée pour arrêter l’événement d’écoute. Contrairement à l’identifiant, qui est détruit automatiquement à l’arrivée d’un événement, une interface ne peut pas être détruite automatiquement si l’événement est reçu juste avant l’appel de la méthode cancel(), cela provoquera une erreur de segmentation car l’interface sera déjà détruite. Par conséquent, une fois l’événement reçu, l’interface IEvents doit être explicitement libérée. Cela peut être fait, par exemple, juste avant de demander un événement à la file d’attente:

events->release();
events = NULL;

events = attachment->queEvents(&status, this, eveLen, eveBuffer);

Définir le pointeur de l’interface à NULL est utile en cas d’exception dans queEvents. À d’autres égards, la gestion des événements n’a pas changé par rapport à l’API ISC. Pour plus d’informations, utilisez notre exemple 08.events.cpp.