使用 ppx_rapper_lwt 的 SQLite CREATE、INSERT、SELECT

任务

数据库 / SQLite / SQLite CREATE、INSERT、SELECT

使用的 Opam 包

  • ppx_rapper_lwt 测试版本:3.1.0 — 使用的库:ppx_rapper_lwt
  • ppx_rapper 测试版本:3.1.0 — 使用的库:ppx_rapper
  • caqti-driver-sqlite3 测试版本:1.9.0 — 使用的库:caqti-driver-sqlite3
  • caqti-lwt 测试版本:1.9.0 — 使用的库:caqti-lwt
  • lwt 测试版本:5.7.0 — 使用的库:lwt, lwt.unix

代码

Caqti/ppx_rapper 组合使用 Lwt 环境。让运算符 ( let* )( let*? ) 像往常一样为 Lwt 定义,以获得简洁的链式 promise 符号。( let*? ) 从返回的 Ok result 中提取结果,或者在 Error err 值的情况下停止执行。

let ( let* ) = Lwt.bind
let ( let*? ) = Lwt_result.bind

辅助函数 iter_queries 按顺序调度查询列表。每个查询都是一个函数,它以数据库的连接句柄作为参数。

let iter_queries queries connection =
  List.fold_left
    (fun a f ->
      Lwt_result.bind a (fun () -> f connection))
    (Lwt.return (Ok ()))
    queries

这里的 %rapper 节点让 ppx_rapper 生成代码,这样,当应用 create_employees_table () connection 函数时,提供的 SQL CREATE 查询将在没有任何参数的情况下运行,也不接收来自数据库的任何数据。

如果查询成功执行,我们将获得 Ok () 值,否则我们将获得 Error 值。

let create_employees_table =
  [%rapper
    execute {sql| CREATE TABLE employees
              (name VARCHAR,
              firstname VARCHAR,
              age INTEGER)
            |sql}
  ]

type employee =
  { name:string; firstname:string; age:int }
let employees = [
  {name = "Dupont"; firstname = "Jacques"; age = 36};
  {name = "Legendre"; firstname = "Patrick"; age = 42}
  ]

对于 SQL INSERT 查询,ppx_rapper 生成一个函数 insert_employee (p: employee) connection。标记 record_in 告诉 ppx_rapper 从提供的记录值中读取 namefirstnameage 值,而 %[TYPE_NAME]{[INPUT_FIELD_NAME]} 符号指定对输入值执行哪些转换。

let insert_employee =
  [%rapper
    execute
    {sql| INSERT INTO employees VALUES
        (%string{name},
        %string{firstname},
        %int{age})
    |sql}
    record_in
  ]

get_many 标记让 ppx_rapper 生成查询数据库并接收值列表的代码。record_out 标记指定每个列表项将是一个记录。

@[TYPE_NAME]{[COLUMN_NAME]} 符号指定对输出值执行哪些转换。

let get_all_employees =
  [%rapper
    get_many
    {sql|SELECT
        @string{name},
        @string{firstname},
        @int{age}
      FROM employees
    |sql}
    record_out
  ]

这是另一个查询示例,它使用 get_opt 标记通过 SQL WHERE 子句选择一行。此查询既有输入(name)也有输出值(namefirstnameage)。

这里缺少 record_in 标记让 ppx_rapper 生成代码,其中输入值作为命名参数传递。get_opt 标记意味着结果将是一个选项:如果未找到匹配条件的行,则为 None;如果一行匹配条件,则为 Some r

let get_employee_by_name =
  [%rapper
    get_opt
    {sql|SELECT
        @string{name},
        @string{firstname},
        @int{age}
      FROM employees
      WHERE name=%string{name}
    |sql}
    record_out
  ]


所有由 ppx_rapper 生成的查询函数都接受一个参数和一个 connection 参数。函数 insert_employee 必须使用类型为 employeeconnection 的值调用。要从列表中插入多个记录,我们使用 List.map 创建一个函数列表。每个函数在调用时将执行其关联的查询。函数 iter_queries 按顺序运行查询。

请注意,如果您需要插入大量记录,建议执行批量插入查询。

let execute_queries connection =
  let*? () = create_employees_table () connection in
  let*? () =
    iter_queries
      (List.map insert_employee employees)
      connection
  in
  let*? employees = get_all_employees () connection in
  employees |> List.iter (fun employees ->
    Printf.printf
      "name=%s, firstname=%s, age=%d\n"
      employees.name
      employees.firstname
      employees.age);
  let*? employees =
    get_employee_by_name ~name:"Dupont" connection
  in
  match employees with
  | Some employees' ->
    Printf.printf
      "found:name=%s, firstname=%s, age=%d\n"
      employees'.name
      employees'.firstname
      employees'.age;
    Lwt_result.return ()
  | None ->
    print_string "Not found";
    Lwt_result.return ()

主程序首先建立 Lwt 环境。函数 with_connection 打开数据库,使用 connection 数据库句柄执行函数,并再次关闭数据库连接,即使抛出异常也是如此。

let () =
  match Lwt_main.run @@
  Caqti_lwt.with_connection
    (Uri.of_string "sqlite3:essai.sqlite")
    execute_queries
  with
  | Result.Ok () ->
    print_string "OK\n"
  | Result.Error err ->
    print_string (Caqti_error.show err)

讨论

Caqti 库允许使用 SQLite、MariaDB 和 PostgreSQL 进行可移植编程。ppx_rapper 将带注释的 SQL 字符串转换为 Caqti 查询。此预处理器使所有类型转换透明,并利用 OCaml 的强类型。它还检查给定查询的 SQL 语法。参见 Caqti 参考页面ppx_rapper 参考页面

配方不起作用?评论不清楚或过时了吗?

打开一个问题 或者 为这个配方做出贡献

此任务的其他配方