Une implémentation avancée du marquage avec JPA

Une implémentation avancée du marquage avec JPA

1. Vue d'ensemble

Le marquage est un modèle de conception qui nous permet d’effectuer un filtrage et un tri avancés de nos données. Cet article est une suite dea Simple Tagging Implementation with JPA.

Par conséquent, nous reprendrons là où cet article s'est arrêté et couvrirons les cas d'utilisation avancés du balisage.

2. Balises approuvées

La mise en œuvre du marquage avancé la plus connue est probablement la balise d’approbation. Nous pouvons voir ce modèle sur des sites comme Linkedin.

Essentiellement, la balise est une combinaison d'un nom de chaîne et d'une valeur numérique. Ensuite, nous pouvons utiliser le nombre pour représenter le nombre de fois que l'étiquette a été votée ou «approuvée».

Voici un exemple de création de ce type de tag:

@Embeddable
public class SkillTag {
    private String name;
    private int value;

    // constructors, getters, setters
}

Pour utiliser cette balise, nous en ajoutons simplement unList à notre objet de données:

@ElementCollection
private List skillTags = new ArrayList<>();

Nous avons mentionné dans l'article précédent que l'annotation@ElementCollection crée automatiquement un mappage un-à-plusieurs pour nous.

Ceci est un exemple de cas d'utilisation pour cette relation. Étant donné que chaque balise possède des données personnalisées associées à l'entité sur laquelle elle est stockée, nous ne pouvons pas économiser de l'espace avec un mécanisme de stockage plusieurs-à-plusieurs.

Plus loin dans l'article, nous aborderons un exemple de cas où plusieurs-à-plusieurs a du sens.

Étant donné que nous avons intégré la balise de compétence dans notre entité d'origine, nous pouvons l'interroger comme n'importe quel autre attribut.

Voici un exemple de requête recherchant un étudiant avec plus d'un certain nombre de recommandations:

@Query(
  "SELECT s FROM Student s JOIN s.skillTags t WHERE t.name = LOWER(:tagName) AND t.value > :tagValue")
List retrieveByNameFilterByMinimumSkillTag(
  @Param("tagName") String tagName, @Param("tagValue") int tagValue);

Voyons ensuite un exemple d'utilisation de ceci:

Student student = new Student(1, "Will");
SkillTag skill1 = new SkillTag("java", 5);
student.setSkillTags(Arrays.asList(skill1));
studentRepository.save(student);

Student student2 = new Student(2, "Joe");
SkillTag skill2 = new SkillTag("java", 1);
student2.setSkillTags(Arrays.asList(skill2));
studentRepository.save(student2);

List students =
  studentRepository.retrieveByNameFilterByMinimumSkillTag("java", 3);
assertEquals("size incorrect", 1, students.size());

Nous pouvons maintenant rechercher la présence de la balise ou avoir un certain nombre de mentions pour la balise.

Par conséquent, nous pouvons combiner cela avec d'autres paramètres de requête pour créer une variété de requêtes complexes.

3. Balises de localisation

Une autre implémentation de marquage populaire est la balise de localisation. Nous pouvons utiliser une balise d'emplacement de deux manières principales.

Tout d'abord, il peut être utilisé pour baliser un emplacement géophysique.

En outre, il peut être utilisé pour marquer un emplacement dans un support tel qu'une photo ou une vidéo. La mise en œuvre du modèle est presque identique dans tous ces cas.

Voici un exemple de marquage d'une photo:

@Embeddable
public class LocationTag {
    private String name;
    private int xPos;
    private int yPos;

    // constructors, getters, setters
}

L'aspect le plus remarquable des balises de localisation est la difficulté d'effectuer un filtre de géolocalisation en utilisant simplement une base de données. Si nous devons rechercher dans des limites géographiques, une meilleure approche consiste à charger le modèle dans un moteur de recherche (comme Elasticsearch) qui prend en charge les géolocalisations.

Par conséquent, nous devrions nous concentrer sur le filtrage par nom de balise pour ces balises d'emplacement.

La requête ressemblera à notre implémentation de balisage simple de l'article précédent:

@Query("SELECT s FROM Student s JOIN s.locationTags t WHERE t.name = LOWER(:tag)")
List retrieveByLocationTag(@Param("tag") String tag);

L'exemple d'utilisation des balises d'emplacement vous semblera également familier:

