Manuel: Accès à la base de données

This page is a translated version of the page Manual:Database access and the translation is 100% complete.
Other languages:
Deutsch • ‎English • ‎Nederlands • ‎español • ‎français • ‎lietuvių • ‎português • ‎čeština • ‎русский • ‎中文 • ‎日本語

Cet article donne un aperçu de l'accès aux bases de données et des problèmes généraux liés à ces bases dans MediaWiki.

Lorsque vous codez dans MediaWiki, vous accèdez normalement à la base de données uniquement par les fonctions dédiées de MediaWiki.

Schéma des bases de données

Pour les informations concernant le schéma de la base de données MediaWiki, telles que la description des tables et leur contenu, voir Schéma de base de données . Historiquement dans MediaWiki, ceci était aussi documenté dans maintenance/tables.sql, néanmoins à partir de MediaWiki 1.35, cela sera déplacé progressivement dans maintenance/tables.json comme partie de l'initiative du schéma abstrait (Abstract Schema initiative). Cela signifie que maintenance/tables.json est transformé en maintenance/tables-generated.sql par un maintenance script , ce qui le rend plus facile à générer les fichiers du schéma pour prendre en charge les différents moteurs de base de données.

Connexion à MySQL

Utiliser sql.php

MediaWiki fournit un script de maintenance pour accéder à la base de données. Exécutez depuis le répertoire maintenance :

php sql.php

Vous pouvez ensuite émettre des requêtes vers la base de données. Vous pouvez aussi fournir un nom de fichier que MediaWiki exécutera, en substituant les variables spéciales de MediaWiki selon le cas. Pour plus d'informations, voir Manuel:Sql.php .

Cela fonctionne avec tout serveur de base de données. Néanmoins l'invite n'a pas toutes les fonctionnalités du client de commandes en mode ligne fourni avec votre base de données.

Utiliser le client de commandes en ligne mysql

## Paramètres de la base de données
$wgDBtype           = "mysql";
$wgDBserver         = "localhost";
$wgDBname           = "your-database-name";
$wgDBuser           = "your-database-username";  // Default: root
$wgDBpassword       = "your-password";

Votre nom d'utilisateur et mot de passe MySQL se trouvent dans le fichier LocalSettings.php, par exemple :

Avec SSH, connectez-vous ainsi :

mysql -u <$wgDBuser> -p --database=<$wgDBname>

en remplaçant <$wgDBuser> et <$wgDBname> par l'information contenue dans le fichier LocalSettings.php . On vous demandera ensuite votre mot de passe $wgDBpassword avant d'afficher l'invite mysql>.

Niveau d'abstraction de la base de données

MediaWiki fournit un niveau d'abstraction de la base de données. Sauf si vous travaillez sur le niveau d'abstraction, vous n'avez en aucun cas à appeler directement les fonctions PHP de la base de données (telles que mysql_query() ou pg_send_query().)

Le niveau d'abstraction est accessible via la classe Wikimedia\Rdbms\Database . Une instance de cette classe peut être obtenue en appelant getConnectionRef() (de préférence) ou getConnection() sur un ILoadBalancer injecté . La fonction wfGetDB() est en fin de vie et ne doit pas être utilisée dans le nouveau code. Une autre possibilité est typiquement d'être appelé avec un seul paramètre, qui est la constante DB_REPLICA (pour les demandes de lecture) ou DB_PRIMARY (pour les demandes d'écriture et les lectures qui ont besoin absolument de l'information la plus récente). La distinction entre master et réplicat est importante dans un environnement multi-bases, tel que Wikimedia. Voir la section des fonctions conteneur ci-après pour connaître les possibilités d'utilisation de l'objet Database qui est renvoyé.

Les conteneurs de résultat des requêtes select sont des tableaux dont les clés sont des entiers qui commencent à 1. Pour faire une demande de lecture, le code suivant suffit habituellement :

$lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
...
$dbr = $lb->getConnectionRef( DB_REPLICA );
$res = $dbr->select( /* ...see docs... */ );
foreach( $res as $row ) {
	...
}

Pour une requête d'écriture, utiliser un code similaire à :

