API App Engine Datastore pour les anciens services groupés

Remarque : Les développeurs qui créent des applications sont vivement encouragés à utiliser la bibliothèque cliente NDB qui présente plusieurs avantages supplémentaires par rapport à cette bibliothèque cliente, tels que la mise en cache automatique des entités via l'API Memcache. Si vous utilisez actuellement l'ancienne bibliothèque cliente DB, consultez le guide de migration de DB vers NDB.

Ce document décrit le modèle de données des objets stockés dans Datastore, la manière dont les requêtes sont structurées à l'aide de l'API et le traitement des transactions.

Entités

Dans Datastore, les objets sont appelés entités. Une entité est associée à une ou plusieurs propriétés nommées, chacune pouvant avoir une ou plusieurs valeurs. Les valeurs de propriété sont compatibles avec plusieurs types de données comme les nombres entiers, les nombres à virgule flottante, les chaînes, les dates et les données binaires, entre autres. Une requête effectuée sur une propriété à valeurs multiples permet de tester si une des valeurs répond aux critères de la requête. Cela rend ces propriétés utiles pour les tests d'adhésion.

Genres, clés et identifiants

Chaque entité Datastore est d'un genre particulier, ce qui permet de la classer dans une catégorie pour les besoins des requêtes. Par exemple, une application de ressources humaines peut représenter chaque employé d'une entreprise avec une entité du genre Employee. De plus, chaque entité a sa propre clé qui l'identifie de manière unique. Cette dernière comprend les composants suivants :

  • Le genre de l'entité
  • Un identifiant, qui peut être l'un des éléments suivants :
    • une chaîne de nom de clé
    • un identifiant entier
  • Un chemin d'ancêtre facultatif localisant l'entité dans la hiérarchie Datastore

L'identifiant est attribué lors de la création de l'entité. Comme il fait partie de la clé de l'entité, l'identifiant est associé de manière permanente à celle-ci et ne peut pas être modifié. Il peut être attribué de deux manières :

  • L'application peut spécifier sa propre chaîne de nom de clé pour l'entité.
  • Vous pouvez demander à Datastore d'attribuer automatiquement à l'entité un ID numérique sous forme d'entier.

Chemins d'ancêtre

Dans Cloud Datastore, les entités forment un espace structuré de manière hiérarchique, semblable à la structure de répertoires d'un système de fichiers. Lorsque vous créez une entité, vous pouvez éventuellement en désigner une autre comme son parent. La nouvelle entité est alors un enfant de l'entité parente. Contrairement à ce qui se produit dans un système de fichiers, l'entité parente n'a pas besoin d'exister réellement. Une entité sans parent est une entité racine. L'association entre une entité et son parent est permanente, et elle ne peut plus être modifiée une fois l'entité créée. Cloud Datastore n'attribue jamais le même ID numérique à deux entités ayant le même parent ou à deux entités racines (celles sans parent).

Les ancêtres d'une entité correspondent à son parent, au parent de son parent et ainsi de suite, de manière récursive. Ses descendants sont ses enfants, les enfants de ses enfants, etc. Une entité racine et tous ses descendants appartiennent au même groupe d'entités. La séquence d'entités commençant par une entité racine, puis allant du parent à l'enfant et menant à une entité donnée, constitue le chemin d'ancêtre de cette entité. La clé complète identifiant l'entité consiste en une séquence de paires genre/identifiant, qui spécifie son chemin d'ancêtre et se termine par les valeurs de l'entité elle-même :

[Person:GreatGrandpa, Person:Grandpa, Person:Dad, Person:Me]

Pour une entité racine, le chemin d'ancêtre est vide, et la clé est constituée uniquement du genre et de l'identifiant de l'entité :

[Person:GreatGrandpa]

Le schéma suivant illustre ce concept :

Affiche la relation entre l'entité racine et les entités enfants dans le groupe d'entités

Requêtes et index

