Uma implementação de marcação avançada com JPA
1. Visão geral
A marcação é um padrão de design que nos permite realizar filtragem e classificação avançadas em nossos dados. Este artigo é uma continuação dea Simple Tagging Implementation with JPA.
Portanto, continuaremos de onde o artigo parou e cobriremos casos de uso avançados para marcação.
2. Tags endossadas
Provavelmente, a implementação de marcação avançada mais conhecida é a de Endosso. Podemos ver esse padrão em sites como o Linkedin.
Essencialmente, a tag é uma combinação de um nome de sequência e um valor numérico. Em seguida, podemos usar o número para representar o número de vezes que a tag foi votada ou "endossada".
Aqui está um exemplo de como criar esse tipo de tag:
@Embeddable
public class SkillTag {
private String name;
private int value;
// constructors, getters, setters
}
Para usar essa tag, simplesmente adicionamosList deles ao nosso objeto de dados:
@ElementCollection
private List skillTags = new ArrayList<>();
Mencionamos no artigo anterior que a anotação@ElementCollection cria automaticamente um mapeamento um para muitos para nós.
Este é um caso de uso de modelo para esse relacionamento. Como cada tag possui dados personalizados associados à entidade em que está armazenada, não podemos economizar espaço com um mecanismo de armazenamento de muitos para muitos.
Posteriormente neste artigo, cobriremos um exemplo de quando muitos para muitos fazem sentido.
Como incorporamos a tag de habilidade em nossa entidade original, podemos consultá-la como qualquer outro atributo.
Aqui está um exemplo de consulta à procura de qualquer aluno com mais do que um certo número de recomendações:
@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);
A seguir, vamos ver um exemplo de como usar isso:
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());
Agora, podemos procurar a presença da tag ou ter um certo número de recomendações para a tag.
Conseqüentemente, podemos combinar isso com outros parâmetros de consulta para criar uma variedade de consultas complexas.
3. Tags de localização
Outra implementação de marcação popular é a Etiqueta de Localização. Podemos usar uma tag de localização de duas maneiras principais.
Primeiro de tudo, ele pode ser usado para marcar uma localização geofísica.
Além disso, pode ser usado para marcar um local na mídia, como uma foto ou vídeo. A implementação do modelo é quase idêntica em todos esses casos.
Aqui está um exemplo de marcação de uma foto:
@Embeddable
public class LocationTag {
private String name;
private int xPos;
private int yPos;
// constructors, getters, setters
}
O aspecto mais notável das tags de localização é a dificuldade de executar um filtro de geolocalização usando apenas um banco de dados. Se precisarmos pesquisar dentro dos limites geográficos, uma abordagem melhor é carregar o modelo em um mecanismo de pesquisa (como o Elasticsearch), que possui suporte interno para localizações geográficas.
Portanto, devemos nos concentrar na filtragem pelo nome da tag para essas tags de localização.
A consulta será semelhante à nossa implementação de marcação simples do artigo anterior:
@Query("SELECT s FROM Student s JOIN s.locationTags t WHERE t.name = LOWER(:tag)")
List retrieveByLocationTag(@Param("tag") String tag);
O exemplo para usar tags de localização também parecerá familiar:
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());
Se o Elasticsearch estiver fora de questão e ainda for necessário pesquisar em limites geográficos, o uso de formas geométricas simples tornará os critérios de consulta muito mais legíveis.
Vamos deixar de descobrir se um ponto está dentro de um círculo ou retângulo é um exercício direto para o leitor.
4. Tags de valor-chave
Às vezes, precisamos armazenar tags um pouco mais complicadas. Convém marcar uma entidade com um pequeno subconjunto de tags-chave, mas que pode conter uma grande variedade de valores.
Por exemplo, poderíamos marcar um aluno com uma tagdepartment e definir seu valor paraComputer Science. Cada aluno terá a chavedepartment, mas todos podem ter valores diferentes associados a ela.
A implementação será semelhante às Tags endossadas acima:
@Embeddable
public class KVTag {
private String key;
private String value;
// constructors, getters and setters
}
Podemos adicioná-lo ao nosso modelo assim:
@ElementCollection
private List kvTags = new ArrayList<>();
Agora podemos adicionar uma nova consulta ao nosso repositório:
@Query("SELECT s FROM Student s JOIN s.kvTags t WHERE t.key = LOWER(:key)")
List retrieveByKeyTag(@Param("key") String key);
Também podemos adicionar rapidamente uma consulta para pesquisar por valor ou por chave e valor. Isso nos dá flexibilidade adicional na maneira como pesquisamos nossos dados.
Vamos testar e verificar se tudo funciona:
@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());
}
Seguindo esse padrão, podemos projetar objetos aninhados ainda mais complicados e usá-los para marcar nossos dados, se necessário.
A maioria dos casos de uso pode ser atendida com as implementações avançadas sobre as quais falamos hoje, mas a opção existe para ser tão complicado quanto necessário.
5. Reimplementação de tags
Finalmente, vamos explorar uma última área de marcação. Até agora, vimos como usar a anotação@ElementCollection para facilitar a adição de tags ao nosso modelo. Embora seja simples de usar, tem uma compensação bastante significativa. A implementação de um para muitos sob o capô pode levar a muitos dados duplicados em nosso armazenamento de dados.
Para economizar espaço, precisamos criar outra tabela que unirá nossas entidadesStudent às nossas entidadesTag. Felizmente, o Spring JPA fará a maior parte do trabalho pesado para nós.
Vamos reimplementar nossas entidadesStudenteTag para ver como isso é feito.
5.1. Definir Entidades
Primeiro de tudo, precisamos recriar nossos modelos. Vamos começar com um modeloManyStudent:
@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
}
Há algumas coisas a serem observadas aqui.
Primeiro, estamos gerando nosso ID, de modo que os vínculos da tabela são mais fáceis de gerenciar internamente.
Em seguida, estamos usando a anotação@ManyToMany para dizer ao Spring que queremos uma ligação entre as duas classes.
Finalmente, usamos a anotação@JoinTable para configurar nossa tabela de junção real.
Agora podemos avançar para nosso novo modelo de tag, que chamaremos deManyTag:
@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
}
Como já configuramos nossa tabela de junção no modelo de aluno, tudo o que temos que nos preocupar é em configurar a referência dentro deste modelo.
Usamos o atributomappedBy para informar ao JPA que queremos este link para a Tabela de junção que criamos antes.
5.2. Definir Repositórios
Além dos modelos, também precisamos configurar dois repositórios: um para cada entidade. Vamos deixar Spring Data fazer todo o trabalho pesado aqui:
public interface ManyTagRepository extends JpaRepository {
}
Uma vez que não precisamos pesquisar apenas tags atualmente, podemos deixar a classe do repositório vazia.
Nosso repositório de alunos é apenas um pouco mais complicado:
public interface ManyStudentRepository extends JpaRepository {
List findByManyTags_Name(String name);
}
Mais uma vez, estamos permitindo que o Spring Data gere automaticamente as consultas para nós.
5.3. Teste
Finalmente, vamos ver como tudo isso se parece em um teste:
@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());
}
A flexibilidade adicionada pelo armazenamento das tags em uma tabela pesquisável separada supera em muito a menor quantidade de complexidade adicionada ao código.
Isso também nos permite reduzir o número total de tags que armazenamos no sistema, removendo tags duplicadas.
No entanto, muitos para muitos não é otimizado para os casos em que desejamos armazenar informações de estado específicas da entidade junto com a tag.
6. Conclusão
Este artigo começou de ondethe previous one parou.
Primeiro, apresentamos vários modelos avançados que são úteis ao projetar uma implementação de marcação.
Por fim, reexaminamos a implementação da marcação do último artigo no contexto de um mapeamento de muitos para muitos.
Para ver exemplos funcionais do que falamos hoje, verifique ocode on Github.