$dbw = $lb->getConnectionRef( DB_PRIMARY );
$dbw->insert( /* ...see docs... */ );

Nous utilisons la convention $dbr pour la lecture et $dbw pour l'écriture afin de vous aider à suivre le cas où l'objet base de données est un réplicat (lecture seule) ou un master (lecture/écriture). Si vous écrivez dans un replicat, le monde risque d'exploser. Ou pour être plus précis, la requête suivante d'écriture qui a réussi sur le master peut échouer lors de la duplication sur le réplicat parce que la clé est déja définie. La duplication sur le réplicat va s'arrêter et la réparation de la base de données peut prendre des heures avant que celle-ci ne remonte en ligne. En initialisant read_only dans my.cnf pour le réplicat, vous éviterez ce scenario, mais étant donné les conséquences désastreuses, nous préférons avoir autant de contrôles que possible.

Fonctions conteneur

Nous fournissons une fonction query() pour le SQL brut, mais les fonctions conteneur telles que select() et insert() sont habituellement plus pratiques. Elles peuvent prendre en compte des éléments tels que les préfixes de tables et l'échappement des caractères dans certaines situations. Si vous devez réaliser votre propre SQL, veuillez lire la documentation concernant tableName() et addQuotes(). Les deux vous seront nécessaires. Gardez à l'esprit que le fait de ne pas utiliser addQuotes() correctement peut entraîner des failles importantes dans la sécurité de votre wiki.

Une autre raison importante pour utiliser les méthodes de haut niveau plutôt que de construire vos propres requêtes est pour vous assurer que votre code s'exécutera correctement indépendamment du type de base de données. Actuellement MySQL et MariaDB sont les mieux prises en charge. SQLite bénéficie également d'un bon support mais il est beaucoup plus lent que MySQL et MariaDB. Le support de PostgreSQL existe également mais il n'est pas aussi stable que pour MySQL.

Dans la suite, nous listons les fonctions conteneur disponibles. Pour une description détaillée des paramètres des fonctions conteneur, voir les documents concernant la classe Database . Voir en particulier Database::select pour l'explication des paramètres suivants, utilisés dans les autres fonctions conteneur : $table, $vars, $conds, $fname, $options, et $join_conds .

Les paramètres $table, $vars, $conds, $fname, $options, et $join_conds NE doivent PAS valoir null ni false (cela fonctionnait jusque la version 1.35) mais être égaux à la chaîne vide '' ou au tableau vide [].
function select( $table, $vars, $conds = '', $fname = 'Database::select', $options = [], $join_conds = [] );
function selectField( $table, $var, $cond = '', $fname = __METHOD__, $options = [] );
function selectRow( $table, $vars, $conds = '', $fname = 'Database::select', $options = [] );
function insert( $table, $a, $fname = 'Database::insert', $options = [] );
function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect', $insertOptions = [], $selectOptions = [] );
function update( $table, $values, $conds, $fname = 'Database::update', $options = [] );
function delete( $table, $conds, $fname = 'Database::delete' );
function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' );
function buildLike(/*...*/);

Fonction conteneur : select()

La fonction select() fournit l'interface MediaWiki pour une instruction SELECT. Les composants de l'instruction SELECT sont codés comme les paramètres de la fonction select(). Exemple :

$dbr = $lb->getConnectionRef( DB_REPLICA );
$res = $dbr->select(
	'category',                              // $table The table to query FROM (or array of tables)
	[ 'cat_title', 'cat_pages' ],            // $vars (columns of the table to SELECT)
	'cat_pages > 0',                         // $conds (The WHERE conditions)
	__METHOD__,                              // $fname The current __METHOD__ (for performance tracking)
	[ 'ORDER BY' => 'cat_title ASC' ]        // $options = []
);

Cet exemple correspond à la requête :

SELECT cat_title, cat_pages FROM category WHERE cat_pages > 0 ORDER BY cat_title ASC

Les JOINs sont également possibles; par exemple :

$res = $dbw->select(
	[ 'watchlist', 'user_properties' ],
	[ 'wl_user' ],
	[
		'wl_user != 1' ,
		'wl_namespace' => '0',
		'wl_title' => 'Main_page',
		'up_property' => 'enotifwatchlistpages',
	],
	__METHOD__,
	[],
	[
		'user_properties' => [ 'INNER JOIN', [ 'wl_user=up_user' ] ]
	]
);

