Продвинутая реализация тегов с 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 автоматически создает для нас отображение «один ко многим».

Это пример использования модели для этих отношений. Поскольку каждый тег содержит персонализированные данные, связанные с объектом, в котором он хранится, мы не можем сэкономить место с помощью механизма хранения "многие ко многим".

Позже в этой статье мы рассмотрим пример, когда имеет смысл использовать принцип «многие ко многим».

Поскольку мы встроили тег навыка в исходную сущность, мы можем запрашивать его, как и любой другой атрибут.

Вот пример запроса для поиска любого учащегося с более чем определенным количеством одобрений:

@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. Теги местоположения

Другая популярная реализация тегов - это тег местоположения. Мы можем использовать тег местоположения двумя основными способами.

Прежде всего, его можно использовать для обозначения геофизического местоположения.

Кроме того, его можно использовать для обозначения местоположения на носителе, такого как фотография или видео. Реализация модели практически идентична во всех этих случаях.

Вот пример добавления тега к фотографии:

@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. Теги "ключ-значение"

Иногда нам нужно хранить теги, которые немного сложнее. Возможно, мы захотим пометить сущность небольшим подмножеством ключевых тегов, но они могут содержать самые разные значения.

Например, мы могли бы пометить студента тегом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. Повторная реализация тегов

Наконец, мы собираемся изучить еще одну область применения тегов. До сих пор мы видели, как использовать аннотацию@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 manyTags = new HashSet<>();

    // constructors, getters and setters
}

Здесь нужно отметить пару вещей.

Во-первых, мы генерируем свой идентификатор, чтобы было проще управлять связями таблиц внутри.

Затем мы используем аннотацию@ManyToMany, чтобы сообщить 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. Определить репозитории

В дополнение к моделям, нам также нужно настроить два хранилища: по одному для каждой сущности. Мы позволим Spring Data сделать здесь всю тяжелую работу:

public interface ManyTagRepository extends JpaRepository {
}

Поскольку сейчас нам не нужно искать только по тегам, мы можем оставить класс репозитория пустым.

Наш репозиторий для студентов немного сложнее:

public interface ManyStudentRepository extends JpaRepository {
    List 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 students = manyStudentRepository
      .findByManyTags_Name("full time");

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

Гибкость, добавленная хранением тегов в отдельной доступной для поиска таблице, намного перевешивает незначительную сложность, которая добавляется в код.

Это также позволяет нам сократить общее количество тегов, которые мы храним в системе, удаляя дубликаты тегов.

Однако функция "многие ко многим" не оптимизирована для случаев, когда мы хотим хранить информацию о состоянии, специфичную для объекта, вместе с тегом.

6. Заключение

Эта статья началась с того места, где остановилсяthe previous one.

Прежде всего, мы представили несколько продвинутых моделей, которые полезны при разработке реализации тегирования.

Наконец, мы повторно рассмотрели реализацию тегов из предыдущей статьи в контексте сопоставления «многие ко многим».

Чтобы увидеть рабочие примеры того, о чем мы говорили сегодня, ознакомьтесь сcode on Github.