20 CRUD & data-management patterns
Every pattern works on one table (article) in SQL Server. Use it in the app,
open the SQL panel to see the statements it ran, then read the Java method that did it.
Each card below says where the feature lives. Click Try it and use it on the demo articles.
After every action, the SQL button at the top shows the exact statements sent to SQL Server.
Open the Java method named on the card (in src/main/java/com/crudlab) and change it.
Activity log > Reset demo data gives you a fresh start.
CRUD basics
Insert / Create
CRUD basicsSaves a new row. The id comes from IDENTITY, the code from a SEQUENCE when you leave it empty.
ArticleService.create() -> repo.save(article)SQL show the statement
INSERT INTO article (code, title, ...) VALUES (?, ?, ...); -- SQL Server returns the new id (IDENTITY)
Update / Edit
CRUD basicsChanges the fields of one row. A hidden row_version stops you from overwriting someone else's save.
ArticleService.update() -> dirty checking, no save() call neededSQL show the statement
UPDATE article SET title = ?, ..., row_version = ? WHERE id = ? AND row_version = ?;
Delete (hard delete)
CRUD basicsRemoves the row for good. It cannot be undone, so the page asks you to confirm.
ArticleService.hardDelete() -> repo.delete(article)SQL show the statement
DELETE FROM article WHERE id = ? AND row_version = ?;
Safe delete
Soft delete
Safe deleteHides the row by setting deleted = 1. The row stays in the table and shows up in Trash.
ArticleService.softDelete()SQL show the statement
UPDATE article SET deleted = 1, deleted_at = SYSDATETIME() WHERE id = ?; -- every list adds: WHERE deleted = 0
Restore
Safe deleteBrings a soft-deleted row back by setting deleted = 0.
ArticleService.restore()SQL show the statement
UPDATE article SET deleted = 0, deleted_at = NULL WHERE id = ?;
Copy & combine
Duplicate / Clone
Copy & combineCopies a row into a new draft with a new code (CODE-COPY, CODE-COPY2 ...) and remembers the source.
ArticleService.duplicate()SQL show the statement
SELECT ... FROM article WHERE id = ?; INSERT INTO article (..., source_id) VALUES (..., ?);
Merge
Copy & combineCombines two rows into one: tags are joined, content appended, the second row goes to the trash with merged_into_id.
ArticleService.merge()SQL show the statement
UPDATE article SET content = ?, tags = ?, ... WHERE id = @keep; UPDATE article SET deleted = 1, merged_into_id = @keep WHERE id = @other;
Upsert
Copy & combineOne statement that inserts the row when the code is new and updates it when the code exists.
UpsertService.upsert() -> JdbcTemplate + MERGESQL show the statement
MERGE article WITH (HOLDLOCK) AS t USING (SELECT ? AS code, ...) AS s ON t.code = s.code WHEN MATCHED THEN UPDATE ... WHEN NOT MATCHED THEN INSERT ... OUTPUT $action;
Archive
Archive
ArchiveMoves old rows out of the everyday list without deleting them.
ArticleService.archive()SQL show the statement
UPDATE article SET archived = 1, archived_at = SYSDATETIME() WHERE id = ?;
Unarchive
ArchiveBrings an archived row back to the everyday list.
ArticleService.unarchive()SQL show the statement
UPDATE article SET archived = 0, archived_at = NULL WHERE id = ?;
Life cycle
Save draft
Life cycleSaves work that is not ready yet. Drafts have status = 'DRAFT' and their own tab.
ArticleService.create(form, false)SQL show the statement
INSERT INTO article (..., status) VALUES (..., 'DRAFT');
Publish
Life cycleMakes a row public: status = 'PUBLISHED' and the time it was published.
ArticleService.publish()SQL show the statement
UPDATE article SET status = 'PUBLISHED', published_at = SYSDATETIME() WHERE id = ?;
Unpublish
Life cycleTakes a published row offline again (status = 'UNPUBLISHED'); a draft simply stays a draft.
ArticleService.unpublish()SQL show the statement
UPDATE article SET status = 'UNPUBLISHED' WHERE id = ?;
Batch
Batch insert
BatchSaves many rows in one transaction. Compare JDBC batching with JPA saveAll() and see the time.
BatchInsertService.insert() -> jdbc.batchUpdate(...) or repo.saveAll(...)SQL show the statement
INSERT INTO article (code, title, ...) VALUES (?, ?, ...); -- x N, sent in batches of 500
Batch update
BatchChanges category, priority or status of all ticked rows with ONE UPDATE statement. Locked rows are skipped.
ArticleRepository.batchUpdate() -> JPQL bulk updateSQL show the statement
UPDATE article SET category = COALESCE(?, category), ... WHERE id IN (?, ?, ?) AND locked = 0;
Batch delete
BatchMoves all ticked rows to the trash, or deletes them for good, with one statement.
ArticleRepository.batchSoftDelete() / batchHardDelete()SQL show the statement
UPDATE article SET deleted = 1 WHERE id IN (...) AND locked = 0; DELETE FROM article WHERE id IN (...) AND locked = 0;
Flags
Pin
FlagsKeeps important rows at the top of every list: ORDER BY pinned DESC comes first.
ArticleService.setPinned()SQL show the statement
UPDATE article SET pinned = 1, pinned_at = SYSDATETIME() WHERE id = ?; SELECT ... ORDER BY pinned DESC, updated_at DESC;
Mark as favourite
FlagsA personal star. The Favourites tab lists them.
ArticleService.setFavourite()SQL show the statement
UPDATE article SET favourite = 1 WHERE id = ?;
Mark as read
FlagsUnread rows are bold with a blue dot. Opening an article marks it as read; you can also mark it unread again.
ArticleService.setRead() / openAndMarkRead()SQL show the statement
UPDATE article SET is_read = 1, read_at = SYSDATETIME() WHERE id = ?;
Lock record
FlagsA locked row cannot be edited, deleted, archived, published or merged until it is unlocked. Upsert and batch actions skip it.
ArticleService.lock() + requireUnlocked() in every changing methodSQL show the statement
UPDATE article SET locked = 1, locked_at = SYSDATETIME(), locked_reason = ? WHERE id = ?;
How the project is organised
src/main/java/com/crudlab ├─ domain/ Article, ActivityLog (JPA entities), enums ├─ repo/ ArticleRepository (Spring Data + JPQL bulk updates) │ ArticleSpecs (dynamic WHERE), ArticleCounts ├─ service/ ArticleService features 1-9, 11-13, 15-20 │ UpsertService feature 10 (MERGE) │ BatchInsertService feature 14 (batchUpdate) ├─ web/ ArticleController (pages), ArticleApiController (REST) │ ToolsController, FeatureCatalog ├─ sql/ SqlTrace + Hibernate StatementInspector └─ seed/ DemoDataService (demo articles, reset) src/main/resources ├─ db/migration/V1__create_tables.sql (Flyway) ├─ templates/ Thymeleaf pages └─ static/ css + js
- One method = one transaction.
@Transactionalon the service commits all statements or none. - Lock checks live in the service (
requireUnlocked), so the pages and the REST API follow the same rule. - Soft delete is a filter. Every list adds
deleted = 0; Trash is justdeleted = 1. - Bulk JPQL updates skip the entity: no
@Version, no@PreUpdate, so the query setsrow_versionandupdated_atitself. - Post / Redirect / Get. Every POST answers with a redirect, so F5 never repeats an action.
- Parameters, never string concatenation. Every value is sent as
?, which stops SQL injection.