1. 概要

タグ付けは、データに対して高度なフィルタリングと並べ替えを実行できるようにするデザインパターンです。 この記事は、JPAを使用した単純なタグ付けの実装の続きです。

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

2. 承認されたタグ

おそらく最もよく知られている高度なタグ付けの実装は、承認タグです。 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つの一般的なタグ付けの実装は、ロケーションタグです。 ロケーションタグは、主に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. キー値タグ

場合によっては、少し複雑なタグを保存する必要があります。 キータグの小さなサブセットでエンティティにタグを付けることもできますが、それにはさまざまな値を含めることができます。

たとえば、学生に department タグを付けて、その値を ComputerScienceに設定できます。 各学生には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 アノテーションを使用して、モデルにタグを簡単に追加する方法を見てきました。 使い方は簡単ですが、かなり大きなトレードオフがあります。 内部での1対多の実装は、データストア内の多数の重複データにつながる可能性があります。

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

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 アノテーションを使用して、2つのクラス間のリンクが必要であることをSpringに通知します。

最後に、 @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
}

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

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. 結論

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

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

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

今日お話しした内容の実際の例を確認するには、GitHubコードを確認してください。