Pratiquer le NoSQL grâce à la coupe du monde de football (partie 2)

Le Use Case

Nous sommes toujours en pleine Coupe du Monde, et dans cette série de billets nous mettons en œuvre le back-office d’une application qui permet :

  • de choisir son dispositif tactique parmi une liste prédéfinie : 4-4-2, 4-3-3, 3-5-2…,
  • de choisir ses joueurs pour chaque ligne, parmi une liste prédéfinie et en fonction du dispositif retenu : gardien, défenseurs, milieux, attaquants.

Dans la première partie de ce use case, nous avons vu comment Redis permet d’exposer efficacement des listes prédéfinies et invariantes : dispositifs tactiques, joueurs par ligne…

Nous allons à présent adresser les problématiques suivantes :

  • comment permettre à chaque utilisateur de stocker son équipe, et de la retrouver (pour pouvoir la modifier) lors d’une connexion ultérieure,
  • comment déterminer à tout moment les joueurs les plus souvent retenus par les utilisateurs.

Toujours pour rappel, le choix est fait de développer en Java avec Maven pour le build de notre projet. L’ensemble des sources est toujours accessible sur Github.

Caractéristiques de nos données et de nos besoins de requêtage

Nous allons devoir stocker et retrouver pour chaque utilisateur sa composition (dans le schéma tactique retenu).

Nous allons également devoir permettre de déterminer facilement quel est le schéma le plus populaire, et quels joueurs par poste sont les plus souvent choisis.

On peut faire le constat suivant :

  • il est à prévoir un gros volume de données (beaucoup de gens aiment le football, et encore plus de gens aiment donner leur avis, non ?),
  • ces données sont dénormalisables (on peut répéter dans chaque composition la liste des joueurs, et ne pas les référencer par une foreign key du monde relationnel),
  • ces données sont peu ou pas interconnectées (pas de relations entre deux compositions, si ce n’est le lien vers les joueurs, dont on a vu dans le point précédent qu’il est dénormalisable).

Nous allons comparer l’implémentation de ces use cases pour 2 bases NoSQL qui nous paraissent de bonnes candidates : Cassandra puis, dans un futur billet, MongoDB.

Avec Cassandra

Cassandra est une base NoSQL de la fondation Apache, initialement développée par Facebook. Cassandra est conçue pour fonctionner en cluster, en assurant un stockage massif et une haute-disponibilité.

Cela est classique dans le monde NoSQL, la différence se fait souvent au niveau du data model.
Historiquement Cassandra est une base qui combine l’approche clé – valeur et l’approche orientée colonne.

Concrètement, une ligne possède une clé de partition (qui peut être simple ou composite), et une liste de colonnes, qui sont des paires clés – valeurs (avec en plus un timestamp qui permet de dater le positionnement de cette valeur mais cela concerne une autre problématique qui n’a rien à voir avec le data model).

Parmi ces colonnes autres que la clé de partition, toutes ne sont pas « égales » : on va distinguer les clés de clustering (ou d’agrégation) qui vont régir la façon dont vont être regroupées les autres colonnes.

L’ensemble clé de partition + clé de clustering forme la primary key de la ligne. Les autres colonnes servent à stocker des infos.

Concernant le requêtage, on ne peut requêter par colonnes qu’en descendant de la plus discriminante à la moins discriminante (clé de partition, puis clé de clustering 1, puis clé de clustering 2). On peut aussi requêter par les colonnes de stockage, mais il faut avoir posé un index sur cette colonne.

Enfin pour définir ses données et les manipuler, Cassandra met à notre disposition CQL, sorte de SQL pour Cassandra.

Le modèle

Passons à présent au concret, voici la 1ère partie de notre schéma pour stocker nos données :

CREATE TABLE user_selection (
  email text PRIMARY KEY,
  formation text,
  goalkeepers set<text>,
  defenders set<text>,
  midfields set<text>,
  forwards set<text>
);

Comme nous pouvons le voir CQL ressemble à SQL, mais cette ressemblance est piégeuse.
Notons néanmoins que :

  • l’email du user connecté sera la primary key et la clé de partition de notre table user_selection,
  • les colonnes goalkeepers, defenders, midfields, et forwards sont de type set : elles permettront de stocker les joueurs retenus par le user connecté.

Par cette modélisation simple, on pourra facilement retrouver la sélection d’un user donné. En posant un index sur la colonne formation, on pourrait facilement et efficacement déterminer le dispositif tactique le plus populaire.

