hugsql 中文文档

Table of Contents

1. 介绍

HugSQL 是一个用于拥抱 SQL 的 Clojure 库.

  • 当处理关系型数据库时, SQL 是完成工作的正确工具!
  • HugSQL 在你的 SQL 文件中使用简单的约定来在 Clojure 命名空间中定义数据库函数, 从而实现 Clojure 和 SQL 代码的清晰分离.
  • HugSQL 支持运行时的以下替换:
    • SQL Values (值): where id = :id
    • SQL Value Lists (值列表): where id in (:v*:ids)
    • SQL Identifiers (标识符): from :i:table-name
    • SQL Identifier Lists (标识符列表): select :i*:column-names
    • SQL Tuples (元组): where (a.id, b.id) = (:t:ids)
    • SQL Tuple Lists (元组列表): insert into emp (id, name) values (:t*:people)
    • Raw SQL (原生 SQL)
    • Custom Parameter Types (自定义参数类型, 可自行实现)
  • HugSQL 具有 Clojure Expressions (表达式) 和 Snippets (片段) 功能, 在构建复杂的 SQL 查询时, 提供了 Clojure 的完整表达能力以及部分 SQL 语句的可组合性.
  • HugSQL 拥有基于协议 (protocol-based) 的适配器, 支持多种数据库库, 并随附了对 clojure.java.jdbc (默认), next.jdbcclojure.jdbc 的适配器支持.

2. 安装

Dependencies (依赖)

deps.edn

com.layerware/hugsql {:mvn/version "0.5.3"}

Leiningen

[com.layerware/hugsql "0.5.3"]

JDBC Driver Dependencies (JDBC 驱动依赖)

你还需要指定以下一种 JDBC 驱动依赖:

  • Apache Derby
  • H2
  • MS SQL Server
  • MySQL
  • Oracle
  • Postgresql
  • SQLite

例如, 要提供对 Postgresql 的支持:

Postgresql JDBC deps.edn

org.postgresql/postgresql {:mvn/version "42.3.1"}

Postgresql JDBC Leiningen

[org.postgresql/postgresql "42.3.1"]

HugSQL 默认使用 clojure.java.jdbc 库来运行底层的数据库命令. 如果你更喜欢使用除 clojure.java.jdbc 之外的其他底层数据库库(例如 next.jdbc), 请参阅 HugSQL Adapters 章节.

3. Getting Started

本文档使用 HugSQL 代码库中的 "The Princess Bride" (公主新娘) 示例应用程序. 请随意查看或克隆该仓库, 并按需使用 lein 运行该应用程序.

3.1. Start with SQL (从 SQL 开始)

HugSQL 提供了 SQL 和 Clojure 代码的清晰分离. 你可以通过决定将 SQL 文件放置在应用程序中的何处来开始使用 HugSQL 进行开发.

HugSQL 可以在 classpath 中找到任何 SQL 文件. 你可以将 SQL 文件放在 resources, src 或 classpath 中的其他位置.

我们的示例应用程序将 SQL 文件放在 src 下:

.../examples/princess-bride $ tree
.
├── LICENSE
├── project.clj
├── README.md
├── resources
├── src
│   └── princess_bride
│       ├── core.clj
│       ├── db
│       │   ├── characters.clj
│       │   ├── quotes.clj
│       │   └── sql
│       │       ├── characters.sql
│       │       └── quotes.sql
│       └── db.clj

我们针对 "The Princess Bride" 角色的 SQL 如下所示:

-- src/princess_bride/db/sql/characters.sql
-- The Princess Bride Characters

-- :name create-characters-table
-- :command :execute
-- :result :raw
-- :doc Create characters table
--  auto_increment and current_timestamp are
--  H2 Database specific (adjust to your DB)
create table characters (
  id         integer auto_increment primary key,
  name       varchar(40),
  specialty  varchar(40),
  created_at timestamp not null default current_timestamp
)

/* ...snip... */

-- A :result value of :n below will return affected rows:
-- :name insert-character :! :n
-- :doc Insert a single character returning affected row count
insert into characters (name, specialty)
values (:name, :specialty)

-- :name insert-characters :! :n
-- :doc Insert multiple characters with :tuple* parameter type
insert into characters (name, specialty)
values :tuple*:characters

/* ...snip... */

-- A ":result" value of ":1" specifies a single record
-- (as a hashmap) will be returned
-- :name character-by-id :? :1
-- :doc Get character by id
select * from characters
where id = :id

-- Let's specify some columns with the
-- identifier list parameter type :i* and
-- use a value list parameter type :v* for IN()
-- :name characters-by-ids-specify-cols :? :*
-- :doc Characters with returned columns specified
select :i*:cols from characters
where id in (:v*:ids)

HugSQL 使用特殊的 SQL 注释来完成其工作. 这些约定将在本文档后面部分进行解释. 请继续阅读!

3.2. Now Some Clojure (现在来点 Clojure)

现在我们编写一些 Clojure 代码来定义我们的数据库函数.

(ns princess-bride.db.characters
  (:require [hugsql.core :as hugsql]))

;; The path is relative to the classpath (not proj dir!),
;; so "src" is not included in the path.
;; The same would apply if the sql was under "resources/..."
;; Also, notice the under_scored path compliant with
;; Clojure file paths for hyphenated namespaces
(hugsql/def-db-fns "princess_bride/db/sql/characters.sql")

;; For most HugSQL usage, you will not need the sqlvec functions.
;; However, sqlvec versions are useful during development and
;; for advanced usage with database functions.
(hugsql/def-sqlvec-fns "princess_bride/db/sql/characters.sql")

princess-bride.db.characters 命名空间现在拥有了基于 SQL 文件中的 SQL 语句定义的几个函数. 下面是 sqlvec 输出的示例以及 characters-by-ids-specify-cols 函数的运行示例:

(characters/characters-by-ids-specify-cols-sqlvec
  {:ids [1 2], :cols ["name" "specialty"]})  ;;=>
["select name, specialty from characters
  where id in (?,?)"
,1
,2]

