JPAによる高度なタグ付けの実装

1概要

タグ付けは、データに対して高度なフィルタ処理と並べ替えを実行できるようにするデザインパターンです。この記事はリンクの続きです:/jpa-tagging[JPAによる簡単なタグ付けの実装]。

そのため、その記事がどこで中断されたかを把握し、タグ付けの高度な使用例について説明します。

2承認されたタグ

おそらく最もよく知られている高度なタグ付けの実装はEndorsement Tagです。 Linkedinのようなサイトでこのパターンを見ることができます。

基本的に、タグは文字列名と数値の組み合わせです。それから、タグが投票された、または「承認された」回数を表すために番号を使用できます。

これは、この種のタグを作成する方法の例です。

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

   //constructors, getters, setters
}

このタグを使用するには、それらの List をデータオブジェクトに追加するだけです。

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

前回の記事で、 @ ElementCollection アノテーションは自動的に1対多のマッピングを作成すると述べました。

これは、この関係のモデルユースケースです。各タグには、それが格納されているエンティティに関連付けられたパーソナライズされたデータがあるため、多対多の格納メカニズムではスペースを節約できません。

この記事の後半で、多対多が意味をなす場合の例を取り上げます。

スキルタグは元のエンティティに埋め込まれているので、他の属性と同じようにクエリできます。

これは、一定数以上の推薦を持つ生徒を探すためのクエリの例です。

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

次に、これを使用する方法の例を見てみましょう:

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<Student> students =
  studentRepository.retrieveByNameFilterByMinimumSkillTag("java", 3);
assertEquals("size incorrect", 1, students.size());

これで、タグが存在するか、または特定の数のタグが推奨されているかを検索できます。

したがって、これを他のクエリパラメータと組み合わせて、さまざまな複雑なクエリを作成できます。

3場所タグ

もう1つの一般的なタグ付けの実装はLocation Tagです。ロケーションタグは、主に2つの方法で使用できます。

まず第一に、それは地球物理学的な場所をタグ付けするのに使用することができます。

また、写真やビデオなどのメディア内の場所をタグ付けするためにも使用できます。モデルの実装は、これらすべてのケースでほぼ同じです。

写真にタグを付ける例は次のとおりです。

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

   //constructors, getters, setters
}

ロケーションタグの最も注目すべき点は、データベースだけを使用して位置情報フィルタを実行するのがいかに難しいかです。地理的境界内で検索する必要がある場合は、地理位置情報をサポートしている検索エンジン(Elasticsearchなど)にモデルを読み込むことがより良い方法です。

したがって、これらのロケーションタグのタグ名によるフィルタリングに焦点を当てる必要があります。

このクエリは、前回の記事の簡単なタグ付けの実装に似たものになります。

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

ロケーションタグを使用する例もよく知られています。

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

Elasticsearchが問題外で、それでも地理的な境界で検索する必要がある場合は、単純な幾何学的図形を使用すると、クエリ基準がはるかに読みやすくなります。

読者のための演習として、点が円または長方形の範囲内にあるかどうかはわかります。

4 Key-Valueタグ

時には、もう少し複雑なタグを保存する必要があります。私たちは、キータグの小さなサブセットでエンティティをタグ付けしたいかもしれませんが、それは多種多様な値を含むことができます。

たとえば、生徒に department タグを付けてその値を Computer Science に設定できます。各生徒は department キーを持ちますが、全員が異なる値を関連付けることができます。

実装は、上記の承認済みタグと似ています。

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

   //constructors, getters and setters
}

これをモデルに追加することができます。

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

これでリポジトリに新しいクエリを追加できます。

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

値またはキーと値の両方で検索するクエリをすばやく追加することもできます。これにより、データの検索方法に柔軟性が追加されます。

これを試して、すべてうまくいくことを確認しましょう。

@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<Student> students = studentRepository.retrieveByKeyTag("department");

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

このパターンに従って、さらに複雑なネストしたオブジェクトを設計し、必要に応じてそれらを使用してデータにタグを付けることができます。

ほとんどのユースケースは、今日説明した高度な実装で満たすことができますが、必要に応じて複雑にするという選択肢もあります。

5タグ付けの再実装

最後に、タグ付けの最後の1つの分野について説明します。これまで、 @ ElementCollection アノテーションを使用してモデルにタグを簡単に追加できるようにする方法を説明しました。使い方は簡単ですが、かなり大きなトレードオフがあります。フードの下での一対多の実装は、私たちのデータストアに大量の重複データをもたらす可能性があります。

スペースを節約するために、 Student エンティティを Tag エンティティに結合する別のテーブルを作成する必要があります。幸いなことに、Spring JPAは私たちのために大部分の重い作業を行います。

これがどのように行われるかを確認するために、 Student エンティティと Tag エンティティを再実装します。

5.1. エンティティを定義する

まず最初に、モデルを作り直す必要があります。私たちは ManyStudent モデルから始めます。

@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<ManyTag> manyTags = new HashSet<>();

   //constructors, getters and setters
}

ここで注意すべきことがいくつかあります。

まず、IDを生成しているので、テーブルのリンクは内部で管理しやすいです。

次に、 @ ManyToMany アノテーションを使用して、Springに2つのクラス間のリンケージが必要であることを伝えます。

最後に、 @ JoinTable アノテーションを使用して実際の結合テーブルを設定します。

これで、 ManyTag と呼ぶ新しいタグモデルに進むことができます。

@Entity
public class ManyTag {

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

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

   //constructors, getters, setters
}

我々はすでに学生モデルにjoinテーブルを設定しているので、心配しなければならないのは、このモデルの中にリファレンスを設定することだけです。

mappedBy 属性を使用して、このリンクを以前に作成した結合テーブルへのリンクにすることをJPAに指示します。

5.2. リポジトリの定義

モデルに加えて、エンティティごとに1つずつ、合計2つのリポジトリを設定する必要があります。ここでSpring Dataにすべての重労働を任せましょう。

public interface ManyTagRepository extends JpaRepository<ManyTag, Long> {
}

現在タグだけを検索する必要はないので、リポジトリクラスを空のままにしておくことができます。

私たちの学生リポジトリはもう少し複雑です。

public interface ManyStudentRepository extends JpaRepository<ManyStudent, Long> {
    List<ManyStudent> findByManyTags__Name(String name);
}

繰り返しになりますが、Spring Dataにクエリを自動生成させています。

5.3. テスト中

最後に、これがテストでどのように見えるかを見てみましょう。

@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<ManyStudent> students = manyStudentRepository
      .findByManyTags__Name("full time");

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

タグを別の検索可能なテーブルに格納することによって追加される柔軟性は、コードに追加されるわずかな複雑さをはるかに上回ります。

これにより、重複したタグを削除して、システムに保存されているタグの総数を減らすこともできます。

ただし、多対多は、エンティティ固有の状態情報をタグと一緒に格納する場合には最適化されていません。

6. 結論

この記事では、 前の記事 が中断したところを取り上げました。

まず最初に、タグ付けの実装を設計するときに役立ついくつかの高度なモデルを紹介しました。

最後に、多対多マッピングのコンテキストで、前回の記事からタグ付けの実装を再検討しました。

今日話したことの実用的な例を見るためには、https://github.com/eugenp/tutorials/tree/master/persistence-modules/spring-data-jpa[code on Github]をチェックしてください。

"

  • «** 前へ