Par contre, il n’est pas encore possible de poser des index sur des colonnes de type set. Il nous faut donc une autre table pour stocker les mentions de joueurs :

CREATE TABLE player_mention (
  player text,
  selectioner_email text,
  position text,
  PRIMARY KEY  (player, selectioner_email)
);

Dans cette table, nous avons :

  • le player comme clé de partition,
  • l’email du user qui l’a sélectionné comme clé de clustering,
  • la combinaison de ces 2 colonnes forme la primary key.

Chaque fois qu’un joueur sera retenu par un user, nous irons rajouter une entrée dans cette table. Ainsi, il sera facile de déterminer les joueurs les plus populaires. Cela étant posé, comment attaquer tout cela en Java.

L’applicatif Java

Là encore Spring Data va nous rendre la tâche assez simple. Commençons par ajouter les dépendances dans notre pom.xml :

    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-cassandra</artifactId>
        <version>1.0.0.RELEASE</version>
    </dependency>

Puis définissons les beans nécessaires à la définition de Repository Spring vers une base Cassandra :

  <cassandra:cluster contact-points= »${cassandra.contactpoints} »
                     port= »${cassandra.port} » />

  <cassandra:session keyspace-name= »${cassandra.keyspace} » />

  <cassandra:mapping />

  <cassandra:converter />

  <cassandra:template id= »cassandraTemplate » />

Le cluster permet de déterminer où se trouvent les nœuds du cluster Cassandra. La session permet de déterminer le keyspace (équivalent du schéma Oracle) attaqué. Les composants de mapping et de converter sont des composants techniques indispensables.

Enfin le template est un template classique au sens Spring Data, que l’on injectera dans notre Repository.

Voici notre DAO (ou Repository) Cassandra :

@Repository
public class CassandraDao {

    @Autowired
    @Qualifier(« cassandraTemplate »)
    private CassandraOperations cassandraOperations;

Voici la méthode qui permet de rajouter une UserSelection (objet du domaine représentant une sélection faite par un user) :

      public void addUserSelection(UserSelection userSelection) {
        
        this.cassandraOperations.insert(userSelection);
    }

Voici d’ailleurs l’objet du domaine UserSelection :

@Table(value = « user_selection »)
public class UserSelection {

    @PrimaryKey
    private String email;
    
    private String formation;
    
    private Set<String> goalkeepers;
    
    private Set<String> defenders;
    
    private Set<String> midfields;
    
    private Set<String> forwards;

Notez l’utilisation d’annotations à la JPA pour indiquer les colonnes et le stables cibles.

Poursuivons avec notre autre objet du domaine, PlayerMention, et de sa primary key PlayerMentionPrimaryKey :

@Table(value = « player_mention »)
public class PlayerMention {

    @PrimaryKey
    private PlayerMentionPrimaryKey pk;
    
    private String position;

@PrimaryKeyClass
public class PlayerMentionPrimaryKey implements Serializable {

    @PrimaryKeyColumn(name = « player », ordinal = 0, type = PrimaryKeyType.PARTITIONED)
    private String player;
    
    @PrimaryKeyColumn(name = « selectioner_email », ordinal = 1, type = PrimaryKeyType.CLUSTERED, ordering = Ordering.ASCENDING)
    private String selectionerEmail;

Et voici la méthode qui permet de retrouver toutes les mentions pour un joueur donné :

public Collection<PlayerMention> retrievePlayerMentionsForPlayer(String player) {
        
    Select select = QueryBuilder.select().from(« player_mention »);
    select.where(QueryBuilder.eq(« player », player));
        
    return this.cassandraOperations.select(select, PlayerMention.class);
}

Tout ce code est assez linéaire et explicite : de notre point de vue, seule l’API de requêtage peut paraître un peu obscure.

Conclusion

De gros efforts ont été faits pour simplifier le Data Model de Cassandra et le rendre plus facile à appréhender, notamment via CQL.

Notre exemple met en lumière le fait qu’avec Cassandra il faut impérativement se poser la question  « Comment vais-je requêter mes données » avant de réaliser son modèle. Celui-ci dépend directement des requêtes que l’on aura à faire.

Côté applicatif, à travers Spring Data, les choses se passent assez clairement. Le mapping par annotation des entités métiers est très appréciable.

Dans un prochain billet, nous verrons comment réaliser la même chose en stockant ses données dans MongoDB.

0 commentaires

votre commentaire

Se joindre à la discussion ?
Vous êtes libre de contribuer !

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Inscription newsletter

Ne manquez plus nos derniers articles !