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

From mediawiki.org
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 à la base de données et des problèmes généraux qui y sont liés 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 de la base 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 facilite la génération de fichiers schéma pour prendre en charge différents moteurs de base de données.

Se connecter à 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 possèdent les clients fournis avec votre base de données.

Utiliser le client en ligne de commandes de 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 le mot de passe MySQL se trouvent dans le fichier LocalSettings.php, par exemple :

Avec SSH, connectez-vous en entrant ce qui suit :

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

en remplaçant $wgDBuser et $wgDBname par leurs valeurs dans LocalSettings.php. On vous demandera ensuite d'entrer 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 initiées 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 absolument besoin de l'information la plus récente). La distinction entre données primaires et données répliquées 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 en 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 en écriture :

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

Nous utilisons la convention $dbr pour les connexions en lecture (données répliquées) et $dbw pour les connexions en écriture (données primaires). $dbProvider est également utilisé pour l'instance IConnectionProvider

SelectQueryBuilder

Version de MediaWiki :
1.35

La classe SelectQueryBuilder est préférable dans la formulation des requêtes en lecture dans le nouveau code. Dans le code plus ancien, il est possible que select() et les méthodes associées de la classe Database soient utilisés 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éclarations intermédiaires de variable. 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 à la requête suivante :

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 séparément à chaque ligne du résultat en utilisant une boucle « foreach ». Chaque ligne est représentée comme 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 plusieurs lignes, ou un champ particulier dans une seule ligne :

// 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 d'aencapsulation

Nous fournissons une fonction « query() » pour le SQL brut, mais les fonctions d'encapsulation 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 vraiment créer votre propre code SQL, veuillez lire la documentation concernant les fonctions « 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 de vous assurer que votre code s'exécutera correctement indépendamment du type de la base de données utilisée. Actuellement MySQL et MariaDB sont les SGBDs les mieux pris en charge. SQLite bénéficie également d'un bon support mais il est beaucoup plus lent que ceux de MySQL ou MariaDB. Le support pour PostgreSQL existe également mais il n'est pas aussi stable que celui de MySQL.

Voici la liste des fonctions d'encapsulation disponibles. Pour une description détaillée des paramètres pour les fonctions d'encapsulation, voir la documentations de 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 d'encapsulation.

Les paramètres $table, $vars, $conds, $fname, $options, et $join_conds NE doivent PAS valoir null ni false (cela fonctionnait jusqu'à la version 1.35) mais doivent correspondre à 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 assurer la compatibilité avec PostgreSQL, les IDs d'insertion sont obtenues en utilisant « nextSequenceValue() » et « insertId() ». Le paramètre pour nextSequenceValue() peut être obtenu à partir de la déclaration CREATE SEQUENCE dans maintenance/postgres/tables.sql et suit toujours le format de x_y_seq, avec x comme nom de table (par exemple page) et y devenant la clé primaire (par exemple page_id), comme 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 d'une requête simple

Voir aussi : Database optimization

Les développeurs MediaWiki qui doivent écrire des requêtes de base de données doivent avoir une compréhension minimale des bases de données et surtout des problèmes de performance qu'elles impliquent. 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 importées de QueryPage. Un piège courant pour les nouveaux développeurs est de soumettre du code contenant des requêtes SQL qui examinent un trop grand nombre de lignes. N'oubliez pas que COUNT(*) équivaut à O(N) ; donc, compter le nombre de lignes dans une table, c'est comme compter les étoiles dans le ciel.

Rétrocompatibilité (ou compatibilité descendante)

Souvent (à cause de modifications dans la structure de la base de données), on devra accéder à différentes DBs pour assurer la compatibilité descendante. 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 grandes installations de MediaWiki (telles que Wikipedia), utilisent un vaste ensemble de serveurs MySQL miroirs qui dupliquent chacun les écritures faites sur le serveur primaire MySQL. 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.

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

Latence (délai)

La latence (le retard, le décalage, le délai à l'affichage des requêtes) apparaît principalement lorsqu'un grand nombre d'écritures est envoyé au serveur primaire. Les écritures sur le serveur primaire sont exécutées en parallèle, mais sont faites en série quand elles sont dupliquées sur les serveurs miroirs. Le serveur primaire enregistre la requête dans le journal des exécutions quand la transaction est validée (commit). Les serveurs miroirs scrutent le journal des exécutions et exécutent la requête dès qu'elle y apparaît. Ils peuvent assurer une requête en lecture en même temps qu'ils traitent une requête en écriture, mais ils ne lisent plus rien dans le journal et ne traitent donc plus d'autres écritures. Cela signifie que si la requête en écriture prend beaucoup de temps pour s'exécuter, les miroirs vont avoir un délai par rapport au serveur primaire, le temps que celui-ci achève la requête en écriture.

La latence peut exagérément augmenter, proportionnellement à la charge en lecture. Dans MediaWiki, la répartition de charge arrêtera l'envoi des requêtes en lecture à un miroir lorsque la latence dépasse 5 secondes. Si les seuils de charge sont mal configurés, ou s'il y a régulièrement trop de charge, cela peut amener à ce qu'un miroir soit toujours décalé d'environ 5 secondes.

Dans Wikimedia en mode production, les bases de données ont la semi-synchronisation activée, ce qui signifie qu'un changement ne sera validé sur le serveur primaire que si la validation est déja réalisée sur au moins la moitié des miroirs. Cela signifie qu'une surcharge trop importante peut entraîner le rejet des écritures avec un message d'erreur en retour à l'utilisateur. Ceci laisse aux miroirs une chance de rattraper leur retard.

Avant que nous ayons ces mécanismes, les miroirs étaient régulièrement à la traîne de plusieurs 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 cet ordre par l'utilisateur. Il est acceptable d'avoir quelques secondes de décalage, tant que l'utilisateur perçoit une image cohérente de ses 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