En plus de récupérer les entités à partir de Datastore directement à l'aide de leurs clés, une application peut exécuter une requête pour les récupérer en fonction des valeurs de leurs propriétés. La requête s'exécute sur des entités d'un genre donné. Elle peut spécifier des filtres sur les valeurs de propriété, les clés et les ancêtres des entités, et peut renvoyer zéro, une ou plusieurs entités en tant que résultats. Une requête peut également spécifier des ordres de tri pour séquencer les résultats en fonction de leurs valeurs de propriété. Les résultats incluent toutes les entités qui ont au moins une valeur (pouvant être nulle) pour chaque propriété nommée dans les filtres et les ordres de tri, et dont les valeurs de propriété répondent à tous les critères de filtre spécifiés. La requête peut renvoyer des entités entières, des entités projetées ou simplement des clés d'entité.

Une requête type comprend les éléments suivants :

  • Un genre d'entité auquel s'applique la requête
  • Zéro, un ou plusieurs filtres basés sur les valeurs de propriété, les clés et les ancêtres des entités
  • Zéro, un ou plusieurs ordres de tri, pour séquencer les résultats
Lorsque la requête est exécutée, elle récupère toutes les entités d'un genre donné qui répondent à tous les filtres définis, en les triant dans l'ordre spécifié. Les requêtes s'exécutent en lecture seule.

Remarque : Pour économiser de la mémoire et améliorer les performances, une requête doit, dans la mesure du possible, spécifier une limite concernant le nombre de résultats renvoyés.

Une requête peut également inclure un filtre d'ancêtre limitant les résultats au seul groupe d'entités descendant d'un ancêtre spécifié. Une telle requête est appelée requête ascendante. Par défaut, les requêtes ascendantes renvoient des résultats fortement cohérents, qui sont forcément à jour en tenant compte des dernières modifications apportées aux données. Les requêtes non ascendantes, en revanche, peuvent s'étendre à l'ensemble de Datastore plutôt qu'à un seul groupe d'entités, mais sont seulement cohérentes à terme et peuvent renvoyer des résultats obsolètes. Si une cohérence forte est essentielle pour votre application, vous devrez peut-être en tenir compte lors de la structuration de vos données, en plaçant les entités associées dans le même groupe d'entités afin qu'elles puissent être récupérées à l'aide d'une requête ascendante plutôt qu'une requête non ascendante. Pour en savoir plus, consultez la section Structurer des données pour renforcer la cohérence.

App Engine prédéfinit un index simple sur chaque propriété d'une entité. Une application App Engine peut définir d'autres index personnalisés dans un fichier de configuration d'index appelé index.yaml. Le serveur de développement ajoute automatiquement des suggestions à ce fichier lorsqu'il est confronté à des requêtes qui ne peuvent pas être exécutées avec les index existants. Vous pouvez définir des index manuellement en modifiant le fichier avant de transférer l'application.

Remarque : Le mécanisme de requête basé sur les index permet l'exécution d'un large éventail de requêtes et convient à la plupart des applications. Toutefois, il n'est pas compatible avec certains genres de requêtes couramment rencontrés dans d'autres technologies de base de données. Plus précisément, les requêtes de jointure et d'agrégation ne sont pas acceptées dans le moteur de requêtes en mode Datastore. Pour connaître les limites associées aux requêtes Datastore, consultez la page Requêtes Datastore.

Transactions

Toute tentative d'insertion, de mise à jour ou de suppression d'une entité a lieu dans le cadre d'une transaction. Une même transaction peut inclure un nombre illimité d'opérations de ce type. Pour maintenir la cohérence des données, la transaction garantit que toutes les opérations qu'elle contient sont appliquées à Datastore en tant qu'unité ou, en cas d'échec de l'une de ces opérations, qu'aucune d'entre elles n'est appliquée.

Vous pouvez effectuer plusieurs actions sur une entité au sein d'une même transaction. Par exemple, pour incrémenter un champ de compteur dans un objet, vous devez lire la valeur du compteur, calculer la nouvelle valeur, puis la stocker à nouveau. Sans transaction, il est possible pour un autre processus d'incrémenter le compteur entre le moment où vous lisez la valeur et le moment où vous le mettez à jour, ce qui entraîne le remplacement de la valeur mise à jour par votre application. L'exécution des étapes de lecture, de calcul et d'écriture au cours de la même transaction garantit qu'aucun autre processus n'interfère avec l'incrémentation.

