Eine erweiterte Tagging-Implementierung mit JPA

Eine erweiterte Tagging-Implementierung mit JPA

1. Überblick

Tagging ist ein Entwurfsmuster, mit dem wir erweiterte Filter- und Sortierfunktionen für unsere Daten ausführen können. Dieser Artikel ist eine Fortsetzung vona Simple Tagging Implementation with JPA.

Daher werden wir dort weitermachen, wo dieser Artikel aufgehört hat, und erweiterte Anwendungsfälle für das Tagging behandeln.

2. Indossierte Tags

Die wahrscheinlich bekannteste erweiterte Tagging-Implementierung ist das Endorsement-Tag. Wir können dieses Muster auf Sites wie Linkedin sehen.

Im Wesentlichen ist das Tag eine Kombination aus einem Zeichenfolgennamen und einem numerischen Wert. Dann können wir die Zahl verwenden, um die Häufigkeit darzustellen, mit der das Tag gewählt oder „gebilligt“ wurde.

Hier ist ein Beispiel, wie Sie diese Art von Tag erstellen:

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

    // constructors, getters, setters
}

Um dieses Tag zu verwenden, fügen wir einfach einList davon zu unserem Datenobjekt hinzu:

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

Wir haben im vorherigen Artikel erwähnt, dass die Annotation@ElementCollectionautomatisch eine Eins-zu-Viele-Zuordnung für uns erstellt.

Dies ist ein vorbildlicher Anwendungsfall für diese Beziehung. Da jedem Tag personalisierte Daten zugeordnet sind, die der Entität zugeordnet sind, auf der es gespeichert ist, können wir mit einem Viele-zu-Viele-Speichermechanismus keinen Platz sparen.

Später in diesem Artikel werden wir ein Beispiel dafür behandeln, wann viele zu viele sinnvoll sind.

Da wir das Skill-Tag in unsere ursprüngliche Entität eingebettet haben, können wir es wie jedes andere Attribut abfragen.

Hier ist eine Beispielabfrage, die nach einem Schüler mit mehr als einer bestimmten Anzahl von Vermerken sucht:

@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);

Schauen wir uns als nächstes ein Beispiel für die Verwendung an:

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

Jetzt können wir entweder nach dem Vorhandensein des Tags suchen oder nach einer bestimmten Anzahl von Vermerken für das Tag.

Folglich können wir dies mit anderen Abfrageparametern kombinieren, um eine Vielzahl komplexer Abfragen zu erstellen.

3. Standort-Tags

Eine weitere beliebte Tagging-Implementierung ist das Location Tag. Wir können ein Standort-Tag auf zwei Arten verwenden.

Erstens kann es verwendet werden, um einen geophysikalischen Ort zu markieren.

Es kann auch verwendet werden, um einen Ort in Medien wie Fotos oder Videos mit Tags zu versehen. Die Implementierung des Modells ist in all diesen Fällen nahezu identisch.

Hier ist ein Beispiel für das Markieren eines Fotos:

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

    // constructors, getters, setters
}

Der bemerkenswerteste Aspekt von Standort-Tags ist, wie schwierig es ist, einen Geolocation-Filter nur mit einer Datenbank durchzuführen. Wenn wir innerhalb geografischer Grenzen suchen müssen, ist es besser, das Modell in eine Suchmaschine (wie Elasticsearch) zu laden, die Geolokalisierungsfunktionen unterstützt.

Daher sollten wir uns darauf konzentrieren, nach dem Tag-Namen für diese Standort-Tags zu filtern.

Die Abfrage ähnelt unserer einfachen Tag-Implementierung aus dem vorherigen Artikel:

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

Das Beispiel zur Verwendung von Standort-Tags kommt ebenfalls bekannt vor:

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

Wenn Elasticsearch nicht in Frage kommt und wir weiterhin nach geografischen Grenzen suchen müssen, werden die Abfragekriterien durch die Verwendung einfacher geometrischer Formen besser lesbar.

Es bleibt für den Leser unkompliziert, herauszufinden, ob sich ein Punkt innerhalb eines Kreises oder Rechtecks ​​befindet.

4. Schlüsselwert-Tags

Manchmal müssen wir Tags speichern, die etwas komplizierter sind. Möglicherweise möchten wir eine Entität mit einer kleinen Teilmenge von Schlüssel-Tags versehen, die jedoch eine Vielzahl von Werten enthalten können.

Zum Beispiel könnten wir einen Schüler mit einemdepartment-Tag versehen und seinen Wert aufComputer Science setzen. Jeder Schüler hat den Schlüsseldepartment, aber alle können unterschiedliche Werte haben.

