spring-data-jpa — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited spring-data-jpa (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
@Entity
@Table(name = "orders")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // JPA requires no-arg, hide from callers
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(updatable = false, nullable = false)
private UUID id;
@Column(nullable = false)
private String customerEmail;
@Enumerated(EnumType.STRING) // always STRING, never ORDINAL
@Column(nullable = false)
private OrderStatus status;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
@CreationTimestamp
@Column(updatable = false)
private Instant createdAt;
@UpdateTimestamp
private Instant updatedAt;
// Static factory, not public constructor
public static Order create(String customerEmail) {
Order order = new Order();
order.customerEmail = customerEmail;
order.status = OrderStatus.PENDING;
return order;
}
// Behavior on entity, not in service
public void addItem(Product product, int quantity) {
items.add(OrderItem.create(this, product, quantity));
}
}@Enumerated(EnumType.STRING) always — ORDINAL breaks on enum reorderingGenerationType.UUID for IDs — never expose auto-increment integers@NoArgsConstructor(access = PROTECTED) — required by JPA, hidden from app code@Getter from Lombok — no @Setter on entities (use behavior methods)= new ArrayList<>()) — never nullIdentify: One query for orders + N queries for each order's items = N+1.
Fix with JOIN FETCH:
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Optional<Order> findByIdWithItems(@Param("id") UUID id);
// For lists — use @EntityGraph to avoid duplicates
@EntityGraph(attributePaths = {"items", "items.product"})
List<Order> findByStatus(OrderStatus status);Fix with Projections for read-only views:
// Interface projection — no entity loaded
public interface OrderSummary {
UUID getId();
String getCustomerEmail();
OrderStatus getStatus();
Instant getCreatedAt();
}
List<OrderSummary> findByStatus(OrderStatus status); // fast, no lazy loading issuespublic interface OrderRepository extends JpaRepository<Order, UUID> {
// Derived query — simple conditions
List<Order> findByStatusAndCustomerEmail(OrderStatus status, String email);
// JPQL — for joins and complex conditions
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.status = :status")
List<Order> findActiveOrdersWithItems(@Param("status") OrderStatus status);
// Native SQL — only when JPQL can't do it
@Query(value = "SELECT * FROM orders WHERE created_at > NOW() - INTERVAL '7 days'",
nativeQuery = true)
List<Order> findRecentOrders();
// Exists check — faster than findById + isPresent
boolean existsByCustomerEmailAndStatus(String email, OrderStatus status);
// Projection
List<OrderSummary> findByCustomerEmail(String email);
}// Always use Pageable for list endpoints
Page<Order> findByStatus(OrderStatus status, Pageable pageable);
// In service
Page<Order> orders = orderRepository.findByStatus(status, PageRequest.of(page, size, Sort.by("createdAt").descending()));// Parent side (Order)
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
// Child side (OrderItem) — owns the FK
@ManyToOne(fetch = FetchType.LAZY) // LAZY always on @ManyToOne
@JoinColumn(name = "order_id", nullable = false)
private Order order;
// Helper on parent to keep both sides in sync
public void addItem(OrderItem item) {
items.add(item);
item.setOrder(this);
}OFFSET pagination scans and discards every skipped row. On page 5,000 the DB reads 100,000 rows to return 20. For large or infinite-scroll datasets, paginate by the last seen key (the "seek" method):
// ❌ Slow on deep pages — OFFSET grows linearly
Page<Order> findByStatus(OrderStatus status, Pageable pageable);
// ✅ Keyset — constant time regardless of depth. Pass the last row's createdAt + id.
@Query("""
SELECT o FROM Order o
WHERE o.status = :status
AND (o.createdAt < :lastCreatedAt
OR (o.createdAt = :lastCreatedAt AND o.id < :lastId))
ORDER BY o.createdAt DESC, o.id DESC
""")
List<Order> findNextPage(OrderStatus status, Instant lastCreatedAt, UUID lastId, Limit limit);The (createdAt, id) tuple breaks ties so the cursor is stable when timestamps collide. Index (status, created_at DESC, id DESC).
Saving a list one row at a time is N round-trips. Enable JDBC batching so Hibernate groups them:
spring:
jpa:
properties:
hibernate:
jdbc.batch_size: 50
order_inserts: true
order_updates: trueCaveat: GenerationType.IDENTITY silently disables insert batching (Hibernate needs the generated key per row). GenerationType.UUID or a pooled sequence preserves it — another reason to prefer UUIDs.
FetchType.EAGER — always use LAZY on @ManyToOne and @ManyToMany@Enumerated(EnumType.ORDINAL) — always use STRINGLong IDs — use UUIDfindAll() for list endpoints — always use PageableOFFSET pagination on huge tables — switch to keyset for deep pagesorphanRemoval = true on @OneToMany — child records become orphansitems access in loopsGenerationType.IDENTITY — batching is silently off; use UUID/sequence~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.