Перейти к основному содержимому

Расширенная реализация тегов с JPA

· 7 мин. чтения

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 .