jpa-postgres
Skill0 starsChanged 26 days agoMITView on GitHub
Provides expert knowledge of JPA entity design, relationship mapping, Flyway migrations, and PostgreSQL-specific optimizations.
- Deletes or force-pushes
In this repository219 other files it gives its agents.
- .agents/AGENTS.mdAGENTS.md
- AGENTS.mdAGENTS.md
- .claude/CLAUDE.mdCLAUDE.md
- CLAUDE.mdCLAUDE.md
- .cursor/rules/behavioral-discipline.mdcCursor rule
- .cursor/rules/context-budget.mdcCursor rule
- .cursor/rules/orchestration.mdcCursor rule
- .cursor/rules/yagni-stdlib.mdcCursor rule
- androidSkill
- android-ui-designSkill
- api-versioningSkill
- astroSkill
- auth-token-inspectorSkill
- backlogSkill
- cc-api-contractSkill
- cc-db-migrationSkill
- cc-featureSkill
- cc-fixSkill
- cc-pagespeedSkill
- cc-refactorSkill
- cc-reviewSkill
- cc-spec-mutationSkill
- cc-tdd-cycleSkill
- cc-test-planSkill
- code-reviewSkill
- commitSkill
- conductor-setupSkill
- councilSkill
- database-migrationsSkill
- django-ormSkill
- django-testingSkill
- django-uvSkill
- drizzle-schema-architectSkill
- evaluationSkill
- fastapi-pydantic-strictSkill
- find-skillsSkill
- jpa-nplusone-detectorSkill
- laravel-specialistSkill
- livewire-alpine-bridgeSkill
- multi-agent-orchestrationSkill
- nextjs-typescriptSkill
- openspecSkill
- pagespeed-insightsSkill
- pagespeed-perfSkill
- php-proSkill
- python-django-stackSkill
- python-fastapi-stackSkill
- pythonSkill
- rust-asyncSkill
- rust-engineeringSkill
- rust-sqlxSkill
- security-ai-llmSkill
- security-blue-teamSkill
- security-cloudSkill
- security-cryptoSkill
- security-exploit-devSkill
- security-grcSkill
- security-incident-responseSkill
- security-log-analysisSkill
- security-malware-analysisSkill
- The first 60 shown.
The file
Show its rules and text
We split this skill into the rules it gives an agent
18 rules. About 0 are common, 9 are distinctive, and 9 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈9
Distinctive · estimate
This author's own choices. Worth a look.
9
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 51
Use UUIDs. Do not use auto-increment integers as public-facing identifiers.
- Distinctive: line 113
Do not delete rows — mark them as deleted and filter them transparently.
- Distinctive: line 146
Always be explicit. Never rely on Hibernate defaults.
- Very specific: line 241
Do not use @ManyToMany directly.
- Very specific: line 323
Use DISTINCT with JOIN FETCH on collections to avoid duplicate parent rows in the result.
- Very specific: line 326
Rule: never access a lazy collection outside a transaction.
- Distinctive: line 332
When you only need a subset of fields, do not load the full entity.
- Distinctive: line 360
Use projections for list endpoints and reports.
- Distinctive: line 371
Use a timestamp, not a sequential number, to avoid conflicts in parallel branches.
- Distinctive: line 382
Never modify a migration that has already been applied in any environment
- Distinctive: line 408
Document rollback SQL in comments at the top of each migration file:
- Very specific: line 432
Use TIMESTAMPTZ (with timezone), not TIMESTAMP.
- Very specific: line 436
Use JSONB for semi-structured data that does not warrant its own table.
- Very specific: line 460
Use RETURNING to avoid an extra SELECT after an INSERT or UPDATE:
- Distinctive: line 476
Use native queries when JPQL cannot express what you need:
- Very specific: line 502
Always paginate. Never call findAll() on a large table.
- Very specific: line 518
Use Slice<T> instead of Page<T> when you do not need the total count (cheaper — no COUNT query).
- Very specific: line 619
Use @DataJpaTest with H2 for:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
JPA + PostgreSQL
Entity Design
Primary Keys
Use UUIDs. Do not use auto-increment integers as public-facing identifiers.
@Entity
@Table(name = "users")
class User(
@Id
@GeneratedValue(strategy = GenerationType.UUID)
val id: UUID = UUID.randomUUID(),
@Column(name = "email", nullable = false, unique = true, length = 255)
var email: String,
@Column(name = "name", nullable = false, length = 100)
var name: String
)UUID generation strategy GenerationType.UUID is available in Hibernate 6+
(Spring Boot 3+). For earlier versions, use @UuidGenerator from Hibernate or
generate manually.
Auditing
Enable automatic timestamp management with Spring Data auditing.
// Enable in main application class or a @Configuration class
@EnableJpaAuditing
@SpringBootApplication
class Application
// Base class for auditable entities
@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
abstract class AuditableEntity {
@Column(name = "created_at", nullable = false, updatable = false)
@CreatedDate
lateinit var createdAt: Instant
@Column(name = "updated_at", nullable = false)
@LastModifiedDate
lateinit var updatedAt: Instant
}
// Entity extends the base class
@Entity
@Table(name = "users")
class User(
@Id @GeneratedValue(strategy = GenerationType.UUID)
val id: UUID = UUID.randomUUID(),
@Column(name = "email", nullable = false, unique = true)
var email: String,
var name: String
) : AuditableEntity()Soft Delete
Do not delete rows — mark them as deleted and filter them transparently.
@Entity
@Table(name = "users")
@SQLDelete(sql = "UPDATE users SET deleted_at = NOW() WHERE id = ?")
@FilterDef(name = "deletedFilter", parameters = [ParamDef(name = "isDeleted", type = Boolean::class)])
@Filter(name = "deletedFilter", condition = "deleted_at IS NULL")
class User(
@Id @GeneratedValue(strategy = GenerationType.UUID)
val id: UUID = UUID.randomUUID(),
@Column(name = "email", nullable = false, unique = true)
var email: String,
@Column(name = "deleted_at")
var deletedAt: Instant? = null
)Alternative using @Where (simpler, but applies globally without ability to
disable):
@Entity
@Table(name = "users")
@SQLDelete(sql = "UPDATE users SET deleted_at = NOW() WHERE id = ?")
@Where(clause = "deleted_at IS NULL")
class User(...)Column Annotations
Always be explicit. Never rely on Hibernate defaults.
@Column(
name = "email", // explicit column name
nullable = false, // maps to NOT NULL constraint
unique = true, // maps to UNIQUE constraint
length = 255 // VARCHAR(255) — ignored for TEXT type
)
var email: StringFor PostgreSQL TEXT type (unbounded), use columnDefinition:
@Column(name = "description", nullable = false, columnDefinition = "TEXT")
var description: StringIndexes
Define indexes on the entity, not in migration scripts, for searchable fields.
@Entity
@Table(
name = "users",
indexes = [
Index(name = "idx_users_email", columnList = "email", unique = true),
Index(name = "idx_users_created_at", columnList = "created_at")
]
)
class User(...)For partial indexes (e.g., soft-delete), use a migration script — JPA cannot express partial indexes.
Relationship Mapping
One-to-Many
@Entity
@Table(name = "orders")
class Order(
@Id @GeneratedValue(strategy = GenerationType.UUID)
val id: UUID = UUID.randomUUID(),
@OneToMany(
mappedBy = "order",
cascade = [CascadeType.ALL],
fetch = FetchType.LAZY, // ALWAYS lazy by default
orphanRemoval = true
)
val items: MutableList<OrderItem> = mutableListOf()
)
@Entity
@Table(name = "order_items")
class OrderItem(
@Id @GeneratedValue(strategy = GenerationType.UUID)
val id: UUID = UUID.randomUUID(),
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id", nullable = false)
val order: Order,
@Column(name = "quantity", nullable = false)
var quantity: Int
)Rules:
fetch = FetchType.LAZYis the default for@OneToMany— make it explicit anywayCascadeType.ALLonly when the child lifecycle is completely owned by the parentorphanRemoval = truewhen removing an item from the collection should delete the row
Many-to-One
@ManyToOne(fetch = FetchType.LAZY) // LAZY — never EAGER
@JoinColumn(name = "user_id", nullable = false)
val user: UserFetchType.EAGER on @ManyToOne causes N+1 problems. Always use LAZY and
fetch eagerly with JOIN FETCH when needed.
Many-to-Many
Do not use @ManyToMany directly. Use a join entity with explicit fields.
// bad — opaque join table, no room for additional fields
@ManyToMany
@JoinTable(name = "user_roles")
val roles: MutableSet<Role>
// good — explicit join entity
@Entity
@Table(name = "user_roles")
class UserRole(
@Id @GeneratedValue(strategy = GenerationType.UUID)
val id: UUID = UUID.randomUUID(),
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
val user: User,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "role_id", nullable = false)
val role: Role,
@Column(name = "assigned_at", nullable = false)
val assignedAt: Instant = Instant.now()
)The join entity approach is more flexible — you can add fields like
assignedAt, assignedBy, and query through a repository.
N+1 Problem
The N+1 problem occurs when loading a collection results in one query for the parent and N additional queries for each child, where N is the number of parents.
Detection:
Enable SQL logging in tests:
# application-test.yml
spring:
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql.BasicBinder: TRACECount the queries. If you see SELECT * FROM order_items WHERE order_id = ?
executed once per order, you have N+1.
Fix with @EntityGraph:
@Repository
interface OrderRepository : JpaRepository<Order, UUID> {
@EntityGraph(attributePaths = ["items", "items.product"])
fun findWithItemsById(id: UUID): Optional<Order>
@EntityGraph(attributePaths = ["items"])
fun findAllWithItems(): List<Order>
}Fix with JPQL JOIN FETCH:
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
fun findByIdWithItems(@Param("id") id: UUID): Optional<Order>
@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items WHERE o.status = :status")
fun findByStatusWithItems(@Param("status") status: OrderStatus): List<Order>Use DISTINCT with JOIN FETCH on collections to avoid duplicate parent rows
in the result.
Rule: never access a lazy collection outside a transaction. This causes
LazyInitializationException. If a service method needs related data, fetch it
within the same transaction using @EntityGraph or JOIN FETCH.
Projections for Read Queries
When you only need a subset of fields, do not load the full entity. Use projections.
Interface projection:
interface UserSummary {
val id: UUID
val email: String
val name: String
}
@Repository
interface UserRepository : JpaRepository<User, UUID> {
fun findAllProjectedBy(): List<UserSummary>
fun findProjectedById(id: UUID): UserSummary?
}DTO projection with constructor expression:
data class UserDto(val id: UUID, val email: String)
@Query("SELECT new com.example.user.dto.UserDto(u.id, u.email) FROM User u WHERE u.active = true")
fun findActiveUserDtos(): List<UserDto>Use projections for list endpoints and reports. Load full entities only when you need to modify them.
Flyway Migrations
Naming Convention
V{timestamp}__{description}.sqlUse a timestamp, not a sequential number, to avoid conflicts in parallel branches.
V20260507120000__create_users_table.sql
V20260507120001__add_users_email_index.sql
V20260508090000__add_orders_table.sqlMigration Rules
- Never modify a migration that has already been applied in any environment
- Each migration must be forward-only — Flyway does not support automatic rollbacks
- DDL changes and data migrations must be in separate files
- Destructive operations (DROP COLUMN, DROP TABLE) require a multi-step process:
- Migration 1: deploy code that no longer uses the column
- Migration 2: drop the column (safe once all instances are updated)
Schema Migration Example
-- V20260507120000__create_users_table.sql
CREATE TABLE users (
id UUID NOT NULL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE UNIQUE INDEX idx_users_email ON users (email) WHERE deleted_at IS NULL;Rollback Strategy
Document rollback SQL in comments at the top of each migration file:
-- V20260507120001__add_user_role_column.sql
-- Rollback: ALTER TABLE users DROP COLUMN role;
ALTER TABLE users ADD COLUMN role VARCHAR(50) NOT NULL DEFAULT 'USER';Automated rollback is not used. Manual rollback is applied only in emergencies.
PostgreSQL Specifics
Column Types
| Use case | PostgreSQL type | JPA mapping |
| Short text, known max length | VARCHAR(n) | @Column(length = n) |
| Long text, no known max | TEXT | @Column(columnDefinition = "TEXT") |
| Structured semi-structured data | JSONB | @Column(columnDefinition = "JSONB") |
| Timestamps with timezone | TIMESTAMPTZ | Instant |
| UUIDs | UUID | UUID |
| Money/currency | NUMERIC(19,4) | BigDecimal |
Use TIMESTAMPTZ (with timezone), not TIMESTAMP. Store all timestamps in UTC.
JSONB
Use JSONB for semi-structured data that does not warrant its own table.
@Column(name = "metadata", columnDefinition = "JSONB")
@Type(JsonType::class) // requires hypersistence-utils dependency
var metadata: Map<String, Any> = emptyMap()JSONB is indexable. JSON is not. Always use JSONB.
Partial Indexes
Partial indexes cannot be expressed through JPA annotations. Use a migration:
-- For soft-deleted tables: only index active (non-deleted) rows
CREATE UNIQUE INDEX idx_users_email_active ON users (email) WHERE deleted_at IS NULL;
-- For status-filtered queries
CREATE INDEX idx_orders_pending ON orders (created_at) WHERE status = 'PENDING';Native Queries with RETURNING
Use RETURNING to avoid an extra SELECT after an INSERT or UPDATE:
@Modifying
@Query(
value = "INSERT INTO audit_log (user_id, action, created_at) VALUES (:userId, :action, NOW()) RETURNING id",
nativeQuery = true
)
fun insertAndReturnId(
@Param("userId") userId: UUID,
@Param("action") action: String
): UUIDComplex Queries
Use native queries when JPQL cannot express what you need:
@Query(
value = """
SELECT u.id, u.email, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id AND o.deleted_at IS NULL
WHERE u.deleted_at IS NULL
GROUP BY u.id, u.email
HAVING COUNT(o.id) > :minOrders
ORDER BY order_count DESC
LIMIT :limit
""",
nativeQuery = true
)
fun findActiveUsersWithMinOrders(
@Param("minOrders") minOrders: Int,
@Param("limit") limit: Int
): List<Map<String, Any>>Query Performance
Pagination
Always paginate. Never call findAll() on a large table.
@Repository
interface UserRepository : JpaRepository<User, UUID> {
fun findAll(pageable: Pageable): Page<UserSummary>
fun findByRole(role: UserRole, pageable: Pageable): Page<UserSummary>
}
// In service
fun list(page: Int, size: Int): Page<UserSummary> {
val pageable = PageRequest.of(page, size, Sort.by("createdAt").descending())
return userRepository.findAll(pageable)
}Use Slice<T> instead of Page<T> when you do not need the total count
(cheaper — no COUNT query).
QueryHints for Fetch Control
@QueryHints(
QueryHint(name = HINT_FETCHGRAPH, value = "User.withOrders")
)
fun findById(id: UUID): Optional<User>Requires a named entity graph:
@Entity
@NamedEntityGraph(
name = "User.withOrders",
attributeNodes = [NamedAttributeNode("orders")]
)
class User(...)Testing
Unit — @DataJpaTest with H2
Fast. No server required.
@DataJpaTest
class UserRepositoryTest {
@Autowired
private lateinit var userRepository: UserRepository
@Test
fun `should find user by email`() {
val saved = userRepository.save(User(email = "test@example.com", name = "Test User"))
val found = userRepository.findByEmail("test@example.com")
assertThat(found).isNotNull
assertThat(found?.id).isEqualTo(saved.id)
}
@Test
fun `should not find deleted user by email`() {
val user = userRepository.save(User(email = "deleted@example.com", name = "Gone"))
userRepository.delete(user) // triggers @SQLDelete — sets deleted_at
val found = userRepository.findByEmail("deleted@example.com")
assertThat(found).isNull()
}
}Integration — Testcontainers with PostgreSQL
Real database. Use for migrations, JSONB, partial indexes, and native queries.
@SpringBootTest
@Testcontainers
class UserRepositoryIntegrationTest {
companion object {
@Container
@JvmStatic
val postgres = PostgreSQLContainer<Nothing>("postgres:16").apply {
withDatabaseName("testdb")
withUsername("test")
withPassword("test")
}
@DynamicPropertySource
@JvmStatic
fun registerProperties(registry: DynamicPropertyRegistry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl)
registry.add("spring.datasource.username", postgres::getUsername)
registry.add("spring.datasource.password", postgres::getPassword)
}
}
@Autowired
private lateinit var userRepository: UserRepository
@Test
fun `should enforce unique email constraint at db level`() {
userRepository.save(User(email = "dup@example.com", name = "First"))
assertThrows<DataIntegrityViolationException> {
userRepository.save(User(email = "dup@example.com", name = "Second"))
}
}
}Use Testcontainers for:
- Flyway migration validation
- PostgreSQL-specific features (JSONB, partial indexes, native queries)
- Constraint enforcement (unique, foreign key, check constraints)
- Query performance checks that differ between H2 and PostgreSQL
Use @DataJpaTest with H2 for:
- Derived query methods
- Simple CRUD operations
- JPQL queries with standard JPA behavior
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

