SQL Server · CrudLabDB

Upsert

Feature 10: insert the row when the code is new, update it when the code already exists - in one SQL Server MERGE statement.

MERGE … WHEN MATCHED / WHEN NOT MATCHED
Try:
Content, status and flags are not touched by this upsert.
SQLUpsertService.MERGE_SQL
MERGE article WITH (HOLDLOCK) AS t
USING (SELECT CAST(? AS NVARCHAR(40))  AS code,
              CAST(? AS NVARCHAR(200)) AS title,
              CAST(? AS NVARCHAR(500)) AS summary,
              CAST(? AS NVARCHAR(60))  AS category,
              CAST(? AS NVARCHAR(300)) AS tags,
              CAST(? AS NVARCHAR(10))  AS priority) AS s
   ON t.code = s.code
WHEN MATCHED AND t.locked = 0 THEN
    UPDATE SET t.title = s.title,
               t.summary = s.summary,
               t.category = s.category,
               t.tags = s.tags,
               t.priority = s.priority,
               t.row_version = t.row_version + 1,
               t.updated_at = SYSDATETIME()
WHEN NOT MATCHED THEN
    INSERT (code, title, summary, category, tags, priority, status, is_read, read_at)
    VALUES (s.code, s.title, s.summary, s.category, s.tags, s.priority, N'DRAFT', 1, SYSDATETIME())
OUTPUT $action AS merge_action, inserted.id AS id;
  • WITH (HOLDLOCK) keeps the key range locked until the end, so two users cannot both INSERT the same code.
  • WHEN MATCHED AND t.locked = 0: a locked row matches but no branch runs, so nothing is returned.
  • OUTPUT $action returns INSERT or UPDATE, which the page shows above.
  • CAST(? AS NVARCHAR(..)) gives each parameter a clear type for the comparison.
SQL behind this page

Captured by a Hibernate StatementInspector and by the JdbcTemplate code. ? marks a parameter: values are sent separately, which keeps SQL injection out.

Loading this page
1
SELECT
  COUNT(*)                                                                AS total,
  SUM(CASE WHEN deleted = 0 AND archived = 0 THEN 1 ELSE 0 END)           AS active,
  SUM(CASE WHEN deleted = 0 AND archived = 0 AND status = 'DRAFT' THEN 1 ELSE 0 END)       AS drafts,
  SUM(CASE WHEN deleted = 0 AND archived = 0 AND status = 'PUBLISHED' THEN 1 ELSE 0 END)   AS published,
  SUM(CASE WHEN deleted = 0 AND archived = 0 AND status = 'UNPUBLISHED' THEN 1 ELSE 0 END) AS unpublished,
  SUM(CASE WHEN deleted = 0 AND archived = 0 AND pinned = 1 THEN 1 ELSE 0 END)    AS pinned,
  SUM(CASE WHEN deleted = 0 AND favourite = 1 THEN 1 ELSE 0 END)                  AS favourites,
  SUM(CASE WHEN deleted = 0 AND archived = 0 AND is_read = 0 THEN 1 ELSE 0 END)   AS unread,
  SUM(CASE WHEN deleted = 0 AND locked = 1 THEN 1 ELSE 0 END)                     AS locked,
  SUM(CASE WHEN deleted = 0 AND archived = 1 THEN 1 ELSE 0 END)                   AS archived,
  SUM(CASE WHEN deleted = 1 THEN 1 ELSE 0 END)                                    AS trash
FROM article
2
select distinct a1_0.category from article a1_0 where a1_0.deleted=0 order by a1_0.category