Fermer

janvier 8, 2025

Requêtes Firestore pour SQL Minds

Requêtes Firestore pour SQL Minds


Créez rapidement une application avec Firebase, même si vous êtes intimidé par les bases de données NoSQL : la traduction de requêtes en SQL est plus facile que vous ne le pensez.

Les bases de données SQL sont assez différentes des bases de données NoSQL, vous devez donc penser un peu différemment lors de la modélisation de vos données. Cependant, la plupart des traductions du code SQL vers NoSQL sont identiques ou plus simples. Ici, nous utilisons l’API Firebase Modular, mais le firebase-admin L’API sera la même.

TL;DR

Firestore est une base de données de documents qui n’autorise pas les jointures. Vous pouvez les simuler en dupliquant vos données. Toutefois, la plupart des requêtes régulières fonctionnent comme prévu. L’absence de schéma permet de se lever et d’avancer extrêmement vite. Vous devez apprendre les modèles de modélisation des données.

Introduction

Firestore est une base de données de documents NoSQL.

Tableaux

  • Une table est une collection ou une sous-collection. Une ligne est un document.

Schéma

  • Vous devez déclarer un schéma en SQL ; vous n’en avez pas besoin en NoSQL.
  • Chaque document peut contenir différents types de données, même si cela ne serait pas utile.

Créer

Vous créez un tableau lorsque vous créez un document. Un tableau n’existe pas sans document.

Modifier

Il n’y a rien à modifier.

Baisse

Pour supprimer un tableau, vous devez supprimer tous les documents d’une collection. Cela peut également être fait dans le Console Firebase.

Contraintes

Il n’y a pas de schéma, mais vous pouvez déclarer des contraintes en utilisant Règles Firebase.

Conventions de dénomination

Vous verrez généralement les noms de champs en cas de chameau au lieu du cas en cas de serpent.

Clé primaire

Chaque document a un chemin unique. Le documentId() du document est la clé primaire. Dans la collection posts/2xelskelle 2xelskel est l’identifiant du document. Vous pouvez utiliser n’importe quelle chaîne alphanumérique, ainsi que _ personnages. Sinon, il sera généré automatiquement.

Insérer

Vous pouvez insérer des données dans SQL en déclarant chaque champ et valeur.

SQL

INSERT INTO posts (title, content, created_by, created_at)
VALUES (
    'My First Post', 
    'This is the content of my first post.', 
    1, 
    NOW()
);

Firestore

addDoc(
	collection(db, 'posts'),
	{
		title: 'My First Post',
    content: 'This is the content of my first post.',
    createdBy: 1,
    createdAt: serverTimestamp()
  }
});

📝 Notez le serverTimestamp() fonction, qui utilisera la date sur le serveur. Étant donné que Firestore peut être exécuté en toute sécurité sur le client, cela serait sécurisé dans les règles Firestore.

Mise à jour

La mise à jour est fondamentalement la même.

SQL

UPDATE posts
SET title = 'Updated Post Title',
    content = 'This is the updated content of the post.',
    updated_at = NOW()
WHERE post_id = 1;

Firestore

updateDoc(
	collection(db, 'posts', '1'),
	{
		title = 'Updated Post Title',
    content = 'This is the updated content of the post.',
    updatedAt: serverTimestamp()
  }
});

📝 Notez que l’ID du document est 1 dans ce cas. L’ID sera probablement une chaîne générée de manière unique dans une application réelle.

Bouleversé

Vous pouvez insérer un document avec setDoc.

SQL

INSERT INTO posts (id, title, content, created_by, created_at, updated_at)
VALUES (1, 'Upserted Post', 'This is the upserted content.', 1, NOW(), NOW())
ON CONFLICT (id)
DO UPDATE SET 
    title = EXCLUDED.title,
    content = EXCLUDED.content,
    author_id = EXCLUDED.created_by,
    updated_at = NOW();

📝 Cela varie selon les langages SQL.

setDoc(
	doc(db, 'posts', '1'),
	{
		title: 'Upserted Post',
		content: 'This is the upserted content.',
		createdBy: 1,
		createdAt: serverTimestamp(),
		updatedAt: serverTimestamp()
	}, { merge: true }
});

📝 setDoc se comporte comme addDoc sans le { merge: true } paramètre.

Sélectionner

Dans Firestore, vous ne pouvez sélectionner que tous les champs d’un document, sauf si vous utilisez l’API REST avec votre propre code.

SQL

SELECT * FROM posts;

Sélectionnez tous les messages.

Firestore