Cet exemple correspond à la requête :

SELECT wl_user FROM `watchlist` INNER JOIN `user_properties` ON ((wl_user=up_user)) WHERE (wl_user != 1) AND wl_namespace = '0' AND wl_title = 'Main_page'
AND up_property = 'enotifwatchlistpages'

Extension:OrphanedTalkPages fournit un exemple sur la manière d'utiliser les alias de tables dans les requêtes.

Les arguments sont soit des valeurs uniques (comme 'category' ou 'cat_pages > 0'), soit des tableaux si plus d'une valeur est fournie pour une position d'argument (tel que ['cat_pages > 0', $myNextCond]). Si vous passez des chaînes de caractères dans le troisième ou le cinquième argument, vous devez protéger manuellement vos valeurs avec Database::addQuotes() au fur et à mesure que vous construisez la chaîne car le conteneur ne le fait pas pour vous. Les valeurs des noms de tables (premier argument) ou celles des noms de champs (second argument) ne doivent pas être contrôlées par l'utilisateur. La construction du tableau pour $conds est un peu limitée; elle ne peut traiter que l'égalité et les relations IS NULL (c'est à dire où WHERE key = 'value').

Vous pouvez accéder séparément à chaque ligne du résultat en utilisant une boucle foreach. Une fois l'objet ligne obtenu, vous pouvez utiliser l'opérateur -> pour accéder à un champ particulier. Un exemple complet serait :

$dbr = $lb->getConnectionRef( DB_REPLICA );
$res = $dbr->select(
	'category',                              // $table
	[ 'cat_title', 'cat_pages' ],            // $vars (columns of the table)
	'cat_pages > 0',                         // $conds
	__METHOD__,                              // $fname = 'Database::select',
	[ 'ORDER BY' => 'cat_title ASC' ]        // $options = []
);        
$output = '';
foreach( $res as $row ) {
        $output .= 'Category ' . $row->cat_title . ' contains ' . $row->cat_pages . " entries.\n";
}

Ce qui ajoutera une liste alphabétique des catégories (avec respectivement leur nombre d'entrées) à la variable $output. Si la sortie se fait en HTML, assurez-vous d'échapper les valeurs de la base de données avec htmlspecialchars()

Fonctions pratiques

Version de MediaWiki :
1.30

Pour être compatible avec PostgreSQL, les IDs d'insertion sont obtenus en utilisant nextSequenceValue() et insertId(). Le paramètre pour nextSequenceValue() peut être obtenu par l'option CREATE SEQUENCE dans maintenance/postgres/tables.sql et suit toujours le format x_y_seq, avec pour x le nom de la table (c'est à dire celui de la page) et pour y la clé primaire (c'est à dire le page_id), pour donner page_page_id_seq. Par exemple :

$id = $dbw->nextSequenceValue( 'page_page_id_seq' );
$dbw->insert( 'page', [ 'page_id' => $id ] );
$id = $dbw->insertId();

Pour d'autres fonctions utiles comme affectedRows(), numRows(), etc., voir Manual:Database.php.

Optimisation de la requête de base

Les développeurs MediaWiki qui doivent écrire des requêtes dans une base de données doivent avoir certaines notions concernant les bases de données et être conscients des problèmes de performance qu'elles peuvent poser. Les correctifs contenant des fonctions particulièrement lentes ne seront pas acceptés. Les requêtes non indexées ne sont généralement pas les bienvenues dans MediaWiki, sauf dans les pages spéciales dérivées de QueryPage. Un piège général pour les nouveaux développeurs c'est de soumettre un code contenant des requêtes SQL qui examinent un trop grand nombre de lignes. Pensez que COUNT(*) vaut O(N); compter le nombre de lignes dans une table c'est comme compter des graines dans un seau.

Compatibilité arrière

Souvent et à cause des modifications dans la structure de la base de données, différents accès à la base de données sont nécessaires pour assurer la compatibilité arrière. Ceci peut être réalisé par exemple à l'aide des variables globales $wgDBprefix et $wgVersion .

$res = WrapperClass::getQueryFoo();

class WrapperClass {

	public static function getQueryFoo() {
		global $wgDBprefix, $wgVersion;

		$param = '';
		if ( version_compare( $wgVersion, '1.33', '<' ) ) {
			$param = self::getQueryInfoFooBefore_v1_33( $wgDBprefix );
		} else {
			$param = self::getQueryInfoFoo( $wgDBprefix );
		}

		return = $dbw->select(
			$param['tables'],
			$param['fields'],
			$param['conds'],
			__METHOD__,
			$param['options'],
			$param['join_conds'] );
	}

	private static function getQueryInfoFoo( $prefix ) {
		return [
			'tables' => [ 'table1', 'table2', 'table3' ],
			'fields' => [
				'field_name1' => $prefix . 'table1.field1',
				'field_name2' => 'field2',
				
			],
			'conds' => [ 
			],
			'join_conds' => [
				'table2' => [
					'INNER JOIN',
					
				],
				'table3' => [
					'LEFT JOIN',
					
				]
			],
			'options' => [ 
			]
		];
	}

	private static function getQueryInfoFooBefore_v1_33( $prefix ) {
		return [
			'tables' => [ 'table1', 'table2', 'table3_before' ],
			'fields' => [
				'field_name1' => $prefix . 'table1.field1',
				'field_name2' => 'field2_before',
				
			],
			'conds' => [ 
			],
			'join_conds' => [
				'table2' => [
					'INNER JOIN',
					
				],
				'table3_before' => [
					'LEFT JOIN',
					
				]
			],
			'options' => [ 
			]
		];
	}
}

Réplication

Les installations étendues de MediaWiki telles que Wikipedia, utilisent un vaste ensemble de serveurs MySQL répliqués qui dupliquent chacuns les écritures faites sur le serveur MySQL maître. Il est important de comprendre les problèmes liés à ce paramètre si vous souhaitez écrire du code pour Wikipedia.

C'est souvent le cas où le meilleur algorithme à utiliser pour une tâche donnée dépend du fait que la réplication est utilisée ou pas. En raison de notre centrisme Wikipédia sans vergogne, nous utilisons souvent la version de réplication sympatique, mais si vous le souhaitez, vous pouvez utiliser wfGetLB()->getServerCount() > 1 pour savoir si la réplication est utilisée.

Latence

La latence (c'est à dire le retard ou encore le décalage) apparaît principalement lorsqu'un grand nombre d'écritures est envoyé au master. Les écritures sur le master sont exécutées en parallèle mais sont faites en série quand elles sont dupliquées sur les réplicats. Le master enregistre la reqête dans le journal (binlog) quand la transacton est validée (commit). Les réplicats scrutent le binlog et démarrent l'exécution de la requête dès qu'elle apparaît. Ils peuvent répondre aux demandes de lecture en même temps qu'ils traitent une requête d'écriture mais ils ne lisent plus rien dans le binlog et ne traitent donc pas d'autres écritures. Cela signifie que si la demande d'écriture a besoin de temps pour s'exécuter, les réplicats vont attendre le master, le temps qu'il termine sa demande d'écriture.

La latence peut s'allonger quand la charge due aux lectures trop nombreuses augmente. Le partage de charge dans MediaWiki arrête d'envoyer les lectures au réplicat lorsque ce décalage dépasse 30 secondes. Si les seuils de charge sont mal positionnés, ou s'il y a trop de charge habituellement, cela peut faire qu'un réplicat soit toujours décalé d'environ 30 secondes.

Si tous les réplicats sont décalés de plus de 30 secondes (selon la valeur de $wgDBservers ), MediaWiki arrête d'écrire dans la base de données. Toutes les modifications et autres opérations d'écriture sont refusées et une erreur est renvoyée à l'utilisateur. Ceci laisse aux réplicats une chance de se mettre à jour. Avant que nous ayons ce mécanisme, les réplicats étaient régulièrement à la traîne de quelques minutes, ce qui rendait difficile la relecture des modifications récentes.

De plus, MediaWiki essaie de respecter l'ordre chronologique des événements arrivant sur le wiki pour qu'ils soient vus dans l'ordre par l'utilisateur. Il est acceptable d'avoir quelques secondes de décalage, tant que l'utilisateur perçoit une image cohérente suite aux requêtes consécutives. Ceci est réalisé en indiquant l'emplacement du binlog du master dans la session, puis au début de chaque requête en attendant que le replicat s'accroche à cette position avant d'aller y lire. Si ce délai expire, les lectures sont autorisées mais la requête est considérée être en mode réplicat différé. Le mode réplicat différé peut être vérifié en appelant wfGetLB()->getLaggedReplicaMode(). La seule conséquence pratique actuellement est l'affichage d'un avertissement au bas de la page.

Les utilisateurs du shell peuvent voir le temps de réplication avec getLagTimes.php ; les autres peuvent utiliser l'API siteinfo .

Les bases de données ont souvent aussi leur propre système de contrôle en place, voir par exemple pour MariaDB (Wikimedia) et sur Toolforge (VPS Wikimedia Cloud).

Pour éviter la latence

Pour éviter les délais excessifs, les requêtes demandant un grand nombre d'écritures doivent être découpées en paquets plus petits (en général une écriture par ligne). Les requêtes multilignes INSERT ... SELECT sont les pires situations et doivent toutes être évitées. Au lieu de cela, faites d'abord le select puis l'insert.

Même les petites écritures peuvent provoquer une attente si leur fréquence est trop grande et que la réplication n'a pas le temps de les satisfaire correctement. Cela arrive le plus souvent dans les scripts de maintenance. Pour empêcher cela vous devez appeler LBFactory::waitForReplication() régulièrement après quelques centaines d'écritures. La plupart des scripts rendent le nombre exact configurable :

class MyMaintenanceScript extends Maintenance {
    public function __construct() {
        // ...
        $this->setBatchSize( 100 );
    }

    public function execute() {
        $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
        $limit = $this->getBatchSize();
        while ( true ) {
             // ...sélectionnez jusqu'à $limit lignes à écrire, arrêtez la boucle quand il y en plus...
             // ...faites les écritures...
             $lbFactory->waitForReplication();
        }
    }
}

Travailler avec la latence

Malgré tous nos efforts, il n'est pas facile de garantir un environnement sans latence. Le délai de réplication est habituellement de moins d'une seconde mais peut exceptionnellement s'étaler jusqu'à 30 secondes. Pour l'évolutivité, il est très important de maintenir la charge sur le master à un niveau bas, donc le fait d'envoyer simplement toutes vos requêtes au maître n'est pas une solution. Donc si vous avez absolument besoin de données à jour, nous vous proposons l'approche suivante :

  1. faites une requêtes rapide au master pour avoir un numéro de séquence ou une référence horaire
  2. exécutez la requête complète sur le réplicat et vérifiez si elle correspond aux données reçues du master
  3. si ce n'est pas le cas, exécutez la requête complète sur le master

Pour éviter de surcharger le master à chaque fois que les réplicats sont en retard, l'utilisation de cette approche doit être réduite au maximum. Dans la plupart des cas, lisez simplement sur le réplicat et laissez l'utilisateur gérer le retard.

Etreinte fatale

A cause de la fréquence très rapide des écritures sur Wikipedia (et sur certains autres wikis), les développeurs MediaWiki doivent faire très attention à structurer leurs écritures de sorte à éviter que les blocages ne s'éternisent. Par défaut, MediaWiki ouvre une transaction lors de la première requête, puis la valide (commit) avant que la sortie ne soit envoyée. Les verrous seront maintenus à partir du moment où la requête est faite et jusqu'à la validation (commit). Par conséquent vous pouvez réduire le temps de blocage en réalisant le maximum de traitement possible avant de lancer les requêtes d'écriture. Les opérations de mise à jour qui n'ont pas besoin de la base de données peuvent être repoussées jusqu'après le commit en ajoutant un objet à $wgPostCommitUpdateList .

Souvent cette approche n'est pas idéale et il est nécessaire d'inclure de petits groupes de requêtes dans leur propre transaction. Utilisez la syntaxe suivante :

$factory = \MediaWiki\MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
$factory->beginMasterChanges(__METHOD__);
/* Faites les requêtes : */
$factory->commitMasterChanges(__METHOD__);

L'utilisation de verrous en lecture (par exemple avec l'option FOR UPDATE) n'est pas conseillée. Ils sont faiblement implémentés dans InnoDB et provoquent régulièrement des erreurs d'étreinte fatale. Il est également étonnamment facile de paralyser le wiki avec une contention de verrouillage.

Au lieu de verrouiller les lectures, combinez les vérifications d'existence dans vos requêtes d'écriture, en utilisant une condition appropriée dans la clause WHERE de UPDATE ou en utilisant des index uniques en combinaison avec INSERT IGNORE. Puis utilisez le nombre de lignes affecté pour voir si la requête s'est correctement exécutée.

Schéma de la base de données

N'oubliez pas les index quand vous concevez une base de données; il est possible que tout se passe à merveille sur votre wiki de test avec une dizaine de pages, mais que des blocages apparaissent sur le wiki réel. Voir ci-dessus pour les détails.

Pour les conventions de nommage, voir Manuel:Conventions de codage/Base de données .

Compatibilité SQLite

Lorsque vous écrivez les définitions des tables MySQL ou les correctifs de mise à jour, il est important de se rappeler que SQLite partage le schéma de MySQL, mais cela ne fonctionne que si les définitions sont écrites d'une manière particulière :

  • les clés primaires doivent se trouver dans la déclaration de la table principale, et les clés ordinaires doivent être ajoutées séparément avec CREATE INDEX :
mauvais bon
CREATE TABLE /*_*/foo (
    foo_id INT NOT NULL AUTO_INCREMENT,
    foo_text VARCHAR(256),
    PRIMARY KEY(foo_id),
    KEY(foo_text)
);
CREATE TABLE /*_*/foo (
    foo_id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
    foo_text VARCHAR(256)
) /*$wgDBTableOptions*/;

CREATE INDEX /*i*/foo_text ON /*_*/foo (foo_text);

Néanmoins les clés primaires qui s'étendent sur plusieurs champs doivent être incluses dans la définition de la table principale :

CREATE TABLE /*_*/foo (
    foo_id INT NOT NULL,
    foo_text VARCHAR(256),
    PRIMARY KEY(foo_id, foo_text)
) /*$wgDBTableOptions*/;

CREATE INDEX /*i*/foo_text ON /*_*/foo (foo_text);
/*i*/ a été supprimé dans MediaWiki 1.35, voir le remplacement des variables.
  • n'ajoutez pas plus d'une colonne par instruction :
mauvais bon
ALTER TABLE /*_*/foo
    ADD foo_bar BLOB,
    ADD foo_baz INT;
ALTER TABLE /*_*/foo ADD foo_bar BLOB;
ALTER TABLE /*_*/foo ADD foo_baz INT;
  • définissez les valeurs par défaut explicites quand vous ajoutez des colonnes NON NULLES :
mauvais bon
ALTER TABLE /*_*/foo ADD COLUMN foo_bar varchar(32) BINARY NOT NULL;
ALTER TABLE /*_*/foo ADD COLUMN foo_bar varchar(32) BINARY NOT NULL DEFAULT '';

Les contrôles de compatibilité de base peuvent être faits avec :

Ou, si vous avez besoin de tester une correction de mise à jour, avec simultanément :

  • php sqlite.php --check-syntax tables.sql (avec le nouveau tables.sql)
  • php sqlite.php --check-syntax tables.sql filename.sql
    • Parce que les correctifs de la base de données mettent à jour également le fichier tables.sql, pour celui-ci il faut passer la version de pré-validation de tables.sql (le fichier avec la définition complète de la base de données). Sinon vous pouvez obtenir une erreur si par exemple vous perdez un index (parce qu'il n'existe plus dans tables.sql car vous venez juste de le supprimer).

Ce qui précède suppose que vous êtes dans $IP/maintenance/, sinon passez le chemin complet du ficher. Pour les correctifs des extensions, utiliser l'équivalent de ces fichiers pour les extensions.

Voir aussi