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.

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 run.php sql

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 mode ligne de commandes que les clients fournis 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 utilise la bibliothèque Rdbms comme niveau d'abstraction de la base de données. Les développeurs ne doivent pas appeler directement les fonctions de bas niveau de la base de données telles que mysql_query.

Chaque connexion est représentée par Wikimedia\Rdbms\IDatabase à partir de quoi les requêtes peuvent être réalisées. Les connexions peuvent être acquises en appelant getPrimaryDatabase() ou getReplicaDatabase() (selon le cas d'utilisation) sur une instance IConnectionProvider obtenue de préférence par les dépendances, ou à partir de MediaWikiServices ou via le service DBLoadBalancerFactory La fonction wfGetDB() est en fin de vie et ne doit pas être utilisée dans le nouveau code.

Pour obtenir les connexions à la base de données, vous pouvez appeler, soit getReplicaDatabase() (pour les demandes de lecture) ou getPrimaryDatabase() (pour les demandes d'écriture et les lectures qui ont besoin absolument de l'information la plus récente). La distinction entre primaire et réplica 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'interaction avec des objets IDatabase.

Exemple de requête de lecture :

Version de MediaWiki :
1.42
use MediaWiki\MediaWikiServices;

$dbProvider = MediaWikiServices::getInstance()->getConnectionProvider();
$dbr = $dbProvider->getReplicaDatabase();

$res = $dbr->newSelectQueryBuilder()
  ->select( /* ... */ ) //  see docs
  ->fetchResultSet();

foreach ( $res as $row ) {
	print $row->foo;
}

Exemple de requête d'écriture :

Version de MediaWiki :
1.40
$dbw = $dbProvider->getPrimaryDatabase();
$dbw->insert( /* ... */ ); // see docs

Nous utilisons la convention $dbr pour les connexions de lecture (dans le réplica) et $dbw pour les connexions d'écriture (sur le primaire). $dbProvider est également utilisé pour l'instance IConnectionProvider

SelectQueryBuilder

Version de MediaWiki :
1.35

La classe SelectQueryBuilder est la manière préférée de formuler des requêtes de lecture dans le nouveau code. Dans le code plus ancien, vous pouvez trouver select() et les méthodes associées de la classe Database utilisées directement. Le constructeur de requêtes fournit une interface souple et moderne, où les méthodes sont enchaînées jusqu'à ce que la méthode de récupération soit invoquée, sans déclarer de variables intermédiaires. Par exemple :

$dbr = $dbProvider->getReplicaDatabase();
$res = $dbr->newSelectQueryBuilder()
	->select( [ 'cat_title', 'cat_pages' ] )
	->from( 'category' )
	->where( $dbr->expr( 'cat_pages', '>', 0 ) )
	->orderBy( 'cat_title', SelectQueryBuilder::SORT_ASC )
	->caller( __METHOD__ )->fetchResultSet();

Cet exemple correspond au SQL suivant :

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

Les JOINs sont également possibles; par exemple :

$dbr = $dbProvider->getReplicaDatabase();
$res = $dbr->newSelectQueryBuilder()
	->select( 'wl_user' )
	->from( 'watchlist' )
	->join( 'user_properties', /* alias: */ null, 'wl_user=up_user' )
	->where( [
		$dbr->expr( 'wl_user', '!=', 1 ),
		'wl_namespace' => '0',
		'wl_title' => 'Main_page',
		'up_property' => 'enotifwatchlistpages',
	] )
	->caller( __METHOD__ )->fetchResultSet();

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'

Vous pouvez accéder à chaque ligne individuelle du résultat en utilisant une boucle foreach. Chaque ligne est représentée par un objet. Par exemple :

$dbr = $dbProvider->getReplicaDatabase();
$res = $dbr->newSelectQueryBuilder()
	->select( [ 'cat_title', 'cat_pages' ] )
	->from( 'category' )
	->where( $dbr->expr( 'cat_pages', '>', 0 ) )
	->orderBy( 'cat_title', SelectQueryBuilder::SORT_ASC )
	->caller( __METHOD__ )->fetchResultSet();      

foreach ( $res as $row ) {
	print 'Category ' . $row->cat_title . ' contains ' . $row->cat_pages . " entries.\n";
}

Il existe aussi des fonctions pratiques pour récupérer une seule ligne, un champ particulier de une ou plusieurs lignes :

// Equivalent of:
//     $rows = fetchResultSet();
//     $row = $rows[0];
$pageRow = $dbr->newSelectQueryBuilder()
	->select( [ 'page_id', 'page_namespace', 'page_title' ] )
	->from( 'page' )
	->orderBy( 'page_touched', SelectQueryBuilder::SORT_DESC )
	->caller( __METHOD__ )->fetchRow();

// Equivalent of:
//     $rows = fetchResultSet();
//     $ids = array_map( fn( $row ) => $row->page_id, $rows );
$pageIds = $dbr->newSelectQueryBuilder()
	->select( 'page_id' )
	->from( 'page' )
	->where( [
		'page_namespace' => 1,
	] )
	->caller( __METHOD__ )->fetchFieldValues();

// Equivalent of:
//     $rows = fetchResultSet();
//     $id = $row[0]->page_id;
$pageId = $dbr->newSelectQueryBuilder()
	->select( 'page_id' )
	->from( 'page' )
	->where( [
		'page_namespace' => 1,
		'page_title' => 'Main_page',
	] )
	->caller( __METHOD__ )->fetchField();

Dans ces exemples, $pageRow est un objet ligne comme dans l'exemple de foreach ci-dessus, $pageIds est un tableau d'identifiants de page et $pageId est un identifiant de page unique.

Fonctions conteneur

Nous fournissons une fonction query() pour le SQL brut, mais les fonctions conteneur telles que select() et insert() doivent être utilisées à la place. 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 $table, $vars, $conds, $fname, $options, $join_conds utilisés par plusieurs des autres fonctions conteneur.

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, .. );
function selectField( $table, $var, $cond, .. );
function selectRow( $table, $vars, $conds, .. );
function insert( $table, $a, .. );
function insertSelect( $destTable, $srcTable, $varMap, $conds, .. );
function update( $table, $values, $conds, .. );
function delete( $table, $conds, .. );
function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, .. );

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 les Fonctions de Database.php.

