refactor(mod): page query via specifications
Unify the native JSONB query and findAllBy dual path through JpaSpecificationExecutor: - add ModificationSpecification for firearm and tag filters - eagerly fetch the firearm association via entity graph - filter by the firearm id attribute instead of a missing top-level field
This commit is contained in:
4 files changed
+68
-39
No files matched your search
@@ -11,6 +11,9 @@ import com.onixbyte.deltaforceguide.domain.dto.ModificationResponse;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.TuningRequest;
|
||||
import com.onixbyte.deltaforceguide.repository.FirearmRepository;
|
||||
import com.onixbyte.deltaforceguide.repository.ModificationRepository;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -129,4 +132,9 @@ public class ModificationManager {
|
||||
tuning.setTuningValue(request.tuningValue());
|
||||
return tuning;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Modification> findBySpec(Specification<Modification> spec, Pageable pageable) {
|
||||
return modificationRepository.findAll(spec, pageable);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.onixbyte.deltaforceguide.domain.entity.Modification;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.data.jpa.repository.EntityGraph;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
@@ -16,45 +17,23 @@ import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Spring Data JPA repository for {@link Modification} entity operations,
|
||||
* including native JSONB tag filtering for Postgres.
|
||||
* including JSONB tag filtering for Postgres via the specification API.
|
||||
*
|
||||
* @author zihluwang
|
||||
*/
|
||||
@Repository
|
||||
public interface ModificationRepository extends JpaRepository<Modification, Long>, JpaSpecificationExecutor<Modification> {
|
||||
|
||||
@Override
|
||||
@EntityGraph(attributePaths = {"firearm"})
|
||||
Page<Modification> findAllBy(Pageable pageable);
|
||||
|
||||
@EntityGraph(attributePaths = {"firearm"})
|
||||
Page<Modification> findAllByFirearm_Id(Long firearmId, Pageable pageable);
|
||||
@NonNull
|
||||
Page<Modification> findAll(Specification<Modification> spec, @NonNull Pageable pageable);
|
||||
|
||||
@Override
|
||||
@EntityGraph(attributePaths = {"firearm"})
|
||||
@NonNull
|
||||
Optional<Modification> findById(@NonNull Long id);
|
||||
|
||||
/**
|
||||
* Page query modifications with optional firearm and JSONB tag filtering.
|
||||
*
|
||||
* @param firearmId optional firearm ID filter (nullable)
|
||||
* @param tagsJson optional JSON array of tags to match via Postgres {@code @>} operator (nullable)
|
||||
* @param pageable pagination information
|
||||
* @return a page of matching modifications
|
||||
*/
|
||||
@Query(value = """
|
||||
SELECT * FROM modification m
|
||||
WHERE (:firearmId IS NULL OR m.firearm_id = :firearmId)
|
||||
AND (CAST(:tagsJson AS text) IS NULL OR cast(m.tags as jsonb) @> cast(CAST(:tagsJson AS text) as jsonb))
|
||||
""",
|
||||
countQuery = """
|
||||
SELECT count(*) FROM modification m
|
||||
WHERE (:firearmId IS NULL OR m.firearm_id = :firearmId)
|
||||
AND (CAST(:tagsJson AS text) IS NULL OR cast(m.tags as jsonb) @> cast(CAST(:tagsJson AS text) as jsonb))
|
||||
""",
|
||||
nativeQuery = true)
|
||||
Page<Modification> pageQueryByFirearmAndTags(@Param("firearmId") Long firearmId, @Param("tagsJson") String tagsJson, Pageable pageable);
|
||||
|
||||
/**
|
||||
* Retrieve all distinct tag values from modifications, optionally filtered by firearm.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.onixbyte.deltaforceguide.service;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.AccessoryRequest;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.ModificationRequest;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.ModificationResponse;
|
||||
@@ -8,15 +10,14 @@ import com.onixbyte.deltaforceguide.domain.dto.TuningRequest;
|
||||
import com.onixbyte.deltaforceguide.domain.entity.Accessory;
|
||||
import com.onixbyte.deltaforceguide.domain.entity.Firearm;
|
||||
import com.onixbyte.deltaforceguide.domain.entity.Modification;
|
||||
import com.onixbyte.deltaforceguide.domain.entity.User;
|
||||
import com.onixbyte.deltaforceguide.domain.entity.Tuning;
|
||||
import com.onixbyte.deltaforceguide.domain.entity.User;
|
||||
import com.onixbyte.deltaforceguide.manager.ModificationManager;
|
||||
import com.onixbyte.deltaforceguide.repository.FirearmRepository;
|
||||
import com.onixbyte.deltaforceguide.repository.ModificationRepository;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.data.domain.Page;
|
||||
import com.onixbyte.deltaforceguide.specification.ModificationSpecification;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
@@ -55,8 +56,8 @@ public class ModificationService {
|
||||
* Queries modifications with optional firearm and tag filters.
|
||||
*
|
||||
* @param firearmId optional firearm ID filter
|
||||
* @param tags optional tag list filter
|
||||
* @param pageable pagination parameters
|
||||
* @param tags optional tag list filter
|
||||
* @param pageable pagination parameters
|
||||
* @return a paginated response of modification records
|
||||
*/
|
||||
public PageResponse<ModificationResponse> pageQuery(Long firearmId, List<String> tags, Pageable pageable) {
|
||||
@@ -69,12 +70,12 @@ public class ModificationService {
|
||||
}
|
||||
}
|
||||
|
||||
Page<Modification> page;
|
||||
if (tagsJson != null || firearmId != null) {
|
||||
page = modificationRepository.pageQueryByFirearmAndTags(firearmId, tagsJson, pageable);
|
||||
} else {
|
||||
page = modificationRepository.findAllBy(pageable);
|
||||
}
|
||||
var spec = Specification.allOf(
|
||||
ModificationSpecification.hasFirearmId(firearmId),
|
||||
ModificationSpecification.containsTags(tagsJson)
|
||||
);
|
||||
|
||||
var page = modificationManager.findBySpec(spec, pageable);
|
||||
|
||||
return PageResponse.from(page.map(ModificationResponse::from));
|
||||
}
|
||||
@@ -126,7 +127,7 @@ public class ModificationService {
|
||||
/**
|
||||
* Updates an existing modification identified by ID.
|
||||
*
|
||||
* @param id the modification ID
|
||||
* @param id the modification ID
|
||||
* @param request the updated modification data
|
||||
* @return the updated modification response
|
||||
*/
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.onixbyte.deltaforceguide.specification;
|
||||
|
||||
import com.onixbyte.deltaforceguide.domain.entity.Modification;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class ModificationSpecification {
|
||||
|
||||
/**
|
||||
* Filters by firearm ID if specified.
|
||||
*/
|
||||
public static Specification<Modification> hasFirearmId(Long firearmId) {
|
||||
return (root, query, cb) -> {
|
||||
if (Objects.isNull(firearmId)) {
|
||||
return null;
|
||||
}
|
||||
return cb.equal(root.get("firearm").get("id"), firearmId);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the jsonb tags contain the given JSON string.
|
||||
*/
|
||||
public static Specification<Modification> containsTags(String tagsJson) {
|
||||
return (root, query, cb) -> {
|
||||
if (Objects.isNull(tagsJson)) {
|
||||
return null;
|
||||
}
|
||||
// Invoke the PostgreSQL native function 'jsonb_contains' (backing the
|
||||
// '@>' operator). The argument is cast via the 'jsonb' function-style
|
||||
// cast so PostgreSQL resolves the correct overload instead of treating
|
||||
// the bound String as varchar.
|
||||
return cb.isTrue(
|
||||
cb.function("jsonb_contains", Boolean.class,
|
||||
root.get("tags"),
|
||||
cb.function("jsonb", String.class, cb.literal(tagsJson)))
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user