«1. Обзор

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

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

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

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

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

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

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

    // constructors, getters, setters
}

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

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

Мы упоминали в предыдущей статье что аннотация @ElementCollection автоматически создает для нас сопоставление «один ко многим».

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

Позже в статье мы рассмотрим пример, когда многие ко многим имеют смысл.

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

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

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

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

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

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

Вот пример пометки фотографии:

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

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

Например, мы можем пометить студента тегом отдела и установить его значение на Информатика. У каждого студента будет ключ отдела, но все они могут иметь разные значения, связанные с ним.

Реализация будет похожа на одобренные теги выше:

@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. Повторная реализация тегов

Наконец, мы собираемся исследовать последнюю область тегов. До сих пор мы видели, как использовать аннотацию @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
}

Здесь следует отметить несколько моментов.

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

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

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

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

5.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.

Next »

A Simple Tagging Implementation with MongoDB

« Previous

A Simple Tagging Implementation with JPA