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

JPAを使用した高度なタグ付け実装

1. 概要

タグ付けは、データに対して高度なフィルタリングと並べ替えを実行できるデザインパターンです。 この記事はa Simple Tagging Implementation with JPAの続きです。

したがって、その記事が中断したところを取り上げ、タグ付けの高度なユースケースについて説明します。

2. 承認されたタグ

おそらく最もよく知られている高度なタグ付けの実装は、承認タグです。 Linkedinなどのサイトでこのパターンを確認できます。

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

この種のタグを作成する方法の例を次に示します。

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

    // constructors, getters, setters
}

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

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

これで、タグの存在を検索するか、タグに対して一定数の承認を取得することができます。

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

3. ロケーションタグ

もう1つの一般的なタグの実装は、ロケーションタグです。 ロケーションタグは、主に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 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 kvTags = new ArrayList<>();

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

@Query("SELECT s FROM Student s JOIN s.kvTags t WHERE t.key = LOWER(:key)")
List 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 students = studentRepository.retrieveByKeyTag("department");

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

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

ほとんどのユースケースは、今日説明した高度な実装で満たすことができますが、オプションは必要に応じて複雑になることがあります。

5. タグ付けの再実装

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

スペースを節約するには、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 manyTags = new HashSet<>();

    // constructors, getters and setters
}

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

まず、IDを生成するため、テーブルのリンクを内部で管理しやすくなります。

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

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

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

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

学生モデルで結合テーブルをすでに設定しているため、心配する必要があるのは、このモデル内で参照を設定することだけです。

mappedBy属性を使用して、前に作成した結合テーブルへのこのリンクが必要であることをJPAに通知します。

5.2. リポジトリを定義する

モデルに加えて、エンティティごとに1つずつ、2つのリポジトリを設定する必要があります。 ここでは、SpringDataにすべての面倒な作業を任せます。

public interface ManyTagRepository extends JpaRepository {
}

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

私たちの学生リポジトリは少しだけ複雑です:

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

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

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

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

タグを別の検索可能なテーブルに保存することで追加される柔軟性は、コードに追加されるわずかな複雑さよりもはるかに重要です。

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

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

6. 結論

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

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

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

今日お話ししたことの実例を見るには、code on Githubをチェックしてください。