query(
	collection(db, 'posts')
);

Existe

En SQL, vous SELECT d’abord.

SQL

SELECT EXISTS (
    SELECT 1
    FROM posts
    WHERE id = 1
);

Firestore

const postSnap = await getDoc(doc(db, 'posts', '1'));
const exists = postSnap.exists();  

Dans Firestore, vous devez lire le document pour vérifier son existence.

Limite

Bien entendu, nous limitons notre sélection à un ou plusieurs cas d’utilisation réels. Nous ne voulons pas que tous les documents d’une collection ou toutes les lignes d’un tableau.

SQL

SELECT * FROM posts LIMIT 1;

Firestore

query(
	collection(db, 'posts'),
	limit(1)
);

Il n’y a rien de surprenant ici.

Utilisation des requêtes Firestore AND par défaut, mais vous pouvez également ajouter OR requêtes.

SQL

SELECT * FROM posts
WHERE
    (a = 1 AND c = 3) OR
    (a = 1 AND d = 4) OR
    (b = 2 AND c = 3) OR
    (b = 2 AND d = 4);

Firestore

query(
  collection(db, 'posts'),
  or(
    and(where('a', '==', 1), where('c', '==', 3)),
    and(where('a', '==', 1), where('d', '==', 4)),
    and(where('b', '==', 2), where('c', '==', 3)),
    and(where('b', '==', 2), where('d', '==', 4))
  )
);

⚠️ Contrairement à SQL, chaque requête a une limite stricte de 30 disjonctions.

Dans

Vous pouvez rechercher IN ainsi que dans Firestore.

SQL

SELECT *
FROM posts
WHERE created_by IN (1, 2, 3);

Firestore

query(
	collection(db, 'posts'),
	where('created_by', 'in', [1, 2, 3])
)

Gardez à l’esprit qu’il s’agit d’une simplification de :

query(
	collection(db, 'posts'),
	or(
		where('created_by', '==', 1),
		where('created_by', '==', 2),
		where('created_by', '==', 3)
	)
);

Il en va de même pour SQL. IN c’est juste plusieurs OR clauses sur le même domaine, condensées.

Commander par

Vous pouvez trier vos données de la même manière que SQL.

SQL

SELECT * FROM posts
ORDER BY created_by ASC, created_at DESC;

Firestore

query(
	collection(db, 'posts'),
	orderBy('createdBy'),
	orderBy('createdAt', 'desc')
);

📝 L’ordre de tri par défaut est asc.

📝Un orderBy trie également les données, le champ doit donc exister. Sinon, il est filtré.

⚠️ Changer orderBy field nécessite un index composite pour chaque requête. Si vous modifiez le champ, vous avez besoin d’un autre index.

Inégalités

Vous pouvez avoir des inégalités sur plusieurs domaines.

SQL

SELECT * FROM cities
WHERE population > 1000000
  AND density < 10000;

Firestore

query(
  collection(db, 'cities'),
  where('population', '>', 1000000),
  where('density', '<', 10000),
);

📝 Triez les champs d’index par égalités suivis de la plage la plus sélective ou du champ d’inégalité.

⚠️ Triez les champs par ordre décroissant de sélectivité des contraintes de requête.

⚠️ Requêtes avec filtres de plage ou d’inégalité sur les champs du document et uniquement contraintes d’égalité sur la clé du document (__name__) ne sont pas pris en charge.

⚠️ Cloud Firestore limite le nombre de champs de plage ou d’inégalité à 10. Cela évite que les requêtes ne deviennent trop coûteuses à exécuter.

PAS égal

Vous ne pouvez avoir qu’un != sur un champ dans Firestore par requête.

SQL

SELECT * FROM cities
WHERE capital != false;

Firestore

query(
	collection(db, 'cities'),
	where("capital", "!=", false)
);

⚠️Un != La clause de requête peut correspondre à de nombreux documents d’une collection. Pour contrôler le nombre de résultats renvoyés, utilisez une clause limite.

📝 Les requêtes non égales excluent les documents pour lesquels le champ donné n’existe pas.

📝 Pour plusieurs non égaux sur le même champ, utilisez not-in.

Incrémentation

Vous n’avez pas besoin d’incrémenter les colonnes dans SQL jusqu’à ce que votre base de données devienne grande. Quoi qu’il en soit, le code est simple.

SQL

UPDATE posts
SET views = views + 1
WHERE id = 1;

Firestore

updateDoc(
	doc(db, 'posts', '1'),
	{
		views: increment(1)
	}
});