Student student = new Student(0, "Steve");
student.setLocationTags(Arrays.asList(new LocationTag("here", 0, 0));
studentRepository.save(student);

Student student2 = studentRepository.retrieveByLocationTag("here").get(0);
assertEquals("name incorrect", "Steve", student2.getName());

Si Elasticsearch est hors de question et que nous devons toujours rechercher des limites géographiques, l'utilisation de formes géométriques simples rendra les critères de requête beaucoup plus lisibles.

Nous allons laisser la recherche si un point est dans un cercle ou un rectangle est un exercice simple pour le lecteur.

4. Balises clé-valeur

Parfois, nous devons stocker des étiquettes légèrement plus compliquées. Nous pourrions vouloir baliser une entité avec un petit sous-ensemble d'étiquettes de clé, mais cela peut contenir une grande variété de valeurs.

Par exemple, nous pourrions marquer un étudiant avec une balisedepartment et définir sa valeur surComputer Science. Chaque élève aura la clédepartment, mais ils pourraient tous avoir des valeurs différentes associées.

La mise en œuvre ressemblera aux balises approuvées ci-dessus:

@Embeddable
public class KVTag {
    private String key;
    private String value;

    // constructors, getters and setters
}

Nous pouvons l'ajouter à notre modèle comme ceci:

@ElementCollection
private List kvTags = new ArrayList<>();

Nous pouvons maintenant ajouter une nouvelle requête à notre référentiel:

@Query("SELECT s FROM Student s JOIN s.kvTags t WHERE t.key = LOWER(:key)")
List retrieveByKeyTag(@Param("key") String key);

Nous pouvons également ajouter rapidement une requête pour effectuer une recherche par valeur ou par clé et par valeur. Cela nous donne une flexibilité supplémentaire dans la recherche de nos données.

Testons ceci et vérifions que tout fonctionne:

@Test
public void givenStudentWithKVTags_whenSave_thenGetByTagOk(){
    Student student = new Student(0, "John");
    student.setKVTags(Arrays.asList(new KVTag("department", "computer science")));
    studentRepository.save(student);

    Student student2 = new Student(1, "James");
    student2.setKVTags(Arrays.asList(new KVTag("department", "humanities")));
    studentRepository.save(student2);

    List students = studentRepository.retrieveByKeyTag("department");

    assertEquals("size incorrect", 2, students.size());
}

En suivant ce modèle, nous pouvons concevoir des objets imbriqués encore plus complexes et les utiliser pour baliser nos données, le cas échéant.

La plupart des cas d'utilisation sont compatibles avec les implémentations avancées dont nous avons parlé aujourd'hui, mais l'option est là pour aller aussi compliquée que nécessaire.

5. Réimplémentation du balisage

Enfin, nous allons explorer un dernier domaine du balisage. Jusqu'à présent, nous avons vu comment utiliser l'annotation@ElementCollection pour faciliter l'ajout de balises à notre modèle. Bien qu'il soit simple à utiliser, il présente un compromis assez important. L'implémentation un à plusieurs sous le capot peut générer de nombreuses données dupliquées dans notre magasin de données.

Pour économiser de l'espace, nous devons créer une autre table qui joindra nos entitésStudent à nos entitésTag. Heureusement, Spring JPA fera le gros du travail pour nous.

Nous allons réimplémenter nos entitésStudent etTag pour voir comment cela est fait.

5.1. Définir des entités

Tout d'abord, nous devons recréer nos modèles. Nous allons commencer par un modèleManyStudent:

@Entity
public class ManyStudent {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    private String name;

    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "manystudent_manytags",
      joinColumns = @JoinColumn(name = "manystudent_id",
      referencedColumnName = "id"),
      inverseJoinColumns = @JoinColumn(name = "manytag_id",
      referencedColumnName = "id"))
    private Set manyTags = new HashSet<>();

    // constructors, getters and setters
}

Il y a quelques choses à remarquer ici.

Tout d'abord, nous générons notre identifiant, de sorte que les liens de table sont plus faciles à gérer en interne.

Ensuite, nous utilisons l'annotation@ManyToMany pour indiquer à Spring que nous voulons un lien entre les deux classes.

Enfin, nous utilisons l'annotation@JoinTable pour configurer notre table de jointure réelle.

Nous pouvons maintenant passer à notre nouveau modèle de balise que nous appelleronsManyTag:

@Entity
public class ManyTag {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    private String name;

    @ManyToMany(mappedBy = "manyTags")
    private Set students = new HashSet<>();

    // constructors, getters, setters
}

Comme nous avons déjà configuré notre table de jointure dans le modèle étudiant, tout ce dont nous avons à nous soucier est de configurer la référence à l'intérieur de ce modèle.

Nous utilisons l'attributmappedBy pour indiquer à JPA que nous voulons ce lien vers la table de jointure que nous avons créée auparavant.

5.2. Définir les référentiels

En plus des modèles, nous devons également configurer deux référentiels: un pour chaque entité. Nous allons laisser Spring Data faire tout le gros du travail ici:

public interface ManyTagRepository extends JpaRepository {
}

Étant donné que nous n'avons pas besoin de rechercher uniquement des balises pour le moment, nous pouvons laisser la classe de dépôt vide.

Notre référentiel d’étudiants n’est que légèrement plus compliqué:

public interface ManyStudentRepository extends JpaRepository {
    List findByManyTags_Name(String name);
}

Encore une fois, nous laissons Spring Data générer automatiquement les requêtes pour nous.

5.3. Essai

Enfin, voyons à quoi tout cela ressemble dans un test:

@Test
public void givenStudentWithManyTags_whenSave_theyGetByTagOk() {
    ManyTag tag = new ManyTag("full time");
    manyTagRepository.save(tag);

    ManyStudent student = new ManyStudent("John");
    student.setManyTags(Collections.singleton(tag));
    manyStudentRepository.save(student);

    List students = manyStudentRepository
      .findByManyTags_Name("full time");

    assertEquals("size incorrect", 1, students.size());
}

La flexibilité ajoutée par le stockage des balises dans une table de recherche distincte dépasse de loin la complexité mineure ajoutée au code.

Cela nous permet également de réduire le nombre total de tags stockés dans le système en supprimant les tags en double.

Cependant, plusieurs-à-plusieurs n'est pas optimisé pour les cas où nous souhaitons stocker des informations d'état spécifiques à l'entité avec le tag.

6. Conclusion

Cet article a repris là oùthe previous one s'était arrêté.

Tout d’abord, nous avons introduit plusieurs modèles avancés qui sont utiles lors de la conception d’une implémentation de marquage.

Enfin, nous avons réexaminé la mise en œuvre du balisage du dernier article dans le contexte d’une cartographie plusieurs à plusieurs.

Pour voir des exemples concrets de ce dont nous avons parlé aujourd'hui, veuillez consulter lescode on Github.