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.domain.dto.TuningRequest;
|
||||||
import com.onixbyte.deltaforceguide.repository.FirearmRepository;
|
import com.onixbyte.deltaforceguide.repository.FirearmRepository;
|
||||||
import com.onixbyte.deltaforceguide.repository.ModificationRepository;
|
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.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -129,4 +132,9 @@ public class ModificationManager {
|
|||||||
tuning.setTuningValue(request.tuningValue());
|
tuning.setTuningValue(request.tuningValue());
|
||||||
return tuning;
|
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.jspecify.annotations.NonNull;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
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.EntityGraph;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
@@ -16,45 +17,23 @@ import java.util.Optional;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Spring Data JPA repository for {@link Modification} entity operations,
|
* 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
|
* @author zihluwang
|
||||||
*/
|
*/
|
||||||
@Repository
|
@Repository
|
||||||
public interface ModificationRepository extends JpaRepository<Modification, Long>, JpaSpecificationExecutor<Modification> {
|
public interface ModificationRepository extends JpaRepository<Modification, Long>, JpaSpecificationExecutor<Modification> {
|
||||||
|
|
||||||
|
@Override
|
||||||
@EntityGraph(attributePaths = {"firearm"})
|
@EntityGraph(attributePaths = {"firearm"})
|
||||||
Page<Modification> findAllBy(Pageable pageable);
|
@NonNull
|
||||||
|
Page<Modification> findAll(Specification<Modification> spec, @NonNull Pageable pageable);
|
||||||
@EntityGraph(attributePaths = {"firearm"})
|
|
||||||
Page<Modification> findAllByFirearm_Id(Long firearmId, Pageable pageable);
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@EntityGraph(attributePaths = {"firearm"})
|
@EntityGraph(attributePaths = {"firearm"})
|
||||||
@NonNull
|
@NonNull
|
||||||
Optional<Modification> findById(@NonNull Long id);
|
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.
|
* Retrieve all distinct tag values from modifications, optionally filtered by firearm.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.onixbyte.deltaforceguide.service;
|
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.AccessoryRequest;
|
||||||
import com.onixbyte.deltaforceguide.domain.dto.ModificationRequest;
|
import com.onixbyte.deltaforceguide.domain.dto.ModificationRequest;
|
||||||
import com.onixbyte.deltaforceguide.domain.dto.ModificationResponse;
|
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.Accessory;
|
||||||
import com.onixbyte.deltaforceguide.domain.entity.Firearm;
|
import com.onixbyte.deltaforceguide.domain.entity.Firearm;
|
||||||
import com.onixbyte.deltaforceguide.domain.entity.Modification;
|
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.Tuning;
|
||||||
|
import com.onixbyte.deltaforceguide.domain.entity.User;
|
||||||
import com.onixbyte.deltaforceguide.manager.ModificationManager;
|
import com.onixbyte.deltaforceguide.manager.ModificationManager;
|
||||||
import com.onixbyte.deltaforceguide.repository.FirearmRepository;
|
import com.onixbyte.deltaforceguide.repository.FirearmRepository;
|
||||||
import com.onixbyte.deltaforceguide.repository.ModificationRepository;
|
import com.onixbyte.deltaforceguide.repository.ModificationRepository;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.onixbyte.deltaforceguide.specification.ModificationSpecification;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import org.springframework.data.domain.Page;
|
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
@@ -55,8 +56,8 @@ public class ModificationService {
|
|||||||
* Queries modifications with optional firearm and tag filters.
|
* Queries modifications with optional firearm and tag filters.
|
||||||
*
|
*
|
||||||
* @param firearmId optional firearm ID filter
|
* @param firearmId optional firearm ID filter
|
||||||
* @param tags optional tag list filter
|
* @param tags optional tag list filter
|
||||||
* @param pageable pagination parameters
|
* @param pageable pagination parameters
|
||||||
* @return a paginated response of modification records
|
* @return a paginated response of modification records
|
||||||
*/
|
*/
|
||||||
public PageResponse<ModificationResponse> pageQuery(Long firearmId, List<String> tags, Pageable pageable) {
|
public PageResponse<ModificationResponse> pageQuery(Long firearmId, List<String> tags, Pageable pageable) {
|
||||||
@@ -69,12 +70,12 @@ public class ModificationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Page<Modification> page;
|
var spec = Specification.allOf(
|
||||||
if (tagsJson != null || firearmId != null) {
|
ModificationSpecification.hasFirearmId(firearmId),
|
||||||
page = modificationRepository.pageQueryByFirearmAndTags(firearmId, tagsJson, pageable);
|
ModificationSpecification.containsTags(tagsJson)
|
||||||
} else {
|
);
|
||||||
page = modificationRepository.findAllBy(pageable);
|
|
||||||
}
|
var page = modificationManager.findBySpec(spec, pageable);
|
||||||
|
|
||||||
return PageResponse.from(page.map(ModificationResponse::from));
|
return PageResponse.from(page.map(ModificationResponse::from));
|
||||||
}
|
}
|
||||||
@@ -126,7 +127,7 @@ public class ModificationService {
|
|||||||
/**
|
/**
|
||||||
* Updates an existing modification identified by ID.
|
* Updates an existing modification identified by ID.
|
||||||
*
|
*
|
||||||
* @param id the modification ID
|
* @param id the modification ID
|
||||||
* @param request the updated modification data
|
* @param request the updated modification data
|
||||||
* @return the updated modification response
|
* @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