📝 Ceci est plus utile dans les bases de données NoSQL. Vous pouvez également utiliser increment(-1) pour décrémenter.

Compter

Le comptage peut devenir complexe dans Firestore, mais en général, vous souhaitez compter sur le serveur.

SQL

SELECT COUNT(*) AS post_count
FROM posts
WHERE created_by = 1;

Firestore

const countSnap = await getCountFromServer(
  query(
    collection(db, 'posts')
  )
);
const postCount = countSnap.data().count;

📝 Vous avez besoin de deux appels distincts pour compter une requête et renvoyer les résultats de la requête.

Autres agrégations de comptes

Vous pouvez également faire sum et average agrégations.

📝 min et max ne sont pas pris en charge.

Voir Requêtes d’agrégation.

Tri par nombre

Vous ne pouvez pas trier directement par nombre, vous devez donc d’abord compter dans Firestore.

SQL

Disons que nous voulons que tous les utilisateurs et eux post_countou le nombre de messages dont ils disposent.

SELECT users.*, COUNT(posts.id) AS post_count
FROM users
JOIN posts ON users.id = posts.author_id
GROUP BY users.id
ORDER BY post_count DESC;

Firestore

Dans Firestore, il faut créer le post_count avec chaque publication créée dans le document de l’utilisateur. Nous pouvons le faire dans un batch transaction.

  const userRef = doc(db, 'users/1');
  const postRef = doc(collection(db, 'posts'));

  const batch = writeBatch(db);

  batch.set(userRef, {
      postCount: increment(1)
  }, { merge: true });

  batch.set(postRef, {
      createdBy: auth.currentUser,
      createdAt: serverTimestamp(),
      title: 'Some post',
      content: 'post content here'
  });

  batch.commit();
}

Et enfin, nous pourrons trier les documents plus tard.

query(
	collection(db, 'users'),
	orderBy('postCount', 'desc')
);

Vous pouvez sécuriser cela dans les règles Firestore ou créer une fonction de déclenchement Firebase sur onCreate qui incrémente et décrémente automatiquement le document utilisateur.

Les compteurs deviennent extrêmement compliqués dans Firestore. Voir Quatre façons de compter dans Firestore pour les cas de niche.

Unique et distinctif

La seule façon de conserver quelque chose d’unique est de le définir comme ID de document dans Firestore. Si vous souhaitez un champ unique, vous devez le mettre à deux endroits dans une transaction.

Noms d’utilisateur uniques

Pour plus d’unicité, nous pouvons enregistrer un nom d’utilisateur dans le users collection sur le document utilisateur et dans un usernames collecte en une seule transaction. Nous pouvons utiliser les règles Firestore pour sécuriser cela.

SQL

SELECT DISTINCT username
FROM users
ORDER BY username ASC;

Firestore

const batch = writeBatch();

batch.set(
    doc(`usernames/${username}`),
    {
        uid,
        username
    }
);

batch.set(
    doc(`users/${uid}`),
    {
        username
    },
    { merge: true }
);

await batch.commit();

Si vous mettez à jour le nom d’utilisateur, envisagez également de supprimer l’ancien nom d’utilisateur dans le usernames collecte en premier.

query(
	collection(db, 'usernames'),
	orderBy('usernames')
);

Pays distincts

Si vous souhaitez rechercher tous les pays auxquels appartiennent les utilisateurs, vous devez également stocker un identifiant unique. countries collection.

SQL

SELECT country, COUNT(*) as user_count
FROM users
GROUP BY country
ORDER BY user_count DESC;

Firestore

const batch = writeBatch();

batch.set(
    doc(`countries/${country}`),
    {
        uid,
        country
        userCount: increment(1)
    }
);

batch.set(
    doc(`users/${uid}`),
    {
        country
    },
    { merge: true }
);

await batch.commit();

Vous pouvez également créer les pays à l’aide d’une fonction de déclenchement Firebase. Vous devriez également envisager de compter le nombre d’utilisateurs dans chaque pays. Cela peut s’avérer utile si vous souhaitez rechercher le pays le plus populaire auquel appartiennent vos utilisateurs.

query(
	collection(db, 'countries'),
	orderBy('userCount', 'desc')
);

🖋️ Le thème commun que vous commencez à voir est que Firestore vous oblige à créer votre base de données pour les requêtes au lieu de l’inverse.

Comme

Il n’y a pas LIKE équivalent dans Firestore.

SQL

SELECT country
FROM users
WHERE country LIKE 'Uni%';

Cependant, il existe un startsWith hack que vous pouvez utiliser.

Firestore