Die Implementierung sieht ähnlich aus wie bei den oben genannten Endorsed Tags:

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

    // constructors, getters and setters
}

Wir können es wie folgt zu unserem Modell hinzufügen:

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

Jetzt können wir unserem Repository eine neue Abfrage hinzufügen:

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

Wir können auch schnell eine Abfrage hinzufügen, um nach Wert oder sowohl nach Schlüssel als auch nach Wert zu suchen. Dies gibt uns zusätzliche Flexibilität bei der Suche nach unseren Daten.

Lassen Sie uns dies testen und überprüfen, ob alles funktioniert:

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

Nach diesem Muster können wir noch kompliziertere verschachtelte Objekte entwerfen und diese verwenden, um unsere Daten zu kennzeichnen, wenn dies erforderlich ist.

Die meisten Anwendungsfälle können mit den fortgeschrittenen Implementierungen bewältigt werden, über die wir heute gesprochen haben, aber es besteht die Möglichkeit, so kompliziert wie nötig vorzugehen.

5. Tagging erneut implementieren

Schließlich werden wir einen letzten Bereich des Markierens untersuchen. Bisher haben wir gesehen, wie Sie die Annotation@ElementCollectionverwenden können, um das Hinzufügen von Tags zu unserem Modell zu vereinfachen. Es ist zwar einfach zu bedienen, hat aber einen ziemlich bedeutenden Kompromiss. Die Eins-zu-Viele-Implementierung unter der Haube kann zu vielen doppelten Daten in unserem Datenspeicher führen.

Um Platz zu sparen, müssen wir eine weitere Tabelle erstellen, die die EntitätenStudentmit den EntitätenTagverbindet. Glücklicherweise wird Spring JPA den Großteil des Schwerlasttransports für uns erledigen.

Wir werden unsere EntitätenStudent undTag erneut implementieren, um zu sehen, wie dies getan wird.

5.1. Entitäten definieren

Zunächst müssen wir unsere Modelle neu erstellen. Wir beginnen mit einemManyStudent-Modell:

@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
}

Hier gibt es ein paar Dinge zu beachten.

Zunächst generieren wir unsere ID, damit die Tabellenverknüpfungen intern einfacher verwaltet werden können.

Als Nächstes verwenden wir die Annotation@ManyToMany, um Spring mitzuteilen, dass eine Verknüpfung zwischen den beiden Klassen gewünscht wird.

Schließlich verwenden wir die Annotation@JoinTable, um unsere eigentliche Join-Tabelle einzurichten.

Jetzt können wir zu unserem neuen Tag-Modell übergehen, das wirManyTag nennen:

@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
}

Da wir unsere Join-Tabelle bereits im Studentenmodell eingerichtet haben, müssen wir uns nur um die Einrichtung der Referenz in diesem Modell kümmern.

Wir verwenden das AttributmappedBy, um JPA mitzuteilen, dass dieser Link zu der zuvor erstellten Join-Tabelle verwendet werden soll.

5.2. Repositorys definieren

Zusätzlich zu den Modellen müssen zwei Repositorys eingerichtet werden: eines für jede Entität. Wir werden Spring Data hier das ganze schwere Heben überlassen:

public interface ManyTagRepository extends JpaRepository {
}

Da wir derzeit nicht nur nach Tags suchen müssen, können wir die Repository-Klasse leer lassen.

Unser Schülerarchiv ist nur geringfügig komplizierter:

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

Auch hier lassen wir Spring Data die Abfragen automatisch für uns generieren.

5.3. Testen

Lassen Sie uns abschließend in einem Test sehen, wie das alles aussieht:

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

Die Flexibilität, die durch das Speichern der Tags in einer separaten durchsuchbaren Tabelle erreicht wird, überwiegt bei weitem die geringe Komplexität, die dem Code hinzugefügt wird.

Auf diese Weise können wir auch die Gesamtzahl der Tags reduzieren, die wir im System speichern, indem wir doppelte Tags entfernen.

Viele-zu-Viele sind jedoch nicht für Fälle optimiert, in denen entitätsspezifische Statusinformationen zusammen mit dem Tag gespeichert werden sollen.

6. Fazit

Dieser Artikel wurde dort aufgenommen, wothe previous oneaufgehört hat.

Zunächst haben wir einige erweiterte Modelle vorgestellt, die beim Entwerfen einer Tagging-Implementierung hilfreich sind.

Schließlich haben wir die Implementierung des Tagging aus dem letzten Artikel im Rahmen eines Many-to-Many-Mappings erneut untersucht.

Um Arbeitsbeispiele von dem zu sehen, worüber wir heute gesprochen haben, schauen Sie sich bitte diecode on Githuban.