(characters/characters-by-ids-specify-cols db
  {:ids [1 2], :cols ["name" "specialty"]})  ;;=>
({:name "Westley", :specialty "love"}
 {:name "Buttercup", :specialty "beauty"})

你仅仅触及了 HugSQL 功能的皮毛. 请继续阅读以获取完整的使用示例.

4. 使用hugsql

本节提供了 HugSQL 的实际使用示例. 大多数示例来自 HugSQL 仓库中的 "The Princess Bride" 示例应用程序.

4.1. def-db-fns

hugsql.core/def-db-fns 基于 HugSQL 风格的 SQL 文件中的查询和语句, 在你的 Clojure 命名空间中创建函数.

hugsql.core/def-db-fns 文档:

=> (doc hugsql.core/def-db-fns)
-------------------------
hugsql.core/def-db-fns
([file] [file options])
Macro
  Given a HugSQL SQL file, define the database
   functions in the current namespace.

   Usage:

   (def-db-fns file options?)

   where:
    - file is a string file path in your classpath,
      a resource object (java.net.URL),
      or a file object (java.io.File)
    - options (optional) hashmap:
      {:quoting :off(default) | :ansi | :mysql | :mssql
       :adapter adapter }

   :quoting options for identifiers are:
     :ansi double-quotes: "identifier"
     :mysql backticks: `identifier`
     :mssql square brackets: [identifier]
     :off no quoting (default)

   Identifiers containing a period/dot . are split, quoted separately,
   and then rejoined. This supports myschema.mytable conventions.

   :quoting can be overridden as an option in the calls to functions
   created by def-db-fns.

   :adapter specifies the HugSQL adapter to use for all defined
   functions. The default adapter used is
   (hugsql.adapter.clojure-java-jdbc/hugsql-adapter-clojure-java-jdbc)
   when :adapter is not given.

   See also hugsql.core/set-adapter! to set adapter for all def-db-fns
   calls.  Also, :adapter can be specified for individual function
   calls (overriding set-adapter! and the :adapter option here).

def-db-fns 定义的函数具有以下参数数量 (arity):

[db]
[db params]
[db params options & command-options]

其中:

  • db 是一个 db-spec, 连接, 连接池或事务对象.
  • params 是参数数据的哈希映射 (hashmap), 其中的键与 SQL 中的参数占位符名称相匹配.
  • options 是 HugSQL 特定选项的哈希映射(例如 :quoting:adapter).
  • & command-options 是传递给底层适配器和数据库库函数的可变数量的选项. 有关更多详细信息, 请参阅 "Advanced Usage" (高级用法).

4.2. def-sqlvec-fns

HugSQL 生成一种内部称为 sqlvec 的格式. sqlvec 格式是一个向量, 第一个位置是包含任何 ? 占位符的 SQL 字符串, 随后是按位置顺序应用于 SQL 的任意数量的参数值. 例如:

["select * from characters where id = ?", 2]

sqlvec 格式是 clojure.java.jdbc, clojure.jdbcnext.jdbc 用于值参数替换的约定. 由于这些库的底层支持以及 JDBC 驱动程序特定的数据类型处理问题, HugSQL 默认也使用 sqlvec 格式作为值参数.

HugSQL 提供了 hugsql.core/def-sqlvec-fns 来创建返回 sqlvec 格式的函数. 创建的函数默认带有 -sqlvec 后缀, 但这可以通过 :fn-suffix 选项进行配置. 这些函数在开发/调试期间非常有用, 并且可以在不使用内置适配器数据库函数执行查询的情况下使用 HugSQL 的参数替换功能.

hugsql.core/def-sqlvec-fns 文档:

=> (doc hugsql.core/def-sqlvec-fns)
-------------------------
hugsql.core/def-sqlvec-fns
([file] [file options])
Macro
  Given a HugSQL SQL file, define the <name>-sqlvec functions in the
  current namespace.  Returns sqlvec format: a vector of SQL and
  parameter values. (e.g., ["select * from test where id = ?" 42])

  Usage:

   (def-sqlvec-fns file options?)

   where:
    - file is a string file path in your classpath,
      a resource object (java.net.URL),
      or a file object (java.io.File)
    - options (optional) hashmap:
      {:quoting :off(default) | :ansi | :mysql | :mssql
       :fn-suffix "-sqlvec" (default)

   :quoting options for identifiers are:
     :ansi double-quotes: "identifier"
     :mysql backticks: =identifier=
     :mssql square brackets: [identifier]
     :off no quoting (default)

   Identifiers containing a period/dot . are split, quoted separately,
   and then rejoined. This supports myschema.mytable conventions.

   :quoting can be overridden as an option in the calls to functions
   created by def-db-fns.

   :fn-suffix is appended to the defined function names to
   differentiate them from the functions defined by def-db-fns.

4.3. Other Useful Functions (其他有用的函数)

HugSQL 还有其他几个有用的函数, 它们接受 HugSQL 风格的 SQL 并可以在其他上下文(如 REPL)中使用. 有关以下内容的更多信息, 请参阅 API 文档:

  • def-db-fns-from-string
  • def-sqlvec-fns-from-string
  • map-of-db-fns
  • map-of-sqlvec-fns
  • map-of-db-fns-from-string
  • map-of-sqlvec-fns-from-string
  • sqlvec (及其别名 snip)
  • sqlvec-fn (及其别名 snip-fn)
  • db-run
  • db-fn

4.4. DDL (Create, Drop)

-- :name create-characters-table
-- :command :execute
-- :result :raw
-- :doc Create characters table
-- auto_increment and current_timestamp are
-- H2 Database specific (adjust to your DB)
create table characters (
id integer auto_increment primary key,
name varchar(40),
specialty varchar(40),
created_at timestamp not null default current_timestamp
)

/* The create-character-table definition above uses the full,
long-hand "-- :key :value" syntax to specify the :command and
:result. We can save some typing by using the short-hand notation
as the second and (optionally) third values for the :name. Below, the
:! is equivalent to ":command :!", where :! is an alias for
:execute. The default :result is :raw when not specified, so
there is no need to specify it as the third value. */

-- :name drop-characters-table :!
-- :doc Drop characters table if exists
drop table if exists characters

4.5. Insert

-- A :result value of :n below will return affected row count:
-- :name insert-character :! :n
-- :doc Insert a single character
insert into characters (name, specialty)
values (:name, :specialty)

-- :name insert-characters :! :n
-- :doc Insert multiple characters with :tuple* parameter type
insert into characters (name, specialty)
values :tuple*:characters
(characters/insert-character-sqlvec
 {:name "Westley", :specialty "love"})  ;;=>
["insert into characters (name, specialty)
  values (?, ?)"
,"Westley"
,"love"]

(characters/insert-character db
  {:name "Westley", :specialty "love"})  ;;=>
1

(characters/insert-character db
  {:name "Buttercup", :specialty "beauty"})  ;;=>
1

(characters/insert-characters-sqlvec
 {:characters
  [["Vizzini" "intelligence"]
   ["Fezzik" "strength"]
   ["Inigo Montoya" "swordmanship"]]})  ;;=>
["insert into characters (name, specialty)
  values (?,?),(?,?),(?,?)"
,"Vizzini"
,"intelligence"
,"Fezzik"
,"strength"
,"Inigo Montoya"
,"swordmanship"]

(characters/insert-characters
 db
 {:characters
  [["Vizzini" "intelligence"]
   ["Fezzik" "strength"]
   ["Inigo Montoya" "swordmanship"]]})  ;;=>
3

Retrieving Last Inserted ID or Record (检索最后插入的 ID 或记录)

通常情况下, 你希望返回刚刚插入的记录, 或者至少是自动生成的 ID. 此功能在不同的数据库和 JDBC 驱动程序之间差异很大. HugSQL 试图在可能的情况下提供帮助. 你需要选择一个适合你的数据库的选项.

Option #1: INSERT … RETURNING

如果你的数据库支持 INSERTRETURNING 子句(例如 Postgresql 支持此功能), 你可以将 SQL 插入语句的命令类型指定为 :returning-execute, 或者简写为 :<!:

-- :name insert-into-test-table-returning :<!
-- :doc insert with an sql returning clause
insert into test (id, name) values (:id, :name) returning id

Option #2: Get Generated Keys / Last Insert ID / Inserted Record

HugSQL 的 :insert:i! 命令类型指示底层适配器应执行插入, 然后调用 jdbc 驱动程序中的 .getGeneratedKeys. .getGeneratedKeys 的返回值在不同的数据库和 jdbc 驱动程序之间差异很大. 例如, 请参阅 HugSQL 测试套件中的以下代码:

-- :name insert-into-test-table-return-keys :insert :raw
insert into test (id, name) values (:id, :name)
(testing "insert w/ return of .getGeneratedKeys"
;; return generated keys, which has varying support and return values
;; clojure.java.jdbc returns a hashmap, clojure.jdbc returns a vector of hashmaps
  (when (= adapter-name :clojure.java.jdbc)
    (condp = db-name
      :postgresql
      (is (= {:id 8 :name "H"}
          (insert-into-test-table-return-keys db {:id 8 :name "H"} {})))

      :mysql
      (is (= {:generated_key 9}
             (insert-into-test-table-return-keys db {:id 9 :name "I"})))

      :sqlite
      (is (= {(keyword "last_insert_rowid()") 10}
             (insert-into-test-table-return-keys db {:id 10 :name "J"} {})))

      :h2
      (is (= {(keyword "scope_identity()") 11}
             (insert-into-test-table-return-keys db {:id 11 :name "J"} {})))

      ;; hsql and derby don't seem to support .getGeneratedKeys
      nil))

  (when (= adapter-name :clojure.jdbc)
    (condp = db-name
      :postgresql
      (is (= [{:id 8 :name "H"}]
             (insert-into-test-table-return-keys db {:id 8 :name "H"} {})))

      :mysql
      (is (= [{:generated_key 9}]
             (insert-into-test-table-return-keys db {:id 9 :name "I"})))

      :sqlite
      (is (= [{(keyword "last_insert_rowid()") 10}]
             (insert-into-test-table-return-keys db {:id 10 :name "J"} {})))

      :h2
      (is (= [{(keyword "scope_identity()") 11}]
             (insert-into-test-table-return-keys db {:id 11 :name "J"} {})))

      ;; hsql and derby don't seem to support .getGeneratedKeys
      nil)))

4.6. Update

-- :name update-character-specialty :! :n
update characters
set specialty = :specialty
where id = :id
(let [vizzini (characters/character-by-name db {:name "vizzini"})]
 (characters/update-character-specialty-sqlvec
  {:id (:id vizzini), :specialty "boasting"}))  ;;=>
["update characters
  set specialty = ?
  where id = ?"
,"boasting"
,3]

(let [vizzini (characters/character-by-name db {:name "vizzini"})]
 (characters/update-character-specialty db
  {:id (:id vizzini), :specialty "boasting"}))  ;;=>
1

4.7. Delete

-- :name delete-character-by-id :! :n
delete from characters where id = :id
(let [vizzini (characters/character-by-name db {:name "vizzini"})]
  (characters/delete-character-by-id-sqlvec {:id (:id vizzini)}))  ;;=>
["delete from characters where id = ?",3]

(let [vizzini (characters/character-by-name db {:name "vizzini"})]
  (characters/delete-character-by-id db {:id (:id vizzini)}))  ;;=>
1

4.8. Select

-- A ":result" value of ":*" specifies a vector of records
-- (as hashmaps) will be returned
-- :name all-characters :? :*
-- :doc Get all characters
select * from characters
order by id

-- A ":result" value of ":1" specifies a single record
-- (as a hashmap) will be returned
-- :name character-by-id :? :1
-- :doc Get character by id
select * from characters
where id = :id

-- :name character-by-name :? :1
-- :doc Get character by case-insensitive name
select * from characters
where upper(name) = upper(:name)

-- :name characters-by-name-like :?
-- :doc Get characters by name like, :name-like should include % wildcards
select * from characters
where name like :name-like

-- Let's specify some columns with the
-- identifier list parameter type :i* and
-- use a value list parameter type :v* for SQL IN()
-- :name characters-by-ids-specify-cols :? :*
-- :doc Characters with returned columns specified
select :i*:cols from characters
where id in (:v*:ids)

4.9. Transactions

def-db-fns 生成的函数的第一个参数可以接受数据库 spec, 连接, 连接池或事务对象. 请使用你的底层数据库库来获取事务对象. 以下是使用 clojure.java.jdbc/with-db-transaction 的示例:

(clojure.java.jdbc/with-db-transaction [tx db]
  (characters/insert-character tx
   {:name "Miracle Max", :specialty "miracles"})
  (characters/insert-character tx
   {:name "Valerie", :specialty "speech interpreter"}))

4.10. Composability

  • Clojure Expressions
  • Snippets

4.10.1. Clojure Expressions (Clojure 表达式)

Clojure Expressions 赋予 HugSQL SQL 语句完整的 Clojure 能力, 以便在运行时有条件地组合部分 SQL.

表达式在初始文件/字符串解析后立即定义和编译.

表达式写在 SQL 注释中(保持 SQL 优先的工作流程).

表达式应返回 String 或 nil. 返回的 String 可以包含 HugSQL 特定的参数语法.

表达式在运行时有两个可用的绑定符号:

  • params, 参数数据的哈希映射
  • options, 选项的哈希映射

Single-Line Comment Expression (单行注释表达式)

单行注释表达式以 --~ 开头. 注意波浪号 ~. 整个 Clojure 表达式应该包含在单行注释中.

-- :name clj-expr-single :? :1
select
--~ (if (seq (:cols params)) ":i*:cols" "*")
from test
order by id

Multi-Line Comment Expression (多行注释表达式)

多行注释表达式可以包含穿插的 SQL. 表达式:

  • /*~ 开始
  • 所有 "继续" 的部分也以 /*~ 开始
  • ~*/ 结束

当表达式需要表示前进到 "下一个" Clojure 形式时(如下面的 if), 需要一个空的分隔符 /*~*/:

-- :name clj-expr-multi :? :1
select
/*~ (if (seq (:cols params)) */
:i*:cols
/*~*/
*
/*~ ) ~*/
from test
order by id

需要访问除包含的 clojure.core 之外的 Clojure 命名空间的表达式, 可以指定类似于 (ns ...) 用法的 :require:

-- :name clj-expr-generic-update :! :n
/* :require [clojure.string :as string]
            [hugsql.parameters :refer [identifier-param-quote]] */
update :i:table set
/*~
(string/join ","
  (for [[field _] (:updates params)]
    (str (identifier-param-quote (name field) options)
      " = :v:updates." (name field))))
~*/
where id = :id
(clj-expr-generic-update db {:table "test"
                             :updates {:name "X"}
                             :id 3})

4.10.2. Snippets (片段)

Snippets 允许通过定义和生成部分 SQL 语句, 然后使用 "Snippet Parameter Types" (片段参数类型) 将这些片段放入完整的 SQL 语句中, 从而实现查询组合.

你可以使用 -- :snip my-snippet=(而不是 =:name)在 SQL 文件中定义片段:

-- :snip select-snip
select :i*:cols

-- :snip from-snip
from :i*:tables

片段可以包含片段参数. 在下面, :snip*:cond 是一个 "Snippet List Parameter Type" (片段列表参数类型), 它指定 :cond 是一个片段的列表/向量.

-- :snip where-snip
where :snip*:cond

如果你愿意, Snippets 可以变得非常复杂(创建一个完整的 DSL). 下面的 cond-snip 片段使用 "deep-get" (深度获取) 参数名访问来构造 where 子句条件:

-- :snip cond-snip
-- We could come up with something
-- quite elaborate here with some custom
-- parameter types that convert := to =, etc.,
-- Examples:
-- {:conj "and" :cond ["id" "=" 1]}
-- OR
-- {:conj "or" :cond ["id" "=" 1]}
-- note that :conj can be "", too
:sql:conj :i:cond.0 :sql:cond.1 :v:cond.2

使用上述片段, 我们现在可以构建完整的查询. (通过 Clojure 表达式带有可选的 where 子句)

-- :name snip-query :? :*
:snip:select
:snip:from
--~ (when (:where params) ":snip:where")
(snip-query
  db
  {:select (select-snip {:cols ["id","name"]})
   :from (from-snip {:tables ["test"]})
   :where (where-snip {:cond [(cond-snip {:conj "" :cond ["id" "=" 1]})
                              (cond-snip {:conj "or" :cond ["id" "=" 2]})]})})

值得注意的是, 片段返回一个 sqlvec. 这个小细节让你在为 HugSQL 查询提供片段时具有极大的灵活性. 为什么? 因为你不一定需要创建自己的片段 DSL: 你可以为此使用另一个库. 这是两全其美的办法! 此练习留给读者.

4.11. Advanced Usage

每个底层数据库库和相应的 HugSQL 适配器可能支持 execute=/=query 命令的额外选项. 由 def-db-fns 定义的函数具有可变参数的第 4 个参数, 该参数将任何选项传递给底层数据库库.

下面是 HugSQL 测试套件中的一个断言, 显示了一个将 :as-arrays? 选项传递给 clojure.java.jdbc/query 的查询. 请注意在使用此透传功能时, 第 3 个参数(HugSQL 特定选项)是必需的:

(is (= [[:name] ["A"] ["B"]]
      (select-ordered db
        {:cols ["name"] :sort-by ["name"]} {} {:as-arrays? true})))

请注意, 从 clojure.java.jdbc 0.5.8 和 HugSQL 0.4.7 开始, 上述额外选项现在必须是哈希映射 (hashmap), 而不是以前版本中的关键字参数. 在 clojure.java.jdbc 0.5.8 中, 已弃用的用法将发出警告. 在 clojure.java.jdbc 0.6.0+ 中, 该用法已被弃用且不再允许. 详情请参阅 clojure.java.jdbc 变更日志.

5. 深入介绍

HugSQL 鼓励将 SQL, DDL 和 DML 语句存储在 SQL 文件中, 这样你就不必拼接大型字符串或在 Clojure 项目中使用抽象泄漏的 DSL.

为了从你的 SQL 语句生成 Clojure 函数, HugSQL 要求在你的 SQL 文件中使用一组简单的约定. 这些约定允许 HugSQL:

  • 按名称定义函数
  • 为定义的函数添加文档字符串 (docstrings)
  • 确定如何执行(命令类型):
    • SQL select
    • DDL create table/index/view, drop …
    • DML insert, update, delete
    • 任何其他语句 (例如 vacuum analyze)
  • 确定结果类型(形状):
    • 一行 (哈希映射)
    • 多行 (哈希映射的向量)
    • 受影响的行数
    • 你实现的任何其他结果
  • 替换参数:
    • Values (值): where id = :id
    • Value Lists (值列表): where id in (:v*:ids)
    • Tuple Lists (用于多重插入的元组列表): values :tuple*:my-records
    • SQL Identifiers (SQL 标识符): from :i:table-name
    • SQL Identifier Lists (SQL 标识符列表): select :i*:column-names
    • Raw SQL (原生 SQL): :sql:my-query

5.1. SQL File Conventions

HugSQL SQL 文件包含以下形式的特殊单行注释和多行注释:

-- :key value1 value2 value3
OR
/* :key
value1
value2
value3
*/

示例:

-- regular SQL comment ignored by hugsql
/*
regular SQL multi-line comment ignored by hugsql
*/

-- :name query-get-many :? :*
-- :doc My query doc string to end of this line
select * from my_table;

-- :name query-get-one :? :1
/* :doc
My multi-line
comment doc string
*/
select * from my_table limit 1

HugSQL 识别以下键:

  • :name:name- (私有函数): 要创建的函数的名称. 后面可以跟命令和结果(例如 :name :? :*), 而不是作为单独的键/值对提供.
  • :doc: 已创建函数的文档字符串.
  • :command: 要运行的底层数据库命令.
  • :result: 预期的结果类型(形状).
  • :snip:snip- (私有函数): 要创建的函数的名称. :snip 用于代替 :name 来定义片段.
  • :meta: EDN 哈希映射形式的元数据, 用于附加到函数上.
  • :require: 用于 Clojure 表达式支持的命名空间 require 和别名.

5.2. Command

:command 指定针对给定 SQL 运行的底层数据库命令. 内置值为:

  • :query:?: 带有结果集的查询(默认值)
  • :execute:!: 任何语句
  • :returning-execute:<!: 支持 INSERT ... RETURNING
  • :insert:i!: 支持 insert 和 jdbc .getGeneratedKeys

:query:execute 镜像了 clojure.java.jdbc 库中 queryexecute! 之间的区别, 以及 clojure.jdbc 库中 fetchexecute 之间的区别.

有关 :returning-execute:insert 的更多信息, 请参阅 Insert 章节.

当未指定命令时, :query 是默认命令.

为了减少输入, 命令可以指定为 :name 键的第二个值:

-- :name all-characters :?

你可以通过实现 hugsql.core/hugsql-command-fn multimethod 来创建自己的命令函数.

5.3. Result

:result 指定给定 SQL 的预期结果类型. 可用的内置值为:

  • :one:1: 作为哈希映射的一行.
  • :many:*: 作为哈希映射向量的多行.
  • :affected:n: 受影响的行数(插入/更新/删除).
  • :raw: 透传未经处理的结果.

当未指定结果时, :raw 是默认值.

为了减少输入, 结果函数可以指定为 :name 键的第三个值. 你必须提供命令值才能使用此简写约定:

-- :name all-characters :? :*

你可以通过实现 hugsql.core/hugsql-result-fn multimethod 来创建自己的结果函数.

5.4. Parameter Types

在 SQL 语句本身中, HugSQL 理解几种可以在函数调用期间传入的参数类型. 所有参数类型均采用以下形式:

:param-name

或者

:param-type:param-name

当调用 HugSQL 生成的函数时, SQL 语句中的参数会在运行时替换为作为函数第二个参数传入的哈希映射数据. 哈希映射的键与参数的 :param-name 部分相匹配. 参数可以在整个 SQL 语句中重复出现, 并且参数的所有实例都将被替换.

5.4.1. SQL Value Parameters (SQL 值参数)

Value Parameters 在运行时会被替换为给定 Clojure 数据类型的适当 SQL 数据类型.

HugSQL 使用 sqlvec 格式将 Clojure 到 SQL 的转换推迟到底层数据库驱动程序.

Value Parameters 的类型是 :value, 简写为 :v.

Value Parameters 是默认的参数类型, 因此你可以在 SQL 语句中省略参数占位符的类型部分.

--:name value-param :? :*
select * from characters where id = :id

--:name value-param-with-param-type :? :*
select * from characters where id = :v:id
(value-param-sqlvec {:id 42})
;=> ["select * from characters where id = ?" 42]

5.4.2. SQL Value List Parameters (SQL 值列表参数)

Value List Parameters 与 Value Parameters 类似, 但适用于 in (...) 查询所需的值列表.

Value List Parameters 的类型是 :value*, 简写为 :v*.

* 表示零个或多个值的序列.

列表中的每个值都被视为一个值参数, 并且列表用逗号连接.

--:name value-list-param :? :*
select * from characters where name in (:v*:names)
(value-list-param-sqlvec {:names ["Fezzik" "Vizzini"]})
;=> ["select * from characters where name in (?,?)" "Fezzik" "Vizzini"]

5.4.3. SQL Tuple Parameters (SQL 元组参数)

Tuple Parameters 与 Value List Parameters 类似, 因为它们都处理值列表.

Tuple Parameters 的不同之处在于它们将值括在括号中. 此外, 虽然 Tuple Parameter 可以像 Value List Parameter 一样使用(例如, 用于 in (...) 条件), 但通常理解为 Tuple Parameter 的数据值可能具有不同的数据类型, 而 Value Parameters 的值具有相同的数据类型.

Tuple Parameters 的类型是 :tuple, 简写为 :t.

列表中的每个值都被视为一个 Value Parameter. 列表用逗号连接并括在括号中.

-- :name tuple-param
-- :doc Tuple Param
select * from test
where (id, name) = :tuple:id-name
(tuple-param-sqlvec {:id-name [1 "A"]})
;=> ["select * from test\nwhere (id, name) = (?,?)" 1 "A"]

并非所有数据库都支持以这种方式使用元组. Postgresql, MySQL 和 H2 支持它. Derby, HSQLDB 和 SQLite 不支持.

5.4.4. SQL Tuple List Parameters (SQL 元组列表参数)

Tuple List Parameters 支持元组列表. 这对于多记录插入特别有用.

Tuple List Parameters 的类型是 :tuple*, 简写为 :t*.

列表中的每个元组都被视为一个 Tuple Parameter. 列表用逗号连接.

-- :name tuple-param-list
-- :doc Tuple Param List
insert into test (id, name)
values :t*:people
(tuple-param-list-sqlvec {:people [[1 "Ed"] [2 "Al"] [3 "Bo"]]})
;=> ["insert into test (id, name)\nvalues (?,?),(?,?),(?,?)" 1 "Ed" 2 "Al" 3 "Bo"]

并非所有数据库都支持以这种方式使用元组列表. Postgresql, MySQL, H2, Derby 和 SQLite 支持它. HSQLDB 不支持.

BATCH INSERTS (批量插入): 值得注意的是, Tuple List Parameter 仅支持 SQL INSERT...VALUES (...),(...),(...) 语法. 这适用于较小的多记录插入. 但这不同于大批量支持. 数据库的底层 JDBC 驱动程序对 SQL 的大小和允许的绑定参数数量有限制. 如果你要进行大批量插入, 应该在事务内对 HugSQL 生成的插入函数进行 mapdoseq 操作.

5.4.5. SQL Identifier Parameters (SQL 标识符参数)

Identifier Parameters 在运行时会被替换为可选引用的 SQL 标识符.

Identifier Parameters 的类型是 :identifier, 简写为 :i.

--:name identifier-param :? :*
select * from :i:table-name
(identifier-param-sqlvec {:table-name "example"})
;=> ["select * from example"]

从 HugSQL 0.4.6 开始, Identifier Parameters 支持 SQL 别名:

(identifier-param-sqlvec {:table-name ["example" "my_example"]})
;=> ["select * from example as my_example"]

默认情况下, 标识符不被引用(加引号). 你可以在定义函数时作为选项或在调用函数时作为选项指定所需的引用方式.

如果你从用户输入中获取标识符, 你应该使用 :quoting 选项来正确引用和转义标识符, 以防止 SQL 注入!

提供给 hugsql.core/def-db-fns=(及其相关函数)的有效 =:quoting 选项有:

  • :ansi 双引号: "identifier"
  • :mysql 反引号: \=identifier\=
  • :mssql 方括号: [identifier]
  • :off 无引用(默认)

包含句点/点 . 的标识符会被分割, 分别引用, 然后重新连接. 这支持 myschema.mytable 约定.

(hugsql.core/def-db-fns "path/to/good.sql" {:quoting :ansi})

(identifier-param-sqlvec {:table-name "example"})
;=> ["select * from \"example\""]

(identifier-param-sqlvec {:table-name "schema1.example"} {:quoting :mssql})
;=> ["select * from [schema1].[example]"]

5.4.6. Raw SQL Parameters (原生 SQL 参数)

Raw SQL Parameters 允许完整的, 未加引号的, 原生的 SQL 参数替换, 允许你参数化 SQL 关键字(以及任何其他 SQL 部分). 你可以使用它在 order by 列子句上设置 ascdesc, 或者你可以使用它将多个 SQL 语句组合成单个语句.

你应该特别注意, 在使用 Raw SQL Parameters 之前, 始终正确验证任何传入的用户输入, 以防止 SQL 注入安全问题.

SQL Parameters 的类型是 :sql.

--:name sql-keyword-param :? :*
select * from example
order by last_name :sql:last_name_sort
(def user-input "asc")
(defn validated-asc-or-desc [x] (if (= x "desc") "desc" "asc"))
(sql-keyword-param-sqlvec {:last_name_sort (validated-asc-or-desc user-input)})
;=> ["select * from example\norder by last_name asc"]

5.4.7. Snippet Parameters (片段参数)

Snippet Parameters 在运行时会被替换为提供的片段/sqlvec.

有关用法, 请参阅 Snippets.

5.4.8. Snippet List Parameters (片段列表参数)

Snippet List Parameters 在运行时会被替换为提供的片段/sqlvec 列表.

有关用法, 请参阅 Snippets.

5.4.9. Custom Parameter Types (自定义参数类型)

你可以通过实现 hugsql.parameters/hugsql-apply-param multimethod 的方法来创建自己的参数类型:

=> (doc hugsql.parameters/apply-hugsql-param)
-------------------------
hugsql.parameters/apply-hugsql-param
  Implementations of this multimethod apply a hugsql parameter
   for a specified parameter type.  For example:

   (defmethod apply-hugsql-param :value
     [param data options]
     (value-param param data options)

   - :value keyword is the parameter type to match on.
   - param is the parameter map as parsed from SQL
     (e.g., {:type :value :name "id"} )
   - data is the runtime parameter map data to be applied
     (e.g., {:id 42} )
   - options contain hugsql options (see hugsql.core/def-sqlvec-fns)

   Implementations must return a vector containing any resulting SQL
   in the first position and any values in the remaining positions.
   (e.g., ["?" 42])

5.4.10. Deep Get Parameter Names (深度获取参数名)

参数名可以选择使用 "deep get" 语法深入到参数数据结构中. 此语法由通过点号 . 连接的关键字和整数组成. 关键字是哈希映射的键. 整数是向量索引. 例如:

-- first-employee :? :1
select * from employees where id = :value:employees.0.id
(first-employee db {:employees [{:id 1} {:id 2}]})
;=> {:id 1 :name "Al"}

5.4.11. Namespaced Keywords (带命名空间的关键字)

从 HugSQL 0.5.1 开始, 支持显式命名空间的带命名空间关键字, 例如 :myproject.core.employee/name:employee/name. 但是, 不支持像 ::employee/name 这样的双冒号简写.

5.4.12. The Colon (冒号)

由于 HugSQL 征用了冒号 : 供自己使用, 如果你实际上需要在 SQL 中使用冒号, 则需要用反斜杠转义冒号. 转义冒号将防止 HugSQL 将冒号解释为 HugSQL 参数的开始. 例如, Postgresql 数组范围使用冒号:

select my_arr[1\:3] from ...

但是, HugSQL 在 Postgresql 使用双冒号 :: 指示 Postgresql 数据类型的历史类型转换语法方面确实做了一个例外, 因此无需转义冒号. HugSQL 会正确地保留双冒号:

select id::text ...
... where id = :id::bigint
-- with the param type specified
... where id = :v:id::bigint

6. 适配器(adapter)

HugSQL 的设计目标之一是平衡 SQL 模板库(其本身)与开发者选择的底层数据库库之间的耦合. 我们可以完全不耦合–只提供 def-sqlvec-fns. 然而, 将一些底层数据库库函数包装在一个协议中, 为大多数用例提供了一条简便的路径. 而且, 在必须直接使用底层数据库库的地方, HugSQL 试图不妨碍你, 并为你提供做你想做的事情的工具.

本着上述思想, HugSQL 提供了一个适配器协议, 允许你选择底层数据库库.

6.1. Default Adapter (默认适配器)

HugSQL 默认使用 clojure.java.jdbc 库的适配器. 如果你更喜欢使用 next.jdbc, clojure.jdbc 或其他适配器, 你需要配置你的依赖并设置适配器.

hugsql clojar 是一个元 clojar, 它引入了 hugsql-core, hugsql-adapter 和默认适配器 hugsql-adapter-clojure-java-jdbc, 后者使用 clojure.java.jdbc 来运行数据库查询.

如果你希望使用不同的适配器, 你应该绕过 hugsql clojar, 并指定 hugsql-core 和你渴望的适配器 clojar.

6.2. next.jdbc Adapter

用于 next.jdbc 的适配器. (截至 HugSQL 0.5.1).

为了保持与 clojure.java.jdbc 最接近的行为, 该适配器当前默认将结果集 :builder-fn 选项设置为 next.jdbc.result-set/as-unqualified-lower-maps. 在设置适配器时可以覆盖此行为.

deps.edn

com.layerware/hugsql-core {:mvn/version "0.5.3"}
com.layerware/hugsql-adapter-next-jdbc {:mvn/version "0.5.3"}

Leiningen

[com.layerware/hugsql-core "0.5.3"]
[com.layerware/hugsql-adapter-next-jdbc "0.5.3"]

有关在代码中启用适配器的信息, 请参阅 Setting An Adapter.

6.3. clojure.java.jdbc Adapter

用于 clojure.java.jdbc 的适配器.

在此包含仅供参考, 但如果你使用的是 hugsql 元 clojar, 则不需要此项.

deps.edn

com.layerware/hugsql-core {:mvn/version "0.5.3"}
com.layerware/hugsql-adapter-clojure-java-jdbc {:mvn/version "0.5.3"}

Leiningen

[com.layerware/hugsql-core "0.5.3"]
[com.layerware/hugsql-adapter-clojure-java-jdbc "0.5.3"]

有关在代码中启用适配器的信息, 请参阅 Setting An Adapter.

6.4. clojure.jdbc Adapter

用于 clojure.jdbc 的适配器.

deps.edn

com.layerware/hugsql-core {:mvn/version "0.5.3"}
com.layerware/hugsql-adapter-clojure-jdbc {:mvn/version "0.5.3"}

Leiningen

[com.layerware/hugsql-core "0.5.3"]
[com.layerware/hugsql-adapter-clojure-jdbc "0.5.3"]

有关在代码中启用适配器的信息, 请参阅 Setting An Adapter.

6.5. Setting an Adapter (设置适配器)

在你的 Clojure 代码中, 你需要显式设置适配器. 你可以通过 hugsql.core/set-adapter! 全局地(即在应用程序启动时)执行此操作, 或者你可以在使用 hugsql.core/def-db-fns 定义函数时指定 :adapter 作为选项, 或者你可以在调用已定义函数时传入 :adapter 选项.

hugsql.core/set-adapter!

(ns my-app
  (:require [hugsql.core :as hugsql]
            [hugsql.adapter.next-jdbc :as next-adapter
            [next.jdbc.result-set :as rs]])

(defn app-init []
  (hugsql/set-adapter! (next-adapter/hugsql-adapter-next-jdbc)))

;; OR override the :builder-fn behavior
(defn app-init []
  (hugsql/set-adapter! (next-adapter/hugsql-adapter-next-jdbc {:builder-fn result-set/as-unqualified-maps})))

或者

hugsql.core/def-db-fns 上的 :adapter 选项

(ns my-db-stuff
  (:require [hugsql.core :as hugsql]
            [hugsql.adapter.next-jdbc :as next-adapter]))

(hugsql/def-db-fns "path/to/db.sql"
  {:adapter (next-adapter/hugsql-adapter-next-jdbc)})

或者

已定义函数上的 :adapter 选项

(ns my-db-stuff
(:require [hugsql.core :as hugsql]
          [hugsql.adapter.next-jdbc :as next-adapter]))

(def db ;;a db-spec here)

(hugsql/def-db-fns "path/to/db.sql")

(my-query db {:id 1}
  {:adapter (next-adapter/hugsql-adapter-next-jdbc)})

6.6. Create an Adapter (创建适配器)

创建 HugSQL 适配器就像实现 hugsql.adapter/HugsqlAdapter 协议一样简单. 有关示例, 请参阅 clojure.java.jdbc, next.jdbcclojure.jdbc 的实现.

6.7. Adapters from the Community (来自社区的适配器)

HugSQL 的适配器协议鼓励替代适配器来支持你首选的数据库库. 编写你自己的适配器!

  • Robin Heggelund Hansen 编写的 HugSQL Adapter for postgres.async
  • HugSQL Adapter for ClickHouse

7. Frequently Asked Questions

7.1. Comparison with Yesql (与 Yesql 的比较)

Yesql 是一个由 Kris Jenkins 编写的 Clojure 库. 它对使用 SQL 有着类似的看法, HugSQL 全心全意地拥抱这种看法. Yesql 是 HugSQL 的精神前身, 如果没有这个伟大的库, HugSQL 就不会存在.

那么为什么要构建一个类似的库呢?

我的一个项目有一些复杂的 SQL, 需要我生成具有可变列的动态命名的表和视图, 我发现自己不得不退回到字符串拼接来构建我的 SQL. 我意识到我需要类似于 Yesql 的东西, 但要支持不同类型的参数占位符: 即 SQL 标识符和 SQL 关键字. 这是成长为 HugSQL 的种子, 这两个库现在有不少区别.

–Curtis Summers

Yesql 和 HugSQL 之间的区别:

  • Yesql 与 clojure.java.jdbc 耦合. HugSQL 拥有基于协议的适配器, 允许使用多个数据库后端库, 并随附对 clojure.java.jdbc, next.jdbcclojure.jdbc 的支持. 此功能已促成了来自社区的多个适配器. 有关更多信息, 请参阅 HugSQL Adapters.
  • Yesql 仅支持 SQL Value 参数. HugSQL 支持 SQL Values, SQL Tuples, SQL Identifiers, Raw SQL Parameters 以及创建你自己的自定义参数类型. 有关更多详细信息, 请参阅 Parameter Types.
  • Yesql 支持位置参数占位符 ? 和命名参数占位符 :id. HugSQL 仅支持命名参数占位符, 并且没有计划支持位置占位符.
  • Yesql 倾向于使用函数名的命名约定(!<! 后缀)来指示功能. HugSQL 偏好在 SQL 文件中进行显式配置. HugSQL 具有 :result 配置, 指示预期的返回格式(例如, :many = 哈希映射向量, :one = 哈希映射). Yesql 通过将 :result-set-fn 选项传递给 clojure.java.jdbc/query 来支持类似的功能.
  • Yesql (截至 0.5.x) 支持在定义函数时设置默认数据库连接, 并可选择在函数调用时覆盖此连接. HugSQL 期望数据库 spec, 连接或事务对象作为函数调用的第一个参数. 但是, 截至 0.4.1 版本, HugSQL 提供了 map-of-db-fns, 允许其他库包装 HugSQL 创建的函数并设置默认数据库连接. 这正是 Luminus Web 框架的 conman 库所做的.
  • 截至 HugSQL 0.4.0, HugSQL 支持 Clojure 表达式和 Snippets, 用于从较小的部分组合 SQL.
  • 截至 0.5.3 版本, Yesql 已冻结并正在寻找维护者.

7.2. Does HugSQL support my wacky SQL? (HugSQL 支持我古怪的 SQL 吗? )

是的, HugSQL 的解析策略是提取它关心的部分, 并保持 SQL 的其余部分原样. 如果你发现某些 SQL 没有被 HugSQL 正确处理, 那可能是一个 bug. 请提交 issue.

7.3. What about DSLs for SQL? (SQL 的 DSL 怎么样? )

我可以获得类似于 Honey SQL 的 SQL 生成吗?

HugSQL 有几个组合 SQL 的选项. 请参阅 Composability.

HugSQL 鼓励你首先以 SQL 思考, 然后在必要时加入 Clojure 的力量. HoneySQL 首先从 Clojure 方面开始. 这两种都是有效的工作流程, 取决于开发者的偏好和情况. 重要的是要意识到 HugSQL 和 HoneySQL 并不是互斥的: HugSQL Snippet Parameter Types :snip:snip* 可以使用来自 HoneySQL format 函数的 sqlvec 格式输出. 这是两全其美的办法!

7.4. Preventing SQL Injection (防止 SQL 注入)

HugSQL 如何帮助防止 SQL 注入?

HugSQL 试图提供一套工具, 在可能的情况下帮助防止 SQL 注入, 而不会剥夺开发者的能力. 以下是几个潜在的 SQL 注入攻击向量以及 HugSQL 对每个向量的响应:

Value Parameters

Value Parameters, Value List Parameters, Tuple Parameters 和 Tuple List Parameters 都是 SQL 值参数的变体, 它们将 Clojure 数据类型转换为 SQL. 默认情况下, 所有这些参数类型都推迟到底层数据库库来执行 SQL 参数绑定, 以防止 SQL 注入问题.

Identifier Parameters

Identifier Parameters 和 Identifier List Parameters 支持使用 :quoting 选项对标识符进行引用和转义. 默认情况下, :quoting:off, 因为 HugSQL 对你给定的数据库不做任何假设. 如果你不从用户输入中获取标识符, 这对于你的用例可能是可以的.

如果你从用户输入中获取标识符, 你应该使用 :quoting 选项来防止 SQL 注入! 详情请参阅 Identifier Parameters.

Raw SQL Parameters

Raw SQL Parameters 正如其名, 当使用用户输入时, 你有责任清理此参数类型的任何使用.

Snippet Parameters

Snippets 生成 sqlvec, Snippet Parameter Types 消费 sqlvec. 对于包含任何 HugSQL 参数类型的片段, 适用与上述相同的规则. 如果你正在使用来自你自己的代码或另一个库(比如 HoneySQL)的片段(或 sqlvec), 则可能适用其他规则.

Custom Parameter Types

Custom Parameter Types 允许你创建自己的参数类型. 你有责任确保你的实现通过正确转义数据来防止 SQL 注入.

Clojure Expressions

Clojure Expressions 应返回字符串或 nil, 从表达式返回的字符串在运行时被解析以支持 HugSQL 参数. 上述参数类型的相同规则适用.

Author: 青岛红创翻译

Created: 2025-11-21 Fri 15:41