I. Préambule▲
Afin de bien comprendre l’intérêt de cette pratique, il vous est vivement conseillé de lire l’article dédié à l’introduction de RDF [cf. mon article en cours de publication sur développez.com] ainsi qu’un autre d’exemple sur FOAF.
Il est vivement conseillé d’avoir quelques notions du modèle RDF et des requêtes standards SQL pour la lecture de cet article.
Note de l’auteur : d’après les fiches de travail de Tim Berners’Lee, RDF s’accommode très bien d’une base de données relationnelle pour le stockage des données. Nous démontrons une telle symbiose ici.
II. Objectifs▲
- Nous allons sérialiser des données stockées dans une base de données relationnelle vers une présentation « triplet » classique, seulement avec SQL.
- Nous allons réaliser l’équivalent d’une jointure entre deux tables et voir l’intérêt d’une telle procédure.
III. Prérequis et démarrage▲
Le code SQL fourni a été validé sur un Ubuntu 19.04 et la version 10.3 de MariaDB (paquet par défaut de la distribution). À noter que sur cette version, certaines fonctions de pivot de table prévues par le langage SQL ne sont pas disponibles directement.
Vous n’avez pas besoin d’autres outils qu’une connexion à votre base de données, avec des droits de création et de consultation de tables, de vues et de procédures stockées. Pour un souci de confort, je vous invite à tester les requêtes avec PHPMyAdmin ou une interface équivalente, surtout si vous n’êtes pas à l’aise avec les concepts abordés ou les requêtes SQL.
Nous admettrons ici que vous avez une base vierge sur laquelle nous allons créer deux tables et les peupler par quelques données (le format de la table n’a que peu d’importance, tant que chaque table dispose d’une clef primaire ou d’un index unique pour une des colonnes).
Table « factures » :
-- Version de PHP : 7.2.19-0ubuntu0.19.04.1
SET
SQL_MODE =
"NO_AUTO_VALUE_ON_ZERO"
;
SET
AUTOCOMMIT =
0
;
START
TRANSACTION
;
SET
time_zone =
"+00:00"
;
--
-- Base de données : `rdfxml`
--
-- --------------------------------------------------------
--
-- Structure de la table `factures`
--
CREATE
TABLE
`factures`
(
`facture_id`
int
(
11
)
NOT
NULL
,
`facture_emission`
date
DEFAULT
NULL
,
`facture_reglement`
date
DEFAULT
NULL
,
`client_id`
int
(
11
)
NOT
NULL
,
`liste_id`
int
(
11
)
NOT
NULL
,
`facture_total_ht`
decimal
(
10
,2
)
NOT
NULL
,
`facture_total_tva`
decimal
(
10
,2
)
NOT
NULL
,
`facture_total_ttc`
decimal
(
10
,2
)
NOT
NULL
)
ENGINE
=
InnoDB DEFAULT
CHARSET
=
utf8mb4;
--
-- Déchargement des données de la table `factures`
--
INSERT
INTO
`factures`
(
`facture_id`
, `facture_emission`
, `facture_reglement`
, `client_id`
, `liste_id`
, `facture_total_ht`
, `facture_total_tva`
, `facture_total_ttc`
)
VALUES
(
1
, '2019-08-01'
, NULL
, 1
, 1
, '15.00'
, '3.00'
, '18.00'
)
;
INSERT
INTO
`factures`
(
`facture_id`
, `facture_emission`
, `facture_reglement`
, `client_id`
, `liste_id`
, `facture_total_ht`
, `facture_total_tva`
, `facture_total_ttc`
)
VALUES
(
2
, '2019-08-02'
, '2019-08-03'
, 1
, 2
, '8.50'
, '2.00'
, '10.50'
)
;
--
-- Index pour les tables déchargées
--
--
-- Index pour la table `factures`
--
ALTER
TABLE
`factures`
ADD
PRIMARY
KEY
(
`facture_id`
)
;
--
-- AUTO_INCREMENT pour les tables déchargées
--
--
-- AUTO_INCREMENT pour la table `factures`
--
ALTER
TABLE
`factures`
MODIFY
`facture_id`
int
(
11
)
NOT
NULL
AUTO_INCREMENT
, AUTO_INCREMENT
=
3
;
COMMIT
;
Table « clients » :
-- phpMyAdmin SQL Dump
-- version 4.8.2
-- https://www.phpmyadmin.net/
--
-- Hôte : localhost
-- Généré le : Dim 11 août 2019 à 12:23
-- Version du serveur : 10.3.13-MariaDB-2
-- Version de PHP : 7.2.19-0ubuntu0.19.04.1
SET
SQL_MODE =
"NO_AUTO_VALUE_ON_ZERO"
;
SET
AUTOCOMMIT =
0
;
START
TRANSACTION
;
SET
time_zone =
"+00:00"
;
--
-- Base de données : `rdfxml`
--
-- --------------------------------------------------------
--
-- Structure de la table `clients`
--
CREATE
TABLE
`clients`
(
`client_id`
int
(
11
)
NOT
NULL
,
`client_nom`
varchar
(
255
)
NOT
NULL
,
`client_prenom`
varchar
(
255
)
NOT
NULL
)
ENGINE
=
InnoDB DEFAULT
CHARSET
=
utf8mb4;
--
-- Déchargement des données de la table `clients`
--
INSERT
INTO
`clients`
(
`client_id`
, `client_nom`
, `client_prenom`
)
VALUES
(
1
, 'Garderon'
, 'Julien'
)
;
--
-- Index pour les tables déchargées
--
--
-- Index pour la table `clients`
--
ALTER
TABLE
`clients`
ADD
PRIMARY
KEY
(
`client_id`
)
;
--
-- AUTO_INCREMENT pour les tables déchargées
--
--
-- AUTO_INCREMENT pour la table `clients`
--
ALTER
TABLE
`clients`
MODIFY
`client_id`
int
(
11
)
NOT
NULL
AUTO_INCREMENT
, AUTO_INCREMENT
=
2
;
COMMIT
;
IV. Le format triplet▲
IV-A. Exemple de résultat▲
Notre objectif est de récupérer ici un retour de la base qui soit compatible avec le format triplet : c’est-à -dire ici 3 colonnes (sujet, prédicat, objet). Notre finalité est ici d’aboutir à un résultat similaire à ceci :
SELECT
sujet, prédicat, objet FROM
`rdf`
 ;
