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 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-07 13:12:54 +02:00
parent a1d63bbc42
commit db61d6b77f
2 changed files with 63 additions and 3 deletions

View File

@@ -15,11 +15,11 @@ import org.springframework.stereotype.Repository;
@Repository
public interface PersonRepository extends JpaRepository<Person, UUID> {
// 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<Person> searchByName(@Param("query") String query);
@@ -51,9 +51,12 @@ public interface PersonRepository extends JpaRepository<Person, UUID> {
(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)

View File

@@ -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<Person> 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<Person> 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<Person> 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<PersonSummaryDTO> results = personRepository.searchWithDocumentCount("de Gruyter");
assertThat(results).hasSize(1);
assertThat(results.get(0).getLastName()).isEqualTo("Cram");
}
}