Рубрики
Без рубрики

Расширенная реализация тегов с помощью JPA

Взгляните на более продвинутые сценарии тегирования с помощью JPA.

Автор оригинала: baeldung.

1. Обзор

Тегирование-это шаблон проектирования, который позволяет нам выполнять расширенную фильтрацию и сортировку наших данных. Эта статья является продолжением простой реализации тегов с помощью JPA .

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

2. Одобренные теги

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

По сути, тег представляет собой комбинацию строкового имени и числового значения. Затем мы можем использовать это число для представления количества раз, когда тег был проверен или “одобрен”.

Вот пример того, как создать такой тег:

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

    // constructors, getters, setters
}

Чтобы использовать этот тег, мы просто добавляем их Список в наш объект данных:

@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. Определение сущностей

Прежде всего, нам нужно воссоздать наши модели. Начнем с Много студентов модель:

@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 для настройки нашей фактической таблицы соединений.

Теперь мы можем перейти к нашей новой модели тегов, которую мы назовем Maytag :

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

Поскольку мы уже настроили нашу таблицу соединений в модели student, все, о чем нам нужно беспокоиться, – это настройка ссылки внутри этой модели.

Мы используем атрибут mappedBy , чтобы сообщить JPA, что нам нужна эта ссылка на таблицу соединений, которую мы создали ранее.

5.2. Определение Репозиториев

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

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. Заключение

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

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

Наконец, была рассмотрена реализация тегирования из последней статьи в контексте сопоставления “многие ко многим”.

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