Notez que conformément au fonctionnement de RDF, il n’y a plus de notion de « clef » (primaire ou d’index) dans un tel résultat qui, dans notre cas, sera sous le format d’une jonction de vues.
IV-B. Le principe▲
Il n’est pas spécialement difficile de passer d’un format relationnel à un format triplet en SQL. Nous utiliserons pour cela la commande UNION ALL, qui permet de regrouper dans un résultat unique plusieurs requêtes de sélection (le nombre de colonnes et leur type doivent être identiques).
Exécutez dans votre éditeur la requête suivante :
SELECT
`facture_id`
, 'emission'
, `facture_emission`
FROM
`factures`
Le résultat est déjà un simulacre de RDF avec la première colonne qui serait le sujet (identifiant), la colonne « emission » qui serait la valeur de relation (prédicat) et le sujet – en l’occurrence la date d’émission de la facture :
C’est intéressant, mais nous n’avons là qu’une seule colonne pour une table. Exécutez maintenant :
SELECT
`facture_id`
AS
sujet,
'a'
AS
predicat,
'sql:factures'
AS
objet
FROM
`factures`
UNION
ALL
SELECT
`facture_id`
,
'emission'
,
`facture_emission`
FROM
`factures`
UNION
ALL
SELECT
`facture_id`
,
'reglement'
,
`facture_reglement`
FROM
`factures`
UNION
ALL
SELECT
`facture_id`
,
'client'
,
`client_id`
FROM
`factures`
UNION
ALL
SELECT
`facture_id`
,
'items'
,
`liste_id`
FROM
`factures`
UNION
ALL
SELECT
`facture_id`
,
'total#ht'
,
`facture_total_ht`
FROM
`factures`
UNION
ALL
SELECT
`facture_id`
,
'total#tva'
,
`facture_total_tva`
FROM
`factures`
UNION
ALL
SELECT
`facture_id`
,
'total#ttc'
,
`facture_total_ttc`
FROM
`factures`
;
Notre retour contient désormais l’ensemble des données de la table, dans un format conforme aux triplets. Il reste que notre ontologie (c’est-à -dire la grammaire pour écrire les triplets et les rendre symboliquement compatibles dans un espace de sens donné) n’est pas conforme.
De plus, cette écriture, si elle reste faisable à la main, est loin d’être efficace pour un grand nombre de tables et c’est une source d’erreur (oubli). Une meilleure solution doit être trouvée.
IV-C. Les procédures : solution A – s’appuyer exclusivement sur les informations de la table▲
Notre première solution ne s’appuie que sur les informations déjà contenues dans nos tables. Elle a ma préférence si derrière, le système de traitement est capable de supporter des transformations de triplets ou des inférences afin de rendre conforme l’ontologie à celle de l’architecture visée.
Nous utiliserons pour cela une procédure stockée, qui produira à partir des informations sur la table visée (grâce à INFORMATION_SCHEMA.COLUMNS), une requête de création de vue. C’est donc à partir d’une vue de la table, qui est conforme à une présentation au format triplet, que nous pourrions réaliser des jointures.
DROP
PROCEDURE
IF
EXISTS
`serialiser`
;
DELIMITER
|
CREATE
PROCEDURE
`serialiser`
(
IN
table_nom varchar
(
25
)
,
IN
table_colref varchar
(
25
)
,
IN
debug boolean
)
BEGINDECLARE stop_curseur_colonnes INT
DEFAULT
0
;
DECLARE
colonne_nom VARCHAR
(
25
)
;
DECLARE
colonne_type VARCHAR
(
15
)
;
DECLARE
`argument_vide`
CONDITION
FOR
SQLSTATE
'45000'
;
DECLARE
curseur_colonnes CURSOR
FOR
SELECT
COLUMN_NAME
,
data_type
FROM
information_schema.columns
WHERE
TABLE_NAME
=
table_nom;
DECLARECONTINUE HANDLER
FOR
SQLSTATE
'02000'
SET
stop_curseur_colonnes =
1
;
DECLARE
EXIT
HANDLER
FOR
sqlexceptionBEGIN
SELECT
Concat
(
"erreur : la sérialisation a échoué ('"
,
@requete, "')."
)
;
END
;
IF
table_nom =
""
THEN
SET
@requete =
"le nom de la table est vide"
;
SIGNAL
`argument_vide`
;
ENDIF;
IF
table_nom =
""
THEN
SET
@requete =
"le nom de la colonne identifiante est vide"
;
SIGNAL
`argument_vide`
;
ENDIF;
SET
@refSQL =
Concat
(
'CONCAT( "refSQL:facture#", '
, table_colref,
' )'
)
;
SET
@requete =
Concat
(
'SELECT '
, @refSQL, ' as sujet, "a" as predicat, "ontoSQL:'
,
table_nom, '" as objet FROM '
, table_nom
)
;
OPEN
curseur_colonnes;
REPEATFETCH curseur_colonnes INTO
colonne_nom,
colonne_type;
IF
stop_curseur_colonnes =
0
THEN
SET
@requete =
concat
(
@requete, ' UNION ALL SELECT '
, @refSQL,
', "ontoSQL:'
, table_nom, '#'
, colonne_nom,
'", CONCAT( \'"\', IFNULL( '
, colonne_nom,
', "") , \'"^^SQL:'
, colonne_type,
'\' ) FROM '
, table_nom
)
;
ENDIF;
UNTIL
stop_curseur_colonnes END
repeat
;
IF
debug IS
FALSE
THEN
SET
@requetePrepare =
concat
(
'DROP VIEW IF EXISTS `rdf_'
, table_nom,
'`; '
)
;
PREPARE
stmt
FROM
@requetePrepare;
EXECUTE
Stmt;
SET
@requeteVue =
Concat
(
' CREATE VIEW `rdf_'
, table_nom,
'` AS '
, @requete, ' ; '
)
;
PREPARE
stmt
FROM
@requeteVue;
EXECUTE
Stmt;
SET
@requeteSelection =
concat
(
' SELECT * FROM `rdf_'
, table_nom,
'` ; '
)
;
PREPARE
stmt
FROM
@requeteSelection;
EXECUTE
stmt;
DEALLOCATE
PREPARE
stmt;
END
IF
;
IF
DEBUG IS
TRUE
THEN
SELECT
@requete;
END
IF
;
END
|
DELIMITER
;
Cette procédure vous permettra de produire une requête (et éventuellement l’exécuter si DEBUG est à false ) qui produira elle-même une vue conforme à la table. Attention, pour que cette procédure aboutisse et soit « logique », il faut que votre sérialisation se produise avec une colonne « pivot » qui est la colonne identifiante (cf. la remarque en prérequis).
call
`serialiser`
(
"factures"
, "facture_id"
, FALSE
-- la requête sera exécutée immédiatement
)
;
Vous devriez avoir des retours similaires à ceci :
Une vue a été créée (pensez à recharger la page si vous êtes sous PHPMyAdmin et qu’elle n’apparaît pas automatiquement) :
Vous noterez désormais qu’il y a une conformité plus grande des résultats de la vue avec le modèle RDF canonique : les valeurs d’objet indiquent leur type (int , decimal , date , etc.). Les attributs marqués NULL sont traités ici et affichés comme une date invalide. Nous aurions pu les extraire des résultats pour respecter une règle de RDF qui indique qu’il ne faut normalement pas afficher les colonnes marquées NULL, cependant, « l’esprit » de la table est ici d’avoir du sens à une absence de date pour la colonne « facture_reglement » (pas de date = pas de règlement).
La colonne des prédicats semble respecter davantage un début de grammaire et les sujets, si ce ne sont pas directement des URI, sont des références « uniques » à la base visée. Le système de traitement peut donc ensuite faire « pointer » correctement une telle référence à la donnée concernée.
Nous pourrions nous contenter d’une telle présentation et répéter la commande de sérialisation pour la table « clients ». Mais peut-être y a-t-il une autre solution ?
IV-D. Les procédures : solution B – respecter une grammaire définie▲
L’intérêt de SQL est de définir pour chaque colonne un type de valeur bien défini. C’est ainsi que dans la solution précédente, nous avons traité chaque cellule et produit pour chaque objet des triplets, la valeur et son type.
Cependant, la conformité n’est pas idéale et les formats SQL ne correspondent pas précisément aux formats RDF. Pire : le « casting » d’une valeur vers un type string est ici implicite. Ce n’est pas acceptable.
Aussi, nous allons créer dans notre base une nouvelle table qui comportera les colonnes, leur valeur de prédicat et leur valeur d’objet (avec le type) pour chaque table à sérialiser. Si une colonne n’est pas définie dans cet outil de démarrage d’une ontologie, alors elle ne sera jamais reproduite dans la vue RDF – logiquement.
Voici la définition d’une telle table, appelée « ontologies » :
-- phpMyAdmin SQL Dump
-- version 4.8.2
-- https://www.phpmyadmin.net/
--
-- Hôte : localhost
-- Généré le : Dim 11 août 2019 à 13:44
-- Version du serveur : 10.3.13-MariaDB-2
-- Version de PHP : 7.2.19-0ubuntu0.19.04.1
SET
sql_mode =
"NO_AUTO_VALUE_ON_ZERO"
;SET
autocommit =
0
;START
TRANSACTION
;SET
time_zone =
"+00:00"
;
--
-- Base de données : `rdfxml`
--
-- --------------------------------------------------------
--
-- Structure de la table `ontologies`
--CREATE TABLE `ontologies`
(
`onto_id`
int
(
11
)
NOT
NULL
auto_increment
PRIMARY
KEY
,
`onto_table`
varchar
(
255
)
NOT
NULL
,
`onto_orga`
int
(
3
)
NOT
NULL
,INSERT
INTO
`ontologies`
(
`onto_table`
,
`onto_orga`
,
`onto_colonne`
,
`onto_predicat`
,
`onto_masque`
)
VALUES
(
'factures'
,
1
,
'facture_id'
,
'a'
,
'refSQL:factures#facture_id=%s'
)
;INSERT
INTO
`ontologies`
(
`onto_table`
,
`onto_orga`
,
`onto_colonne`
,
`onto_predicat`
,
`onto_masque`
)
VALUES
(
'factures'
,
2
,
'facture_emission'
,
'facture:date#emission'
,
'\"%s\"^^rdf:date'
)
;INSERT
INTO
`ontologies`
(
`onto_table`
,
`onto_orga`
,
`onto_colonne`
,
`onto_predicat`
,
`onto_masque`
)
VALUES
(
'factures'
,
3
,
'facture_reglement'
,
'facture:date#reglement'
,
'\"%s\"^^rdf:date'
)
;INSERT
INTO
`ontologies`
(
`onto_table`
,
`onto_orga`
,
`onto_colonne`
,
`onto_predicat`
,
`onto_masque`
)
VALUES
(
'factures'
,
4
,
'client_id'
,
'client#id'
,
'refSQL:clients#client_id=%s'
)
;INSERT
INTO
`ontologies`
(
`onto_table`
,
`onto_orga`
,
`onto_colonne`
,
`onto_predicat`
,
`onto_masque`
)
VALUES
(
'clients'
,
1
,
'client_id'
,
'a'
,
'refSQL:clients#client_id=%s'
)
;INSERT
INTO
`ontologies`
(
`onto_table`
,
`onto_orga`
,
`onto_colonne`
,
`onto_predicat`
,
`onto_masque`
)
VALUES
(
'clients'
,
2
,
'client_nom'
,
'foaf:familyName'
,
'\"%s\"^^rdf:string'
)
;
--
-- Index pour les tables déchargées
--
--COMMIT;
Vous devriez avoir une table avec les valeurs suivantes :
Notez la colonne « onto_orga » : elle permet de « classer » les triplets, mais surtout de déterminer le poids des colonnes. Dans ma logique, la valeur 1 dans cette colonne pour chaque table équivaut à la colonne de la clef primaire pour la table à sérialiser.
La valeur de prédicat est ici répétée. Nous aurions pu aussi faire des tables « prédicats » et « masque » pour éviter les répétitions de types – mais j’ai préféré la simplicité. Sachez simplement qu’ici, des jointures pour éviter les doublons seraient préférables.
Voyons maintenant la procédure de sérialisation modifiée et son exécution :
DROP
PROCEDURE
IF
EXISTS
`serialiser`
;
DELIMITER
|
CREATE
PROCEDURE
`serialiser`
(
IN
table_nom varchar
(
255
)
,
IN
table_colref varchar
(
255
)
,
IN
debug boolean
)
BEGINDECLARE stop_curseur_colonnes INT
default
0
;
DECLARE
colonne_orga INT
(
3
)
;
DECLARE
colonne_nom VARCHAR
(
255
)
;
DECLARE
colonne_relation VARCHAR
(
255
)
;
DECLARE
colonne_masque VARCHAR
(
255
)
;
DECLARE
`argument_vide`
condition
FOR
sqlstate
'45000'
;
DECLARE
curseur_colonnes CURSOR
FOR
SELECT
onto_orga,
onto_colonne,
onto_predicat,
onto_masque
FROM
ontologies
WHERE
onto_table =
table_nom
ORDER
BY
onto_orga ASC
;
DECLARECONTINUE handler
FOR
sqlstate
'02000'
SET
stop_curseur_colonnes =
1
;
DECLARE
EXIT
HANDLER
for
sqlexceptionBEGIN
SELECT
Concat
(
"erreur : la sérialisation a échoué ('"
,
@requete, "')."
)
;
END
;
IF
table_nom =
""
then
SET
@requete =
"le nom de la table est vide"
;
SIGNAL
`argument_vide`
;
ENDIF;
IF
table_nom =
""
then
SET
@requete =
"le nom de la colonne identifiante est vide"
;
SIGNAL
`argument_vide`
;
ENDIF;
OPEN
curseur_colonnes;
REPEATFETCH curseur_colonnes INTO
colonne_orga,
colonne_nom,
colonne_relation,
colonne_masque;
IF
colonne_orga =
1
then
SET
@refSQL =
concat
(
'REPLACE( \''
, colonne_masque, '\', "%s", '
,
table_colref, ' ) '
)
;
SET
@requete =
Concat
(
' SELECT '
, @refSQL, ' AS sujet, "'
,
colonne_relation, '" AS predicat, "ontoSQL:'
,
table_nom, '" AS objet FROM '
,
table_nom
)
;
ENDIF;
IF
colonne_orga >
1
AND
stop_curseur_colonnes =
0
then
SET
@requete =
concat
(
@requete, ' UNION ALL SELECT '
, @refSQL,
', "'
, colonne_relation,
'", REPLACE( \''
, colonne_masque,
'\', "%s", IFNULL( CAST( '
, colonne_nom,
' AS CHAR CHARACTER SET utf8 ), "" ) ) FROM '
,
table_nom
)
;
ENDIF;
UNTIL
stop_curseur_colonnes END
repeat
;
IF
debug IS
false
THEN
SET
@requetePrepare =
concat
(
'DROP VIEW IF EXISTS `rdf_'
, table_nom,
'`;'
)
;
PREPARE
stmt
FROM
@requetePrepare;
EXECUTE
Stmt;
SET
@requeteVue =
Concat
(
' CREATE VIEW `rdf_'
, table_nom,
'` AS '
, @requete, ' ; '
)
;
PREPARE
stmt
FROM
@requeteVue;
EXECUTE
Stmt;
SET
@requeteSelection =
Concat
(
' SELECT * FROM `rdf_'
, table_nom,
'` ; '
)
;
PREPARE
stmt
FROM
@requeteSelection;
EXECUTE
Stmt;
DEALLOCATE
prepare
stmt;
ENDIF;
IF
debug IS
true
THEN
SELECT
@requete;
ENDIF;
END
|
delimiter
;
CALL
`serialiser`
(
"factures"
, "facture_id"
, false
)
;
Votre retour devrait être similaire à ceci :
La vue pour les factures « rdf_facture » devrait être mise à jour comme suit :
Pensez à exécuter la sérialisation pour la table « clients » :
CALL
`serialiser`
(
"clients"
, "client_id"
, false
)
 ;
IV-E. La jointure : réalisation▲
Nous avons maintenant deux vues, l’une pour la table « factures » et l’autre pour la table « clients ». Pour l’amusement, vous pouvez aussi produire une vue de la table « ontologies », en pensant à ajouter les lignes correspondantes…
Nous allons produire maintenant une vue des vues disponibles, qui nous sera comme « base RDF » :
CREATE
view
`rdf`
AS
SELECT
*
FROM
rdf_clients
UNION
ALL
SELECT
*
FROM
rdf_factures;
Ainsi, nous avons désormais une vue unique « rdf » qui peut être appelée et requêtée, avec seulement les colonnes sélectionnées dans la table « ontologies » et une grammaire respectueuse de RDF :
Vous pouvez vérifier en modifiant une valeur : votre vue basée sur une table SQL qui pourrait être en production est tout à fait conforme et reprend bien les valeurs d’origine.
Notre prochaine étape sera une jointure afin de récupérer la facture n°1 avec les informations disponibles au format RDF du client.
Testez une première requête pour saisir le principe que je vais développer après :
SELECT
*
FROM
rdf
WHERE
(
sujet LIKE
'refSQL:clients#client_id=1%'
OR
objet LIKE
'refSQL:clients#client_id=1%'
)
;
Le résultat montre bien quelques informations qui correspondent à notre facture :
Pour renvoyer toutes les informations disponibles, il faut comprendre que la « jointure » se fera en prenant l’identifiant RDF de la facture et l’identifiant RDF du client. Et éventuellement des autres tables qui pourraient être concernées…
Voici une telle requête, qui pourrait facilement être incorporée dans une autre procédure stockée ou une fonction :
SET
@refSQL =
"refsql:factures#facture_id=1"
;
SET
@refJointure =
"client#id"
;
SELECT
*
FROM
rdf
WHERE
sujet =
@refSQL
UNION
ALL
SELECT
*
FROM
rdf
WHERE
sujet =
(
SELECT
objet
FROM
rdf
WHERE
sujet LIKE
@refSQL
AND
predicat LIKE
@refJointure)
;
La référence de la jointure entre les factures et les clients est donc un prédicat hérité de notre ontologie : client#id (c’est-à -dire : « la propriété Identifiant du concept de Client »). Son exécution nous renvoie parfaitement les données disponibles suivant ce qui a été spécifié dans la table « ontologies » :
IV-F. La question des performances et de la sécurité ?▲
Pour la partie « sécurité », vous pouvez ajuster les droits afin de faire correspondre les vues produites à un profil particulier, qui n’aura donc que l’accès (en lecture) aux données qui correspondent à ce qui est défini dans la table « ontologies » - c’est donc plutôt un progrès, car vous choisissez très finement l’accès, colonne par colonne, pour chaque table sérialisée.
De plus, si vous modifiez l’organisation de vos tables, l’appel à la fonction de sérialisation modifie à la volée votre vue RDF de la table – c’est donc faisable « à chaud » avec une gestion intelligente des verrous.
Sur la question des performances, lors que les jointures sont nombreuses et les tables importantes, la notion de produits cartésiens – une épreuve mortelle pour votre base dans certains cas – se traite ici différemment. À proprement parler, il n’y a pas de produit cartésien, car nous ne sommes que sur une imbrication de requêtes de sélection. Parler de jointure serait donc impropre, mais l’idée, le résultat, c’est d’avoir les mêmes informations disponibles que dans une jointure SQL classique.
Enfin, l’usage « union all » évite un traitement des doublons (peu importe leur existence). L’usage des vues évite le peuplement de tables temporaires. Ce que je propose ici est donc probablement l’une des voies les plus efficaces pour sérialiser des tables en SQL vers des triplets RDF exploitables par un moteur dédié, en utilisant les possibilités offertes nativement par SQL.
Reste que cet usage n’est pas recommandé pour un grand nombre de données, car la génération de la vue n’est pas neutre en termes de délai (jusqu’à plusieurs secondes).
Cependant, il reste des points d’amélioration, en jouant justement sur des tables de résultats. Ainsi, des tests avec les requêtes sur un ensemble de données plus massif (145 k entrées, avec une dizaine de colonnes) renvoient des milliers de lignes (145 k triplets pour chaque colonne de la table SQL…).
Le temps de traitement sur la vue, sans jointure, avoisine alors 4.5 secondes (au lieu de 0.26 seconde avec une requête simple) et n’évolue pas, quel que soit le nombre de traitements ou de recherches appliqués. Dans la plupart des cas, c’est un délai inacceptable.
Une idée pour optimiser cela est l’utilisation d’une table pour stocker les données extraites (ce qui en réduit souvent le nombre), les sérialiser, puis les renvoyer (attention, la base est différente de celle indiquée plus haut, pour l’exemple) :
CREATE
TABLE
vers_rdfSELECT *
FROM
`lexique`
WHERE
`lemme`
=
'zyeuter'
; -- cette requête retourne 8 éléments de la table d'origine "lexique"CREATE VIEW rdf_vers_rdf AS
SELECT
Replace
(
'refSQL:lexique#lexique_id=%s'
, "%s"
, lexique_id )
AS
sujet,
"a"
AS
predicat,
"ontoSQL:lexique"
AS
objet
FROM
vers_rdf
UNION
ALL
SELECT
Replace
(
'refSQL:lexique#lexique_id=%s'
, "%s"
, lexique_id )
,
"lexique:orthographe"
,
replace
(
'"%s"^^rdf:string'
, "%s"
, ifnull
(
cast
(
ortho AS
char
characterSET utf8 )
, ""
)
)
FROM
vers_rdf
UNION
ALL
select
replace
(
'refSQL:lexique#lexique_id=%s'
, "%s"
, lexique_id )
,
"lexique:lemme"
,
replace
(
'"%s"^^rdf:string'
, "%s"
, ifnull
(
cast
(
lemme AS
char
characterSET utf8 )
, ""
)
)
FROM
vers_rdf
UNION
ALL
select
replace
(
'refSQL:lexique#lexique_id=%s'
, "%s"
, lexique_id )
,
"lexique:type"
,
replace
(
'"%s"^^rdf:string'
, "%s"
, ifnull
(
cast
(
cgram AS
char
characterSET utf8 )
, ""
)
)
FROM
vers_rdf ; -- cette requête peut être générée par une procédure, en utilisant les briques de la procédure "sérialiser" déjà indiquéeSELECT *
FROM
rdf_vers_rdf ; -- retourne les résultatsDROP TABLE vers_rdf;DROP VIEW rdf_vers_rdf;
L’ensemble des délais cumulés pour ces opérations, tombe alors à 0.2844 seconde, proche d’une requête SQL classique, en gardant la souplesse de RDF.
Si vous avez encore des doutes sur les performances globales du modèle RDF (au-delà du cas que j’évoque ici, qui s’apparente à un cas d’étude), je vous invite à lire l’introduction sur le Web Data du W3C :
Data is increasingly important to society and W3C has a mature suite of Web standards with plans for further work on making it easier for average developers to work with graph data and knowledge graphs. Linked Data is about the use of URIs as names for things, the ability to dereference these URIs to get further information and to include links to other data. There are ever increasing sources of Linked Open Data on the Web, as well as data services that are restricted to the suppliers and consumers of those services.
|
Les données sont de plus en plus importantes pour la société et le W3C dispose d'une suite de normes Web bien préparée, qui devrait permettre aux développeurs moyens de travailler plus facilement avec les données graphiques et les graphiques de connaissances. Les données liées concernent l'utilisation des URI en tant que noms d'objets, la possibilité de déréférencer ces URI pour obtenir des informations supplémentaires et d'inclure des liens vers d'autres données. Il existe de plus en plus de sources de données ouvertes liées sur le Web, ainsi que de services de données réservés aux fournisseurs et aux consommateurs de ces services.
|
V. Conclusions▲
Ce que j’explique ici est une première approche de ce que peut faire une application de SQL pour la production RDF dans des bases relationnelles.
Pour tester des scénarios et des développements, ou une production sémantique sur peu de tables ou peu de données par table, une telle solution suffit largement.
Si vous connaissez de meilleures méthodes que celle que j’ai démontrée ici, discutons-en sur le forum !
À très bientôt.
VI. Note de la rédaction de Developpez.com▲
Nous tenons à remercier escartefigue pour la relecture orthographique de ce tutoriel.