Transactions et groupes d'entités

Seules les requêtes ascendantes sont autorisées au sein d'une transaction : autrement dit, chaque requête transactionnelle doit être limitée à un seul groupe d'entités. La transaction elle-même peut s'appliquer à plusieurs entités, qui peuvent appartenir à un seul groupe d'entités ou, dans le cas d'une transaction entre groupes, à un maximum de 25 groupes d'entités différents.

Datastore gère les transactions sur la base de la simultanéité optimiste. Lorsque deux transactions ou plus tentent simultanément de modifier le même groupe d'entités (en mettant à jour des entités existantes ou en en créant de nouvelles), la première transaction qui procède au commit de ses modifications aboutit. Toutes les autres transactions échouent au moment du commit. Ces autres transactions peuvent ensuite être relancées sur les données mises à jour. Notez que cette caractéristique limite le nombre d'écritures simultanées que vous pouvez effectuer sur une entité d'un groupe d'entités donné.

Transactions entre groupes

Une transaction sur des entités appartenant à différents groupes d'entités est appelée transaction entre groupes (XG). La transaction peut être appliquée à un maximum de 25 groupes d'entités et aboutira tant qu'aucune transaction simultanée n'affecte les groupes d'entités auxquels elle s'applique. Cela vous permet d'organiser les données de manière plus souple, car vous n'êtes pas contraint de placer des éléments de données disparates sous le même ancêtre pour effectuer des écritures atomiques.

Comme pour les transactions à groupe unique, vous ne pouvez pas effectuer de requête non ascendante dans les transactions entre groupes. Vous pouvez toutefois effectuer des requêtes ascendantes sur des groupes d'entités distincts. Les requêtes non transactionnelles (non ascendantes) peuvent afficher tous, certains ou aucun des résultats d'une transaction précédemment validée. (Pour plus d'informations sur ce problème, consultez Écritures et visibilité des données Datastore.) Cependant, de telles requêtes non transactionnelles ont plus de chances de voir les résultats d'une transaction entre groupes partiellement validée que ceux d'une transaction de groupe unique partiellement validée.

Une transaction entre groupes ne concernant qu'un seul groupe d'entités est soumise aux mêmes performances et aux mêmes coûts qu'une transaction à groupe unique. Dans une transaction entre groupes affectant plusieurs groupes d'entités, les opérations ont le même coût que si elles étaient effectuées dans une transaction à groupe unique, mais peuvent subir une latence plus élevée.

Écritures et visibilité des données Datastore

Les données sont écrites dans Datastore en deux phases :

  1. Lors de la phase de commit, les données d'entité sont enregistrées dans les journaux de transactions de la plupart des instances dupliquées. Les instances dupliquées dans lesquelles elles n'ont pas été enregistrées sont marquées comme ne contenant pas de journaux à jour.
  2. La phase d'application intervient indépendamment dans chaque instance dupliquée et consiste en deux actions effectuées en parallèle :
    • Les données d'entité sont écrites dans l'instance dupliquée.
    • Les lignes d'index de l'entité sont écrites dans l'instance dupliquée. Notez que cette action peut prendre plus de temps que d'écrire les données.

L'opération d'écriture est renvoyée immédiatement après la phase de commit. La phase d'application se déroule ensuite de manière asynchrone, éventuellement à des moments différents dans chaque instance dupliquée, et avec des retards de quelques centaines de millisecondes ou plus au terme de la phase de commit. Si un échec se produit lors de la phase de commit, de nouvelles tentatives sont automatiquement lancées. Toutefois, si des échecs persistent, Datastore renvoie un message d'erreur que votre application reçoit sous forme d'exception. Si la phase de commit aboutit, mais que la phase d'application échoue dans une instance dupliquée spécifique, la phase d'application est déployée jusqu'à son terme dans cette instance dupliquée lorsque l'une des situations suivantes se produit :

  • Des analyses périodiques effectuées par Datastore contrôlent les tâches de commit inachevées et les appliquent.
  • Certaines opérations (get, put, delete et requêtes ascendantes) qui utilisent le groupe d'entités affecté entraînent l'exécution des modifications validées, mais pas encore appliquées, dans l'instance dupliquée où elles s'exécutent avant que la nouvelle opération soit exécutée.

Ce comportement en écriture peut avoir plusieurs conséquences sur la manière et le moment où les données sont visibles pour votre application à différentes étapes des phases de commit et d'application :

  • Si une opération d'écriture signale une erreur d'expiration du délai, il est impossible de déterminer (sans tenter de lire les données) si l'opération a réussi ou échoué.
  • Dans la mesure où Datastore récupère les modifications en attente apportées à l'instance dupliquée où elles s'exécutent et que les requêtes ascendantes les appliquent, ces opérations affichent toujours une vue cohérente de toutes les transactions précédentes ayant abouti. Ceci signifie qu'une opération get (recherche d'une entité mise à jour par sa clé) est garantie de voir la dernière version de cette entité.
  • Les requêtes non ascendantes peuvent renvoyer des résultats obsolètes, car elles peuvent s'exécuter sur une instance dupliquée à laquelle les dernières transactions n'ont pas encore été appliquées. Cela peut se produire même si une opération censée appliquer les transactions en attente a été effectuée, car la requête peut s'exécuter sur une instance dupliquée différente de l'opération précédente.
  • La synchronisation des modifications simultanées peut affecter les résultats des requêtes non ascendantes. Si une entité satisfait initialement une requête, mais est modifiée ultérieurement et ne la satisfait plus, elle peut toujours être incluse dans les résultats de la requête si les modifications n'ont pas encore été appliquées aux index de l'instance dupliquée sur laquelle la requête a été exécutée.

