Jump to content

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, cela était également documenté en maintenance/tables.sql, cependant, à partir de MediaWiki 1.35, cela a progressivement été déplacé à sql/tables.json dans le cadre de l'initiative du schéma abstrait (Abstract Schema initiative). Cela signifie que le sql/tables.json est transformé en fichiers spécifiques à la plateforme, comme sql/mysql/tables-generated.sql par un maintenance script , ce qui facilite la génération de fichiers de schéma pour supporter différents moteurs de bases 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 couche d’abstraction de base de données. Les développeurs ne doivent pas appeler directement des fonctions de base de données de bas niveau, 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 des connexions à la base de données, vous pouvez demander soit getReplicaDatabase() pour les requêtes de lecture, soit getPrimaryDatabase() pour les requêtes d’écriture et de lecture en écriture informante. La distinction entre primaire et réplique est importante dans un environnement multi-bases de données, comme Wikimedia. Voir la section Fonctions d’encapsulage ci-dessous pour savoir comment interagir 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( /* ... */ ) // Voir la documentation
  ->fetchResultSet();

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

Exemple de requête en écriture :

Version de MediaWiki :
1.41
$dbw = $dbProvider->getPrimaryDatabase();
$dbw->newInsertQueryBuilder()
    ->insertInto( /* ... */ ) // Voir la documentation
    ->caller( __METHOD__ )->execute();

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). De plus, $dbProvider sont utilisés 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(); /** Pour MW < 1.40, utilise une méthode plus ancienne pour obtenir une connexion DB **/
$res = $dbr->newSelectQueryBuilder()
	->select( [ 'cat_title', 'cat_pages' ] )
	->from( 'category' )
	->where( 'cat_pages > 0' )
	->orderBy( 'cat_title', SelectQueryBuilder::SORT_ASC )
	->caller( __METHOD__ )->fetchResultSet();

Comme décrit ci-dessous, MW 1.42 introduit une méthode d’assistance, expr(), qui permet d’encapsuler le champ, l’opérateur et la valeur comme une expression. En utilisant cela, la clause where dans l’exemple ci-dessus peut être reécrite comme suit :

