Example lob demonstration
Aller à la navigation
Aller à la recherche
| Fiche express | |
|---|---|
| Type | Exemple DDL/PL-SQL Oracle |
| Contexte | Manipulation des types LOB (CLOB, BLOB, BFILE) via SQL et DBMS_LOB |
| Voir aussi | Example create table · Example create tablespace |
Tour d'horizon pratique des LOB (Large OBjects) Oracle — CLOB (texte),
BLOB (binaire) et BFILE (référence vers un fichier externe au système
de fichiers, en lecture seule côté Oracle) — et du package DBMS_LOB qui permet de
les manipuler par programmation, au-delà de ce que le SQL standard permet.
Ce qu'il faut retenir sur les LOB
- Une colonne LOB stocke en réalité un localisateur (locator), pas directement la
valeur : le SELECT ramène ce pointeur, et Oracle va chercher la donnée
correspondante en arrière-plan.
- SQL*Plus ne peut pas afficher une colonne
BLOBouBFILE
directement (SELECT * FROM …échoue avec « Column or attribute type can not be displayed by SQL*Plus ») — il faut sélectionner explicitement les colonnes affichables et formater les colonnesCLOB(COLUMN … FORMAT a60 WRAP).
- Une petite chaîne de caractères peut être insérée directement dans une colonne LOB
(jusqu'à 4 Ko environ) : Oracle gère la conversion en localisateur en arrière-plan. Pour du
binaire, la chaîne doit être hexadécimale, ou passer par
UTL_RAW.CAST_TO_RAW('...').
- Les localisateurs ne survivent pas à un COMMIT avant Oracle8i : il faut les
reséletionner après validation. À partir de 8i, un localisateur en lecture seule peut franchir les limites de transaction.
- Pour écrire via
DBMS_LOB, la ligne doit être verrouillée
(SELECT ... FOR UPDATE) — une simple sélection ne suffit pas.
- LOB temporaires (8i+,
DBMS_LOB.CREATETEMPORARY) : localisateurs pointant
vers le tablespace temporaire de la session, sans génération de redo/undo, existant pour la
durée de l'appel ou de la session ; à libérer explicitement
(DBMS_LOB.FREETEMPORARY) une fois l'usage terminé.
- Ouverture/fermeture explicite (8i+,
DBMS_LOB.OPEN/CLOSE) :
regroupe les écritures pour éviter de déclencher les triggers sur index étendus à chaque
écriture individuelle ; un LOB ouvert en lecture seule refuse toute écriture tant qu'il n'est
pas refermé (ORA-22294).
Script
/* --------------------------------------------------------
* Préparation : table de test avec un CLOB, un BFILE et un
* BLOB. Les LOB de moins de ~4 Ko peuvent rester "en ligne"
* dans la ligne (ENABLE STORAGE IN ROW) ; les plus gros sont
* stockés hors ligne (DISABLE STORAGE IN ROW).
* -------------------------------------------------------- */
DROP TABLE test_lobs;
DROP DIRECTORY tmp_dir;
DROP TABLE long_data;
CREATE TABLE test_lobs (
c1 NUMBER
, c2 CLOB
, c3 BFILE
, c4 BLOB
)
LOB (c2) STORE AS (ENABLE STORAGE IN ROW)
LOB (c4) STORE AS (DISABLE STORAGE IN ROW)
/
-- Ligne sans localisateur initialisé
INSERT INTO test_lobs VALUES (1, NULL, NULL, NULL);
-- Localisateurs créés mais pointant "dans le vide" (EMPTY_CLOB/EMPTY_BLOB)
INSERT INTO test_lobs VALUES (2, EMPTY_CLOB(), BFILENAME(NULL, NULL), EMPTY_BLOB());
-- Insertion directe de données courtes. '48656C6C6F' est du hexadécimal
-- ('Hello' en ASCII) ; UTL_RAW.CAST_TO_RAW convertit une chaîne de
-- caractères en RAW pour compléter le BLOB.
INSERT INTO test_lobs
VALUES (3, 'Some data for record 3.', BFILENAME(NULL, NULL),
'48656C6C6F' || UTL_RAW.CAST_TO_RAW(' there!'));
-- SQL*Plus ne peut afficher c3 (BFILE) ni c4 (BLOB) : ne sélectionner que c1/c2
COLUMN c2 FORMAT a60 WRAP
SELECT c1, c2 FROM test_lobs;
/* --------------------------------------------------------
* Depuis un bloc PL/SQL, on peut insérer une variable
* caractère dans un CLOB, mais pas la sélectionner directement
* dans une variable caractère si elle dépasse la taille du
* type cible (ici VARCHAR2(10), volontairement trop petit
* pour illustrer la limite).
* -------------------------------------------------------- */
DECLARE
c_lob VARCHAR2(10);
BEGIN
c_lob := 'Record 4.';
INSERT INTO test_lobs VALUES (4, c_lob, BFILENAME(NULL, NULL), EMPTY_BLOB());
END;
/
/* --------------------------------------------------------
* Depuis 8.1, TO_LOB migre des données LONG/LONG RAW vers
* CLOB/BLOB, dans un INSERT...SELECT ou un CREATE TABLE AS SELECT.
* -------------------------------------------------------- */
CREATE TABLE long_data (
c1 NUMBER
, c2 LONG
)
/
INSERT INTO long_data VALUES (1, 'This is some long data to be migrated to a CLOB');
INSERT INTO test_lobs
SELECT 5, TO_LOB(c2), NULL, NULL
FROM long_data
/
SELECT c1, c2 FROM test_lobs WHERE c1 = 5;
ROLLBACK;
/* --------------------------------------------------------
* BFILE : référence en lecture seule vers un fichier du système
* de fichiers du serveur, via un alias DIRECTORY. Les fichiers
* doivent être créés côté OS au préalable (hors SQL*Plus, via
* un shell ou un outil externe) puis associés par BFILENAME.
* -------------------------------------------------------- */
CREATE DIRECTORY tmp_dir AS '/tmp'
/
UPDATE test_lobs SET c3 = BFILENAME('TMP_DIR', 'rec2.txt') WHERE c1 = 2;
UPDATE test_lobs SET c3 = BFILENAME('TMP_DIR', 'rec3.txt') WHERE c1 = 3;
COMMIT;
-- Longueur des LOB (0 pour les localisateurs "vides")
COLUMN len_c2 FORMAT 9999
COLUMN len_c3 FORMAT 9999
COLUMN len_c4 FORMAT 9999
SELECT c1
, DBMS_LOB.GETLENGTH(c2) len_c2
, DBMS_LOB.GETLENGTH(c3) len_c3
, DBMS_LOB.GETLENGTH(c4) len_c4
FROM test_lobs
/
/* --------------------------------------------------------
* SUBSTR/INSTR sur LOB : paramètres inversés par rapport aux
* fonctions SQL standard (LOB, longueur, offset pour SUBSTR ;
* LOB, motif, offset, occurrence pour INSTR). Pour un BFILE le
* fichier doit d'abord être ouvert -> utilisable en PL/SQL
* uniquement dans ce cas.
* -------------------------------------------------------- */
COLUMN sub_c2 FORMAT a10
COLUMN ins_c4 FORMAT 99
SELECT c1
, DBMS_LOB.SUBSTR(c2, 9, 3) sub_c2
, DBMS_LOB.INSTR(c4, UTL_RAW.CAST_TO_RAW('ello'), 1, 1) ins_c4
FROM test_lobs
/
/* --------------------------------------------------------
* Écriture/lecture pas à pas avec DBMS_LOB : WRITE, READ,
* WRITEAPPEND, APPEND, TRIM, ERASE, COPY, plus lecture d'un
* BFILE vers un CLOB via LOADFROMFILE.
* -------------------------------------------------------- */
SET SERVEROUTPUT ON
SET LONG 1000
DECLARE
b_lob BLOB;
c_lob CLOB;
c_lob2 CLOB;
bf BFILE;
buf VARCHAR2(100) :=
'This is some text to put into a CLOB column in the' ||
CHR(10) || 'database. The data spans 2 lines.';
n NUMBER;
fn VARCHAR2(50);
fd VARCHAR2(50);
-- Affiche le contenu d'un CLOB ligne par ligne (découpe sur CHR(10))
PROCEDURE print_clob IS
offset NUMBER;
len NUMBER;
o_buf VARCHAR2(200);
amount NUMBER;
f_amt NUMBER := 0;
f_amt2 NUMBER;
amt2 NUMBER := -1;
BEGIN
len := DBMS_LOB.GETLENGTH(c_lob);
offset := 1;
WHILE len > 0 LOOP
amount := DBMS_LOB.INSTR(c_lob, CHR(10), offset, 1);
IF amount = 0 THEN
amount := len;
amt2 := amount;
ELSE
f_amt2 := amount;
amount := amount - f_amt;
f_amt := f_amt2;
amt2 := amount - 1;
END IF;
IF amt2 != 0 THEN
DBMS_LOB.READ(c_lob, amt2, offset, o_buf);
DBMS_OUTPUT.PUT_LINE(o_buf);
END IF;
len := len - amount;
offset := offset + amount;
END LOOP;
END;
BEGIN
-- Initialise les localisateurs de la ligne 1 (créés vides à la volée) et
-- verrouille la ligne via la clause RETURNING de l'UPDATE
UPDATE test_lobs SET c2 = EMPTY_CLOB(), c4 = EMPTY_BLOB()
WHERE c1 = 1 RETURNING c2, c4 INTO c_lob, b_lob;
SELECT c2 INTO c_lob2 FROM test_lobs WHERE c1 = 3;
DBMS_LOB.WRITE(c_lob, LENGTH(buf), 1, buf);
print_clob;
COMMIT;
-- Après COMMIT, le localisateur en écriture doit être resélectionné
-- avec verrou (FOR UPDATE)
SELECT c2 INTO c_lob FROM test_lobs WHERE c1 = 1 FOR UPDATE;
DBMS_LOB.WRITEAPPEND(c_lob, 1, CHR(10));
DBMS_LOB.APPEND(c_lob, c_lob2);
DBMS_OUTPUT.PUT_LINE(CHR(10));
print_clob;
-- Compare la fin de c_lob avec c_lob2 : si identique, la retire (TRIM
-- prend la taille FINALE souhaitée, pas la quantité à retirer)
n := DBMS_LOB.GETLENGTH(c_lob) - DBMS_LOB.GETLENGTH(c_lob2);
IF DBMS_LOB.COMPARE(c_lob, c_lob2, DBMS_LOB.GETLENGTH(c_lob2), n + 1, 1) = 0 THEN
DBMS_LOB.TRIM(c_lob, n - 1);
END IF;
DBMS_OUTPUT.PUT_LINE(CHR(10));
print_clob;
-- ERASE remet les octets à zéro SANS raccourcir la longueur logique
-- (contrairement à TRIM)
n := DBMS_LOB.GETLENGTH(c_lob);
DBMS_LOB.ERASE(c_lob, n, 1);
DBMS_LOB.COPY(c_lob, c_lob2, DBMS_LOB.GETLENGTH(c_lob2), 1, 1);
n := DBMS_LOB.GETLENGTH(c_lob2) + 1;
DBMS_LOB.WRITE(c_lob, 1, n, CHR(10));
-- Ajoute des données lues depuis le BFILE de la ligne 3
SELECT c3 INTO bf FROM test_lobs WHERE c1 = 3;
DBMS_LOB.FILEGETNAME(bf, fd, fn);
DBMS_OUTPUT.PUT_LINE(CHR(10));
DBMS_OUTPUT.PUT_LINE('Appending data from file ' || fn ||
' in directory aliased by ' || fd || ':');
DBMS_OUTPUT.PUT_LINE(CHR(10));
IF DBMS_LOB.FILEEXISTS(bf) = 1 AND DBMS_LOB.FILEISOPEN(bf) = 0 THEN
DBMS_LOB.FILEOPEN(bf);
END IF;
DBMS_LOB.LOADFROMFILE(c_lob, bf, DBMS_LOB.GETLENGTH(bf), n + 1, 1);
DBMS_LOB.FILECLOSE(bf);
print_clob;
COMMIT;
END;
/
COMMIT;
SELECT c1, c2 FROM test_lobs;
/* --------------------------------------------------------
* Isolation en lecture : un localisateur donne une image
* cohérente au moment où il a été sélectionné. Les changements
* faits via SQL direct (pas via ce même localisateur) ne sont
* pas visibles tant qu'on ne resélectionne pas.
* -------------------------------------------------------- */
DECLARE
c_lob CLOB;
BEGIN
SELECT c2 INTO c_lob FROM test_lobs WHERE c1 = 1;
DBMS_OUTPUT.PUT_LINE('Before update length of c2 is ' ||
DBMS_LOB.GETLENGTH(c_lob));
UPDATE test_lobs SET c2 = 'This is a string.' WHERE c1 = 1;
DBMS_OUTPUT.PUT_LINE('After update length of c2 is ' ||
DBMS_LOB.GETLENGTH(c_lob));
SELECT c2 INTO c_lob FROM test_lobs WHERE c1 = 1;
DBMS_OUTPUT.PUT_LINE('After reselecting locator length of c2 is ' ||
DBMS_LOB.GETLENGTH(c_lob));
ROLLBACK;
END;
/
COMMIT;
/* --------------------------------------------------------
* LOB temporaires (8i+) : localisateurs dans le tablespace
* temporaire, sans undo/redo, à libérer explicitement. Exemple :
* inverser une chaîne CLOB via un LOB temporaire puis insérer
* le résultat comme nouvelle ligne.
* -------------------------------------------------------- */
DECLARE
c_lob CLOB;
t_lob CLOB;
buf VARCHAR2(32000);
buf2 VARCHAR2(32000);
chunk NUMBER;
len NUMBER;
offset NUMBER;
amount NUMBER;
BEGIN
SELECT c2 INTO c_lob FROM test_lobs WHERE c1 = 1;
-- LOB temporaire, sans cache, de durée "call" (limitée à l'appel courant)
DBMS_LOB.CREATETEMPORARY(t_lob, FALSE, DBMS_LOB.CALL);
-- GETCHUNKSIZE renvoie la taille de bloc de stockage du LOB : aligner
-- lectures/écritures sur cette taille améliore les performances
chunk := DBMS_LOB.GETCHUNKSIZE(c_lob);
DBMS_OUTPUT.PUT_LINE('Chunksize of column c2 is ' || chunk);
DBMS_OUTPUT.PUT_LINE('Chunksize of temporary LOB is ' ||
DBMS_LOB.GETCHUNKSIZE(t_lob));
len := DBMS_LOB.GETLENGTH(c_lob);
offset := 1;
buf := NULL;
WHILE offset < len LOOP
IF len - (offset - 1) > chunk THEN
amount := chunk;
ELSE
amount := len - (offset - 1);
END IF;
buf2 := NULL;
DBMS_LOB.READ(c_lob, amount, offset, buf2);
buf := buf || buf2;
offset := offset + amount;
END LOOP;
buf2 := NULL;
FOR i IN REVERSE 1..len LOOP
buf2 := buf2 || SUBSTR(buf, i, 1);
END LOOP;
DBMS_LOB.WRITEAPPEND(t_lob, len, buf2);
-- Insertion en passant directement le localisateur temporaire en bind
INSERT INTO test_lobs VALUES (5, t_lob, NULL, NULL) RETURNING c2 INTO c_lob;
IF DBMS_LOB.ISTEMPORARY(t_lob) = 1 THEN
DBMS_LOB.FREETEMPORARY(t_lob);
END IF;
DBMS_OUTPUT.PUT_LINE('Length of CLOB inserted into record 5 is ' ||
DBMS_LOB.GETLENGTH(c_lob));
COMMIT;
END;
/
SELECT c1, c2 FROM test_lobs WHERE c1 = 5;
/* --------------------------------------------------------
* Ouverture/fermeture explicite (8i+) : un LOB ouvert en lecture
* seule (LOB_READONLY) refuse toute écriture jusqu'à sa fermeture
* (ORA-22294) ; pour écrire, il faut le sélectionner FOR UPDATE
* puis l'ouvrir en LOB_READWRITE.
* -------------------------------------------------------- */
DECLARE
c_lob1 CLOB;
c_lob2 CLOB;
BEGIN
SELECT c2 INTO c_lob1 FROM test_lobs WHERE c1 = 2;
c_lob2 := c_lob1;
DBMS_LOB.OPEN(c_lob1, DBMS_LOB.LOB_READONLY);
BEGIN
DBMS_LOB.WRITEAPPEND(c_lob1, 5, 'Hello'); -- lève ORA-22294
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(SQLERRM);
END;
-- COMMIT/ROLLBACK sont autorisés (aucune transaction ouverte) ; le LOB
-- reste ouvert après
ROLLBACK;
IF DBMS_LOB.ISOPEN(c_lob2) = 1 THEN
DBMS_OUTPUT.PUT_LINE('Closing LOB via locator 2');
DBMS_LOB.CLOSE(c_lob2);
END IF;
IF DBMS_LOB.ISOPEN(c_lob1) = 1 THEN
DBMS_OUTPUT.PUT_LINE('Closing LOB via locator 1');
DBMS_LOB.CLOSE(c_lob1);
END IF;
-- Pour écrire, il faut verrouiller la ligne puis ouvrir en lecture/écriture
SELECT c2 INTO c_lob1 FROM test_lobs WHERE c1 = 2 FOR UPDATE;
DBMS_LOB.OPEN(c_lob1, DBMS_LOB.LOB_READWRITE);
DBMS_LOB.WRITEAPPEND(c_lob1, 5, 'Hello');
DBMS_LOB.WRITEAPPEND(c_lob1, 7, ' there.');
-- Le LOB doit être refermé avant COMMIT/ROLLBACK
DBMS_LOB.CLOSE(c_lob1);
COMMIT;
END;
/
SELECT c2 FROM test_lobs WHERE c1 = 2;
Les créations de fichiers texte via l'échappement shell SQL*Plus (!echo "..." >
/tmp/rec2.txt) ont été retirées du script : elles dépendent de l'OS hôte et sortent du
périmètre SQL — à recréer manuellement côté serveur si l'exemple est rejoué (deux fichiers
texte, référencés ensuite par BFILENAME('TMP_DIR', 'rec2.txt') et
'rec3.txt').
Voir aussi
- Example create table — création de table classique, pour comparaison avec les colonnes LOB
- Example create tablespace — tablespace dans lequel vivent les segments LOB hors ligne