From db61d6b77f15e039f4a8da8710d94e09a9aae156 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 7 Apr 2026 13:12:54 +0200 Subject: [PATCH] feat(search): extend person search to include alias last names Adds LEFT JOIN to person_name_aliases in both searchByName (JPQL) and searchWithDocumentCount (native SQL). Uses DISTINCT/GROUP BY to prevent duplicate results. 4 new integration tests. Co-Authored-By: Claude Sonnet 4.6 --- .../repository/PersonRepository.java | 9 ++- .../repository/PersonRepositoryTest.java | 57 +++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/repository/PersonRepository.java b/backend/src/main/java/org/raddatz/familienarchiv/repository/PersonRepository.java index 6e2bd033..abbed802 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/repository/PersonRepository.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/repository/PersonRepository.java @@ -15,11 +15,11 @@ import org.springframework.stereotype.Repository; @Repository public interface PersonRepository extends JpaRepository { - // Suche nach String in Vor- ODER Nachnamen, sortiert nach Nachname - @Query("SELECT p FROM Person p WHERE " + + @Query("SELECT DISTINCT p FROM Person p LEFT JOIN p.nameAliases a WHERE " + "LOWER(CONCAT(p.firstName,' ',p.lastName)) LIKE LOWER(CONCAT('%', :query, '%')) OR " + "LOWER(CONCAT(p.lastName, ' ', p.firstName)) LIKE LOWER(CONCAT('%', :query, '%')) OR " + - "LOWER(p.alias) LIKE LOWER(CONCAT('%', :query, '%')) " + + "LOWER(p.alias) LIKE LOWER(CONCAT('%', :query, '%')) OR " + + "LOWER(a.lastName) LIKE LOWER(CONCAT('%', :query, '%')) " + "ORDER BY p.lastName ASC, p.firstName ASC") List searchByName(@Param("query") String query); @@ -51,9 +51,12 @@ public interface PersonRepository extends JpaRepository { (SELECT COUNT(*) FROM documents d WHERE d.sender_id = p.id) + (SELECT COUNT(*) FROM document_receivers dr WHERE dr.person_id = p.id) AS documentCount FROM persons p + LEFT JOIN person_name_aliases a ON a.person_id = p.id WHERE LOWER(CONCAT(p.first_name,' ',p.last_name)) LIKE LOWER(CONCAT('%',:query,'%')) OR LOWER(CONCAT(p.last_name,' ',p.first_name)) LIKE LOWER(CONCAT('%',:query,'%')) OR LOWER(p.alias) LIKE LOWER(CONCAT('%',:query,'%')) + OR LOWER(a.last_name) LIKE LOWER(CONCAT('%',:query,'%')) + GROUP BY p.id, p.first_name, p.last_name, p.alias, p.birth_year, p.death_year, p.notes ORDER BY p.last_name ASC, p.first_name ASC """, nativeQuery = true) diff --git a/backend/src/test/java/org/raddatz/familienarchiv/repository/PersonRepositoryTest.java b/backend/src/test/java/org/raddatz/familienarchiv/repository/PersonRepositoryTest.java index 20811c72..b0873d35 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/repository/PersonRepositoryTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/repository/PersonRepositoryTest.java @@ -6,6 +6,8 @@ import org.raddatz.familienarchiv.config.FlywayConfig; import org.raddatz.familienarchiv.model.Document; import org.raddatz.familienarchiv.model.DocumentStatus; import org.raddatz.familienarchiv.model.Person; +import org.raddatz.familienarchiv.model.PersonNameAlias; +import org.raddatz.familienarchiv.model.PersonNameAliasType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase; import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest; @@ -29,6 +31,9 @@ class PersonRepositoryTest { @Autowired private PersonRepository personRepository; + @Autowired + private PersonNameAliasRepository aliasRepository; + @Autowired private DocumentRepository documentRepository; @@ -383,4 +388,56 @@ class PersonRepositoryTest { assertThat(documentRepository.findById(doc1.getId()).orElseThrow().getReceivers()).isEmpty(); assertThat(documentRepository.findById(doc2.getId()).orElseThrow().getReceivers()).isEmpty(); } + + // ─── searchByName with aliases ─────────────────────────────────────────── + + @Test + void searchByName_findsByAliasLastName() { + Person clara = personRepository.save(Person.builder().firstName("Clara").lastName("Cram").build()); + aliasRepository.save(PersonNameAlias.builder() + .person(clara).lastName("de Gruyter").type(PersonNameAliasType.BIRTH).sortOrder(0).build()); + + List results = personRepository.searchByName("de Gruyter"); + + assertThat(results).hasSize(1); + assertThat(results.get(0).getLastName()).isEqualTo("Cram"); + } + + @Test + void searchByName_stillFindsByCurrentLastName_afterAliasAdded() { + Person clara = personRepository.save(Person.builder().firstName("Clara").lastName("Cram").build()); + aliasRepository.save(PersonNameAlias.builder() + .person(clara).lastName("de Gruyter").type(PersonNameAliasType.BIRTH).sortOrder(0).build()); + + List results = personRepository.searchByName("Cram"); + + assertThat(results).hasSize(1); + } + + @Test + void searchByName_doesNotReturnDuplicates_whenMultipleAliasesMatch() { + Person clara = personRepository.save(Person.builder().firstName("Clara").lastName("Cram").build()); + aliasRepository.save(PersonNameAlias.builder() + .person(clara).lastName("de Gruyter").type(PersonNameAliasType.BIRTH).sortOrder(0).build()); + aliasRepository.save(PersonNameAlias.builder() + .person(clara).lastName("Gruyter-Cram").type(PersonNameAliasType.OTHER).sortOrder(1).build()); + + List results = personRepository.searchByName("Gruyter"); + + assertThat(results).hasSize(1); + } + + // ─── searchWithDocumentCount with aliases ──────────────────────────────── + + @Test + void searchWithDocumentCount_findsByAliasLastName() { + Person clara = personRepository.save(Person.builder().firstName("Clara").lastName("Cram").build()); + aliasRepository.save(PersonNameAlias.builder() + .person(clara).lastName("de Gruyter").type(PersonNameAliasType.BIRTH).sortOrder(0).build()); + + List results = personRepository.searchWithDocumentCount("de Gruyter"); + + assertThat(results).hasSize(1); + assertThat(results.get(0).getLastName()).isEqualTo("Cram"); + } }