Optimisation de la requête de base

Voir aussi : Database optimization

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 de la variable globale MW_VERSION (ou de la variable globale $wgVersion avant MediaWiki 1.39) :

/**
* backward compatibility
* @since 1.31.15
* @since 1.35.3
* define( 'DB_PRIMARY', ILoadBalancer::DB_PRIMARY )
* DB_PRIMARY remains undefined in MediaWiki before v1.31.15/v1.35.3
* @since 1.28.0
* define( 'DB_REPLICA', ILoadBalancer::DB_REPLICA )
* DB_REPLICA remains undefined in MediaWiki before v1.28
*/
defined('DB_PRIMARY') or define('DB_PRIMARY', DB_MASTER);
defined('DB_REPLICA') or define('DB_REPLICA', DB_SLAVE);

$res = WrapperClass::getQueryFoo();

class WrapperClass {

	public static function getReadingConnect() {
		return wfGetDB( DB_REPLICA );
	}

	public static function getWritingConnect() {
		return wfGetDB( DB_PRIMARY );
	}

	public static function getQueryFoo() {
		global $wgVersion;

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

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

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

	private static function getQueryInfoFooBefore_v1_33() {
		return [
			'tables' => [
				't1' => 'table1',
				't2' => 'table2',
				't3' => 'table3_before'
			],
			'fields' => [
				'field_name1' => 't1.field1',
				'field_name2' => 't2.field2_before',
				
			],
			'conds' => [ 
			],
			'join_conds' => [
				't2' => [
					'INNER JOIN',
					
				],
				't3' => [
					'LEFT JOIN',
					
				]
			],
			'options' => [ 
			]
		];
	}
}
Version de MediaWiki :
1.35
	public static function getQueryFoo() {

		$param = '';
		if ( version_compare( MW_VERSION, '1.39', '<' ) ) {
			$param = self::getQueryInfoFooBefore_v1_39();
		} else {
			$param = self::getQueryInfoFoo();
		}

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

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 complexités liées aux systèmes largement distribués si vous souhaitez écrire du code pour Wikipédia.

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éplica lorsque la latence dépasse 5 secondes. Si les seuils de charge sont mal choisis, ou s'il y a trop de charge habituellement, cela peut faire qu'un réplica soit toujours décalé d'environ 5 secondes.

Dans la production Wikimedia, les bases de données ont une semi-synchronisation active, ce qui signifie que les changements seront pas validés dans le primaire sauf si la validation est déja faite dans au moins la moitié des réplicas. Cela signifie qu'une surcharge liée à l'ensemble des modifications et aux écritures pourrait entraîner leur refus avec une erreur retournée à l'utilisateur. Ceci laisse aux réplicats une chance de se mettre à jour.

Avant que nous ayons ces mécanismes, les réplicas é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éplique différée peut être vérifié en appelant LoadBalancer::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 Maintenance::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() {
        $limit = $this->getBatchSize();
        while ( true ) {
             // ...sélectionnez jusqu'à $limit lignes à écrire, arrêtez la boucle quand il n'y en a plus...
             // ...faites les écritures...
             $this->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 atteindre 5 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 ou à Database::onTransactionPreCommitOrIdle.

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

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 SqliteMaintenance.php --check-syntax tables.sql - MediaWiki 1.36+
  • php sqlite.php --check-syntax tables.sql - MediaWiki 1.35 et plus ancien
    • 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