export const startsWith = (
    fieldName: string,
    term: string
) => {
    return [
        orderBy(fieldName),
        startAt(term),
        endAt(term + '~')
    ];
};

Cette fonction peut être utilisée pour rechercher une chaîne commençant par un terme. Cela ne fonctionne que dans les objets parents et ne fonctionne pas avec des tableaux ou des objets.

query(
	collection(db, 'users'),
	...startsWith('country', 'Uni')
);

Utilisez une base de données externe ou stockez votre index de recherche dans Firestore si vous le souhaitez Recherche floue capacités. Vous pouvez le faire avec Vecteurs ou une fonction personnalisée comme Soundex.

Jointures internes

Vous pouvez facilement effectuer une jointure interne dans Firestore en interrogeant deux collections différentes, mais cela vous coûtera une lecture de document supplémentaire et pourrait potentiellement ralentir votre application.

Plusieurs-à-Un

L’exemple de classe consiste à ajouter des informations utilisateur à une publication.

SQL

SELECT 
  p.id AS post_id,
  p.title AS post_title,
  p.content AS post_content,
  jsonb_build_object(
    'id', u.id,
    'name', u.name,
    'email', u.email
  ) AS created_by
FROM posts p
JOIN users u ON p.created_by = u.id;

Vous pourriez facilement le faire sans JSON, mais je voulais un objet équivalent.

Firestore

addDoc(
	doc(db, 'posts', 'post-id'),
	{
		title: 'some title',
		content: 'some content',
		createdBy: {
			uid: auth.currentUser.uid,
			displayName: 'auth.currentUser.displayName,
			username: 'get username from user document or custom claim'
		}
	}
});

Cela pourrait être fait de plusieurs manières, mais la question est simple.

getDoc(
	doc(db, 'posts', 'post-id-here')
);	

Encore une fois, vous devez tenir compte du moment où un utilisateur met à jour son username, displayName ou d’autres informations de profil. Vous devez créer une fonction de déclenchement Firebase pour le gérer.

Ici, nous avons messages et tags. Chaque publication peut avoir plusieurs balises, mais vous n’aurez pas un nombre infini de balises sur une publication. Firestore vous permet de stocker des données sous forme de tableaux et d’effectuer une recherche en fonction du tableau. Un tableau fonctionne parfaitement dans ce cas.

SQL

En SQL, nous pouvons avoir une table de balises avec le user_id.

SELECT p.id AS post_id, p.title, p.content, t.tag
FROM posts p
LEFT JOIN tags t ON p.id = t.post_id
ORDER BY p.id;

Ou bien, nous pouvons avoir une table de jonction post_tag.

SELECT p.id AS post_id, p.title, p.content, t.name AS tag_name
FROM posts p
JOIN post_tag pt ON p.id = pt.post_id
JOIN tags t ON pt.tag_id = t.id
ORDER BY p.id;

Firestore

Dans Firestore, nous devons créer les balises à deux endroits différents, similaires à l’index unique.

const batch = writeBatch(db);

const tags = ['tag1', 'tag2'];

// add tags array when adding post
batch.set(
	doc(db, 'posts', 'some-post-id'), {
  tags,
  title: 'some title',
  content: 'some content',
  createdBy: auth.currentUser.uid,
  createdAt: serverTimestamp()
});

// create a tag document
tags.forEach(tag => {
	batch.set(doc(db, 'tags', tag), {
	  postCount: increment(1)
	}, { merge: true });
});

await batch.commit();

🖋️ Il faut aussi penser à décrémenter le postCount sur les balises lorsqu’une publication est modifiée ou supprimée. Cela devrait être fait avec les fonctions Firebase, car les règles Firestore deviendraient trop complexes pour plusieurs balises. Les règles Firestore n’ont pas non plus de boucles.

query(
	collection(db, 'posts')
);

Les balises seront déjà attachées au document de publication.

Requêtes complexes

Firestore ne gère pas les requêtes complexes, mais il existe des solutions de contournement pour la plupart des cas d’utilisation. Cet article ne fait qu’effleurer la surface.

Documents vs tableaux

Une base de données NoSQL est très différente d’une base de données SQL. Vous devez réfléchir à vos requêtes avant même de créer un document. Une fois que vous avez appris les modèles, vous pouvez créer n’importe quelle structure.

🌐 Je reste fermement convaincu que toutes les données sont relationnelles !

Mais Firebase vous permet de créer une application plus rapidement que n’importe quelle plate-forme de base de données que j’ai utilisée. Firestore est incroyable à cet égard.




Source link