Statistiques Datastore

Datastore conserve des statistiques sur les données stockées pour une application, telles que le nombre d'entités d'un genre donné ou l'espace utilisé par les valeurs de propriété d'un type donné. Vous pouvez consulter ces statistiques sur la page du tableau de bord Datastore de la console Google Cloud. Vous pouvez également utiliser l'API Datastore pour accéder à ces valeurs de manière automatisée depuis l'application, en interrogeant des entités portant un nom spécifique ; consultez la section Statistiques du magasin de données en Python 2 pour plus d'informations.

Exemple Datastore Python 2

Dans l'API Python, un modèle décrit un genre d'entité avec les types et la configuration de ses propriétés. Une application définit un modèle à l'aide d'une classe Python, avec des attributs de classe décrivant les propriétés. Le nom de la classe donne son nom au genre de l'entité. Les entités du genre spécifié sont représentées par des instances de la classe de modèle, avec des attributs d'instance représentant les valeurs de propriété. Pour créer une nouvelle entité, créez un objet de la classe que vous souhaitez utiliser, définissez ses attributs, puis enregistrez-le (en appelant une méthode telle que put()) :

import datetime
from google.appengine.ext import db
from google.appengine.api import users

class Employee(db.Model):
  name = db.StringProperty(required=True)
  role = db.StringProperty(required=True,
                           choices=set(["executive", "manager", "producer"]))
  hire_date = db.DateProperty()
  new_hire_training_completed = db.BooleanProperty(indexed=False)
  email = db.StringProperty()

e = Employee(name="John",
             role="manager",
             email=users.get_current_user().email())
e.hire_date = datetime.datetime.now().date()
e.put()

L'API Datastore fournit deux interfaces pour les requêtes : une interface d'objet de requête et un langage de requête de type SQL appelé GQL. Une requête renvoie des entités sous la forme d'instances des classes du modèle :

training_registration_list = ["Alfred.Smith@example.com",
                              "jharrison@example.com",
                              "budnelson@example.com"]
employees_trained = db.GqlQuery("SELECT * FROM Employee WHERE email IN :1",
                                training_registration_list)
for e in employees_trained:
  e.new_hire_training_completed = True
  db.put(e)