->where( $dbr->expr( 'cat_pages', '>', 0 ) )

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', /* sous le nom : */ null, 'wl_user=up_user' )
	->where( [
		'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( '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 :

// équivalent de :
//     $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();

// équivalent de :
//     $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();

// équivalent de :
//     $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.

Bien que vous puissiez utiliser tables() pour ajouter plusieurs tables, il est fortement recommandé d’utiliser join() ou leftJoin() à la place. Tout aliases pour des tables supplémentaires doit être ajouté à join() ou leftJoin(), et non à tables().

UpdateQueryBuilder

Version de MediaWiki :
1.41

Les déclarations SQL UPDATE doivent être faites avec le UpdateQueryBuilder .

$dbw = $this->dbProvider->getPrimaryDatabase();
$dbw->newUpdateQueryBuilder()
	->update( 'user' )
	->set( [ 'user_password' => $newHash->toString() ] )
	->where( [
		'user_id' => $oldRow->user_id,
		'user_password' => $oldRow->user_password,
	] )
	->caller( $fname )->execute();

InsertQueryBuilder

Version de MediaWiki :
1.41

Les déclarations SQL INSERT doivent être effectuées avec l'insertQueryBuilder.

$dbw = $this->dbProvider->getPrimaryDatabase();
$targetRow = [
	'bt_address' => $targetAddress,
	'bt_user' => $targetUserId,
	/* etc */
];
$dbw->newInsertQueryBuilder()
	->insertInto( 'block_target' )
	->row( $targetRow )
	->caller( __METHOD__ )->execute();
$id = $dbw->insertId();

DeleteQueryBuilder

Version de MediaWiki :
1.41

Les déclarations SQL DELETE doivent être effectuées avec le DeleteQueryBuilder.

$dbw = $this->dbProvider->getPrimaryDatabase();
$dbw->newDeleteQueryBuilder()
	->deleteFrom( 'block' )
	->where( [ 'bl_id' => $ids ] )
	->caller( __METHOD__ )->execute();
$numDeleted = $dbw->affectedRows();

ReplaceQueryBuilder

Version de MediaWiki :
1.41

Les déclarations SQL REPLACE doivent être effectuées avec ReplaceQueryBuilder.

$dbw = $this->dbProvider->getPrimaryDatabase();
$dbw->newReplaceQueryBuilder()
	->replaceInto( 'querycache_info' )
	->row( [
		'qci_type' => 'activeusers',
		'qci_timestamp' => $dbw->timestamp( $asOfTimestamp ),
	] )
	->uniqueIndexFields( [ 'qci_type' ] )
	->caller( __METHOD__ )->execute();

UnionQueryBuilder

Version de MediaWiki :
1.41

Les déclarations SQL UNION doivent être effectuées avec l'UnionQueryBuilder.

$dbr = $this->dbProvider->getReplicaDatabase();
$ids = $dbr->newUnionQueryBuilder()
	->add( $db->newSelectQueryBuilder()
		->select( 'bt_id' )
		->from( 'block_target' )
		->where( [ 'bt_address' => $addresses ] )
	)
	->add( $db->newSelectQueryBuilder()
		->select( 'bt_id' )
		->from( 'block_target' )
		->join( 'user', null, 'user_id=bt_user' )
		->where( [ 'user_name' => $userNames ] )
	)
	->caller( __METHOD__ )
	->fetchFieldValues();


Une requête affectant plusieurs lignes

Si vous devez insérer ou mettre à jour plusieurs lignes, essayez de les regrouper en une requête batch pour augmenter l’efficacité. Il est important de garder la déclaration de table (par exemple update(), insertInto(), etc.), et caller(), execute() hors boucle. Tout ce qui concerne la création ou la mise à jour de lignes peut aller dans la boucle (par exemple row()).

$queryBuilder = $this->getDb()->newInsertQueryBuilder()
	->insertInto( 'ores_classification' )
	->caller( __METHOD__ );
foreach ( [ 0, 1, 2, 3 ] as $id ) {
	$predicted = $classId === $id;
	$queryBuilder->row( [
		'oresc_model' => $this->ensureOresModel( 'draftquality' ),
		'oresc_class' => $id,
    	'oresc_probability' => $predicted ? 0.7 : 0.1,
		'oresc_is_predicted' => $predicted ? 1 : 0,
		'oresc_rev' => $revId,
	] );
}
$queryBuilder->execute();

Itération sur un grand ensemble de résultats

Si vous faites une requête à SELECT avec beaucoup de résultats, et que vous devez itérer le résultat par lots, utilisez la classe BatchRowIterator.

$queryBuilder = $dbw->newSelectQueryBuilder()
	->select( $key )
	->from( $table )
	->where( $where )
	->caller( __METHOD__ );

$iterator = new BatchRowIterator( $dbw, $queryBuilder, $key, $batchSize );
foreach ( $iterator as $n => $batch ) {
	$firstRow = reset( $batch );
	$lastRow = end( $batch );
	$dbw->newUpdateQueryBuilder()
		->table( $table )
		->set( $set )
		->where( $where )
		->andWhere( $dbw->expr( $key, '>=', $firstRow->$key ) )
		->andWhere( $dbw->expr( $key, '<=', $lastRow->$key ) )
		->caller( __METHOD__ )
		->execute();
	$this->waitForReplication();
}

Sous-requêtes

If you need to write a query that includes a subquery, use the Subquery class.

$subQuery = $db->newSelectQueryBuilder()
	->select( 'rev_id' )
	->from( 'revision' )
	->where( [ 'rev_page' => $revision->getPageId( $this->wikiId ) ] )
	->orderBy( [ 'rev_timestamp', 'rev_id' ], SelectQueryBuilder::SORT_DESC )
	->useIndex( [ 'revision' => 'rev_page_timestamp' ] )
	->limit( $searchLimit )
	->offset( 1 )
	->caller( __METHOD__ )
	->getSQL();

$candidateRevisions = $db->newSelectQueryBuilder()
	->select( [ 'slot_revision_id', 'role_name', 'content_sha1' ] )
	->from( 'slots' )
	->join( 'content', null, 'content_id = slot_content_id' )
	->join( 'slot_roles', null, 'slot_role_id = role_id' )
	->where( [ 'slot_revision_id' => new Subquery( $subQuery ) ] )
	->caller( __METHOD__ )
	->fetchResultSet();

Aides

Les méthodes d’assistance suivantes doivent être utilisées lorsque cela est approprié, car elles génèrent des requêtes SQL compatibles avec tous les types de bases de données supportées, et elles aident à l’auto-escape.

$dbr->expr()

Version de MediaWiki :
1.42

Devrait être utilisé dans les instructions WHERE chaque fois qu’une comparaison est comparée qui n’est pas une simple déclaration « égal ». Par exemple, $dbr->expr( 'ptrp_page_id', '>', $start ).

Cette méthode peut être enchaînée avec ->and() et ->or(). Par exemple, $db->expr( 'ptrp_page_id', '=', null )->or( 'ptrpt_page_id', '=', null )

$dbr->andExpr() / $dbr->orExpr()

Version de MediaWiki :
1.43

Introduced in Manual:IReadableDatabase.php to supersede makeList() with LIST_AND / LIST_OR. Example:

Before MW 1.43
$dbr->makeList([
	'foo1' => 'bar1',
	'foo2' => 'bar2',
], LIST_OR );
MW1.43+
$dbr->orExpr([
	'foo1' => 'bar1',
	'foo2' => 'bar2',
]);

$dbr->timestamp()

Les différents moteurs de base de données formatent les horodatages de MediaWiki différemment. Utilisez ceci pour assurer la compatibilité. Exemple: $dbr->expr( 'ptrp_reviewed_updated', '>', $dbr->timestamp( $time ) )

RawSQLExpression

Version de MediaWiki :
1.42

Devrait être utilisé dans les instructions WHERE quand vous ne voulez pas que SQL échappe à quoi que ce soit. Si vous comparez un champ à une valeur utilisateur (ce qui est beaucoup plus courant), utilisez plutôt $dbr->expr(). RawSQLExpression ne s’échappe pas, il ne doit donc jamais être utilisé avec une entrée utilisateur. Utilisez-les avec parcimonie ! Exemple : $dbr->expr( new RawSQLExpression( 'rc_timestamp < fp_pending_since' ) )

RawSQLValue

Version de MediaWiki :
1.43

Doit être utilisé dans les instructions WHERE quand vous ne voulez pas que SQL échappe à quoi que ce soit. Si vous comparez un champ à une valeur utilisateur (ce qui est beaucoup plus courant), utilisez plutôt $dbr->expr(). RawSQLValue ne s’échappe pas, il ne doit donc jamais être utilisé avec une entrée utilisateur. Utilisez-les avec parcimonie ! Exemple : $dbr->expr( 'fp_pending_since', '>', new RawSQLValue( $fieldName ) )

Compatibilité avec plusieurs moteurs de bases de données

Une utilisation correcte du constructeur de requêtes offrira la compatibilité avec tous les moteurs de bases de données supportés (MariaDB, MySQL, SQLite, PostgreSQL). Pour éviter les problèmes liés au moteur de base de données, les développeurs sont encouragés à écrire des tests d’intégration PHPUnit qui exercent le code de base de données. En plus de faire attention à l’utilisation de $db->expr() et $db->timestamp() comme mentionné dans les sections ci-dessus, voici quelques éléments supplémentaires à surveiller lors de l’écriture de code compatible multi-moteurs avec le constructeur de requêtes :

  • parfois, il faudra peut-être taper des entiers et des booléens dans les chaînes quand elles passent dans les BLOBs[1]
  • Certains moteurs appliquent strictement NOT NULL, donc des valeurs par défaut devront être fournies[2]
  • Parfois, il faut ajouter ->order_by(), car le tri par défaut pour chaque moteur peut être différent[3][4]

Les correctifs du noyau MediaWiki exécutent des tests sur tous les moteurs pendant l’étape d’envoi. Les extensions et les skins ne font pas de tests sur tous les moteurs. Les développeurs peuvent faire des commentaires de gerrit comme check php ou check experimental pour effectuer des vérifications d’intégration continue sur tous les moteurs de base de données avant l’étape de soumission, ou pour effectuer des vérifications d’extensions et de skins.

Fonctions d'encapsulation et requêtes brutes

Les anciens codes MediaWiki peuvent utiliser des fonctions d'encapsulation comme $dbr->select(), $dbw->insert(). Un code très ancien de MediaWiki peut utiliser $dbw->query(). Il existe aussi certaines fonctions qui génèrent du SQL brut, comme addQuotes(), buildLike(), makeList(). Aucune de ces méthodes n’est considérée comme une bonne pratique aujourd’hui, et devrait être mise à niveau vers les constructeurs de requêtes mentionnés ci-dessus si possible.

Les fonctions d'encapsulation sont supérieures à $dbw->query(), car elles peuvent gérer des choses comme les préfixes de table et les échappements dans certaines circonstances. Si vous devez vraiment créer votre propre code SQL, veuillez lire la documentation pour tableName(), addQuotes(). Vous aurez besoin des deux. Veuillez garder à l’esprit que ne pas utiliser correctement addQuotes() peut introduire de graves failles de sécurité dans 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, le meilleur support concerne MySQL/MariaDB. Il y a un bon support pour SQLite dans le cœur de MediaWiki, cependant il est beaucoup plus lent que MySQL ou MariaDB, et il n’est pas bien pris en charge dans les extensions. Il y a un support pour PostgreSQL, mais il n’est pas aussi stable que 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, $join_conds ne devraient PAS être null ou false (qui fonctionnaient jusqu’à REL 1.35) mais une chaîne vide '' ou un 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();
		}

		$dbr = self::getReadingConnect();
		return = $dbr->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',
					'field_name1 = field_name2'
				],
				'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();
		}

		$dbr = self::getReadingConnect();
		return = $dbr->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 :

<?php

namespace MediaWiki\Extension\MyExtension\Maintenance;

use MediaWiki\Maintenance\Maintenance;

$IP = getenv( 'MW_INSTALL_PATH' );
if ( $IP === false ) {
	$IP = __DIR__ . '/../../..';
}
require_once "$IP/maintenance/Maintenance.php";

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();
        }
    }
}

$maintClass = MyMaintenanceScript::class;
require_once RUN_MAINTENANCE_IF_MAIN;

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ête rapide auprès du serveur principal pour obtenir un numéro de séquence ou un horodatage
  2. Lancez la requête complète sur la réplique et vérifiez si elle correspond aux données que vous avez obtenues du serveur principal
  3. Si ce n’est pas le cas, lancez la requête complète sur le serveur principal

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 .

Voir aussi