Awesome-cl list 里面罗列了很多不同的数据库的插件库,其中大致可以分为四类:

  • 与数据库引擎封装在一起(cl-sqlite、postmodern、cl-redis等),
  • 与多个数据库的接口(clsql、sxql等),
  • 持久对象数据库(bknr.datastore (see chap. 21 of “Common Lisp Recipes”),ubiquitous等),
  • 对象关系映射(ORM) (Mito)

还有其他的数据库工具(pgloader)。

本章将从 Mito 开始介绍。如果必须要使用现有的数据库的话,可以了解下 cl-dbi 和 clsql。如果不需要现有数据库的话,而是想要使用 Lisp 对象进行自动持久化,那么就随意了。

Mito ORM 和 SxQL

可以通过 quicklisp 安装 mito:

  1. (ql:quickload :mito)

概述

Mito 是个 Common Lisp ORM,其支持迁移、关系和 PostgreSQL 的。

  • 支持MySQL、PostSQL 和 SQLite3
  • 在定义模型是,默认会加上 id(主键)、created_atupdated_at 三个字段,和 Ruby 的 ActiveRecord 或 Django 一样,
  • 支持数据库迁移
  • 支持数据库模式版本控制(schema versioning)
  • 在 SBCL 和 CCL 下验证过

作为 ORM,Mito 可以编写类定义、指定表关系以及提供查询函数。如果是要自定义查询,那么就需要用到 SxQL,一个为多个数据库提供统一接口的 SQL 生成器。

使用 Mito 的一般步骤如下:

  • 连接数据库
  • 编写 CLOS 类模型
  • 执行迁移以创建或修改表
  • 创建对象,将其保存到数据库

然后不断执行以上操作。

连接数据库

在 Mito 中可以调用函数 connect-toplevel 来连接 RDBMs:

  1. (mito:connect-toplevel :mysql :database-name "myapp" :username "fukamachi" :password "c0mon-1isp")

数据库驱动类型可以是 :mysqlsqlite3:postgres

当连接 sqlite 时,不需要指定用户名和密码:

  1. (connect-toplevel :sqlite3 :database-name "myapp")

通常,需要提前创建好 MySQL 或 PostgreSQL 的数据库,具体操作参照其文档。

连接时会将 mito:*connection* 设置为新连接然后返回该连接。

断开也是使用 disconnect-toplevel.

=> you might make good use of a wrapper function:

  1. (defun connect ()
  2. "Connect to the DB."
  3. (connect-toplevel :sqlite3 :database-name "myapp"))

模型

定义模型

在 Mito 中,通过使用 (:metaclass mito:dao-table-class) 来定义一个类,该类和数据库中的表相对应。

  1. (defclass user ()
  2. ((name :col-type (:varchar 64)
  3. :initarg :name
  4. :accessor user-name)
  5. (email :col-type (or (:varchar 128) :null)
  6. :initarg :email
  7. :accessor user-email))
  8. (:metaclass mito:dao-table-class))

注意,这些类会自动加入一些属性:名为 id 的主键(如果没有设置主键)以及用来记录时间戳的 created_atupdated_at 字段。要禁用这种行为的话,在 defclass 中将 :auto-pk 以及 :record-timestamps 设置为 nil

可以检查下新定义的类:

  1. (mito.class:table-column-slots (find-class 'user))
  2. ;=> (#<MITO.DAO.COLUMN:DAO-TABLE-COLUMN-CLASS MITO.DAO.MIXIN::ID>
  3. ; #<MITO.DAO.COLUMN:DAO-TABLE-COLUMN-CLASS COMMON-LISP-USER::NAME>
  4. ; #<MITO.DAO.COLUMN:DAO-TABLE-COLUMN-CLASS COMMON-LISP-USER::EMAIL>
  5. ; #<MITO.DAO.COLUMN:DAO-TABLE-COLUMN-CLASS MITO.DAO.MIXIN::CREATED-AT>
  6. ; #<MITO.DAO.COLUMN:DAO-TABLE-COLUMN-CLASS MITO.DAO.MIXIN::UPDATED-AT>)

这个类会隐式地继承 mito:dao-class

  1. (find-class 'user)
  2. ;=> #<MITO.DAO.TABLE:DAO-TABLE-CLASS COMMON-LISP-USER::USER>
  3. (c2mop:class-direct-superclasses *)
  4. ;=> (#<STANDARD-CLASS MITO.DAO.TABLE:DAO-CLASS>)

这在定义适用于所有表的类的时候有很大的帮助。

更多关于 Common Lisp 对象系统的介绍,参见 第15章:CLOS

创建表

在定义完模型后,就需要创建表了:

  1. (mito:ensure-table-exists 'user)

有个辅助函数是:

  1. (defun ensure-tables ()
  2. (mapcar #'mito:ensure-table-exists '('user 'foo 'bar)))

Mito’s documentation 中还介绍了其他的方法。

每次修改完模型后,需要运行一遍数据迁移,具体在在一节中会介绍。

字段(fields)

字段类型(fields type)

字段的类型有以下几种:

(:varchar <integer>)

:serial:bigserial:integer:bigint:unsigned

:timestamp:timestamptz

:bytea

可选字段(optional fields)

可选字段可以使用 (or <real type> :null)

  1. (email :col-type (or (:varchar 128) :null)
  2. :initarg :email
  3. :accessor user-email))

字段约束

:unique-keys 可以像下面这样使用:

  1. (defclass user ()
  2. ((name :col-type (:varchar 64)
  3. :initarg :name
  4. :accessor user-name)
  5. (email :col-type (:varchar 128)
  6. :initarg :email
  7. :accessor user-email))
  8. (:metaclass mito:dao-table-class)
  9. (:unique-keys email))

现在已经知道 :primary-key

可以用 :table-name 来修改表名。

表关系

指定表的关系可以将 :col-type 设置为其他的模型类:

  1. (defclass tweet ()
  2. ((status :col-type :text
  3. :initarg :status
  4. :accessor tweet-status)
  5. ;; This slot refers to USER class
  6. (user :col-type user
  7. :initarg :user
  8. :accessor tweet-user))
  9. (:metaclass mito:dao-table-class))
  10. (table-definition (find-class 'tweet))
  11. ;=> (#<SXQL-STATEMENT: CREATE TABLE tweet (
  12. ; id BIGSERIAL NOT NULL PRIMARY KEY,
  13. ; status TEXT NOT NULL,
  14. ; user_id BIGINT NOT NULL,
  15. ; created_at TIMESTAMP,
  16. ; updated_at TIMESTAMP
  17. ; )>)

现在就可以通过 USER 对象(而不是 USER-ID)来创建或检索 TWEET 对象了。

  1. (defvar *user* (mito:create-dao 'user :name "Eitaro Fukamachi"))
  2. (mito:create-dao 'tweet :user *user*)
  3. (mito:find-dao 'tweet :user *user*)

Mito 不会在引用表中添加外键约束。

一对一

A one-to-one relationship is simply represented with a simple foreign
key on a slot (as :col-type user in the tweet class). Besides, we
can add a unicity constraint, as with (:unique-keys email).

一对多、多对一

The tweet example above shows a one-to-many relationship between a user and
his tweets: a user can write many tweets, and a tweet belongs to only
one user.

The relationship is defined with a foreign key on the “many” side
linking back to the “one” side. Here the tweet class defines a
user foreign key, so a tweet can only have one user. You didn’t need
to edit the user class.

A many-to-one relationship is actually the contrary of a one-to-many.
You have to put the foreign key on the appropriate side.

多对多

A many-to-many relationship needs an intermediate table, which will be
the “many” side for the two tables it is the intermediary of.

And, thanks to the join table, we can store more information about the relationship.

Let’s define a book class:

  1. (defclass book ()
  2. ((title
  3. :col-type (:varchar 128)
  4. :initarg :title
  5. :accessor title)
  6. (ean
  7. :col-type (or (:varchar 128) :null)
  8. :initarg :ean
  9. :accessor ean))
  10. (:metaclass mito:dao-table-class))

A user can have many books, and a book (as the title, not the physical
copy) is likely to be in many people’s library. Here’s the
intermediate class:

  1. (defclass user-books ()
  2. ((user
  3. :col-type user
  4. :initarg :user)
  5. (book
  6. :col-type book
  7. :initarg :book))
  8. (:metaclass mito:dao-table-class))

Each time we want to add a book to a user’s collection (say in
a add-book function), we create a new user-books object.

But someone may very well own many copies of one book. This is an
information we can store in the join table:

  1. (defclass user-books ()
  2. ((user
  3. :col-type user
  4. :initarg :user)
  5. (book
  6. :col-type book
  7. :initarg :book)
  8. ;; Set the quantity, 1 by default:
  9. (quantity
  10. :col-type :integer
  11. :initarg :quantity
  12. :initform 1
  13. :accessor quantity))
  14. (:metaclass mito:dao-table-class))

继承和混合(mixin)

DAO-CLASS 的子类都能被继承。这在创建有相同字段的模型类时就很方便。

  1. (defclass user ()
  2. ((name :col-type (:varchar 64)
  3. :initarg :name
  4. :accessor user-name)
  5. (email :col-type (:varchar 128)
  6. :initarg :email
  7. :accessor user-email))
  8. (:metaclass mito:dao-table-class)
  9. (:unique-keys email))
  10. (defclass temporary-user (user)
  11. ((registered-at :col-type :timestamp
  12. :initarg :registered-at
  13. :accessor temporary-user-registered-at))
  14. (:metaclass mito:dao-table-class))
  15. (mito:table-definition 'temporary-user)
  16. ;=> (#<SXQL-STATEMENT: CREATE TABLE temporary_user (
  17. ; id BIGSERIAL NOT NULL PRIMARY KEY,
  18. ; name VARCHAR(64) NOT NULL,
  19. ; email VARCHAR(128) NOT NULL,
  20. ; registered_at TIMESTAMP NOT NULL,
  21. ; created_at TIMESTAMP,
  22. ; updated_at TIMESTAMP,
  23. ; UNIQUE (email)
  24. ; )>)

当需要一个与数据库其他表都无关的临时表时,可以用 DAO-TABLE-MIXIN。下面代码中的 has-email 类就不会创建表。

  1. (defclass has-email ()
  2. ((email :col-type (:varchar 128)
  3. :initarg :email
  4. :accessor object-email))
  5. (:metaclass mito:dao-table-mixin)
  6. (:unique-keys email))
  7. ;=> #<MITO.DAO.MIXIN:DAO-TABLE-MIXIN COMMON-LISP-USER::HAS-EMAIL>
  8. (defclass user (has-email)
  9. ((name :col-type (:varchar 64)
  10. :initarg :name
  11. :accessor user-name))
  12. (:metaclass mito:dao-table-class))
  13. ;=> #<MITO.DAO.TABLE:DAO-TABLE-CLASS COMMON-LISP-USER::USER>
  14. (mito:table-definition 'user)
  15. ;=> (#<SXQL-STATEMENT: CREATE TABLE user (
  16. ; id BIGSERIAL NOT NULL PRIMARY KEY,
  17. ; name VARCHAR(64) NOT NULL,
  18. ; email VARCHAR(128) NOT NULL,
  19. ; created_at TIMESTAMP,
  20. ; updated_at TIMESTAMP,
  21. ; UNIQUE (email)
  22. ; )>)

更多使用的例子参见mito-auth

问题诊断

“Cannot CHANGE-CLASS objects into CLASS metaobjects.”

当看到下面的错误提示时:

  1. Cannot CHANGE-CLASS objects into CLASS metaobjects.
  2. [Condition of type SB-PCL::METAOBJECT-INITIALIZATION-VIOLATION]
  3. See also:
  4. The Art of the Metaobject Protocol, CLASS [:initialization]

绝对是你先写了个类,然后再添加 Mito 元类,之后再执行这个类。

发生这种情况时,就需要将这个类从当前的包中移除掉。

  1. (setf (find-class 'foo) nil)

或者用 Slime 的检查器,鼠标点一下这个类,然后选择 “移除(remove)” 按钮。

更多相关信息参见这里.

迁移

首先创建一个表:

  1. (ensure-table-exists 'user)

然后修改表:

  1. (mito:migrate-table 'user)

可以用 migration-expressions class 来检查生成的 SQL 语句。例如,创建个 user 表:

  1. (ensure-table-exists 'user)
  2. ;-> ;; CREATE TABLE IF NOT EXISTS "user" (
  3. ; "id" BIGSERIAL NOT NULL PRIMARY KEY,
  4. ; "name" VARCHAR(64) NOT NULL,
  5. ; "email" VARCHAR(128),
  6. ; "created_at" TIMESTAMP,
  7. ; "updated_at" TIMESTAMP
  8. ; ) () [0 rows] | MITO.DAO:ENSURE-TABLE-EXISTS

这次创建之前的 user 定义没有任何修改:

  1. (mito:migration-expressions 'user)
  2. ;=> NIL

现在添加个唯一一个字段 email

  1. (defclass user ()
  2. ((name :col-type (:varchar 64)
  3. :initarg :name
  4. :accessor user-name)
  5. (email :col-type (:varchar 128)
  6. :initarg :email
  7. :accessor user-email))
  8. (:metaclass mito:dao-table-class)
  9. (:unique-keys email))

在次执行 migration-expressions

  1. (mito:migration-expressions 'user)
  2. ;=> (#<SXQL-STATEMENT: ALTER TABLE user ALTER COLUMN email TYPE character varying(128), ALTER COLUMN email SET NOT NULL>
  3. ; #<SXQL-STATEMENT: CREATE UNIQUE INDEX unique_user_email ON user (email)>)

然后再将修改提交:

  1. (mito:migrate-table 'user)
  2. ;-> ;; ALTER TABLE "user" ALTER COLUMN "email" TYPE character varying(128), ALTER COLUMN "email" SET NOT NULL () [0 rows] | MITO.MIGRATION.TABLE:MIGRATE-TABLE
  3. ; ;; CREATE UNIQUE INDEX "unique_user_email" ON "user" ("email") () [0 rows] | MITO.MIGRATION.TABLE:MIGRATE-TABLE
  4. ;-> (#<SXQL-STATEMENT: ALTER TABLE user ALTER COLUMN email TYPE character varying(128), ALTER COLUMN email SET NOT NULL>
  5. ; #<SXQL-STATEMENT: CREATE UNIQUE INDEX unique_user_email ON user (email)>)

查询

创建对象

像平常一样,可以使用 make-instance 来创建 user 对象:

  1. (defvar me
  2. (make-instance 'user :name "Eitaro Fukamachi" :email "e.arrows@gmail.com"))
  3. ;=> USER

然后使用 insert-dao 进行保存:

  1. (mito:insert-dao me)
  2. ;-> ;; INSERT INTO `user` (`name`, `email`, `created_at`, `updated_at`) VALUES (?, ?, ?, ?) ("Eitaro Fukamachi", "e.arrows@gmail.com", "2016-02-04T19:55:16.365543Z", "2016-02-04T19:55:16.365543Z") [0 rows] | MITO.DAO:INSERT-DAO
  3. ;=> #<USER {10053C4453}>

将上面两个步骤合并成一条语句就是:

  1. (mito:create-dao 'user :name "Eitaro Fukamachi" :email "e.arrows@gmail.com")

不能将 user 类给定义为外部类,也不能在 user 所在包的外部创建其对象(无论如何,将所有与数据库相关的操作宝尊在一个 models 包和文件中,在实践中是比较好的)。相反,可以使用构造函数进行对象的创建:

  1. (defun make-user (&key name)
  2. (make-instance 'user :name name))

更新字段

  1. (setf (slot-value me 'name) "nitro_idiot")
  2. ;=> "nitro_idiot"

然后保存:

  1. (mito:save-dao me)

删除数据

  1. (mito:delete-dao me)
  2. ;-> ;; DELETE FROM `user` WHERE (`id` = ?) (1) [0 rows] | MITO.DAO:DELETE-DAO
  3. ;; or:
  4. (mito:delete-by-values 'user :id 1)
  5. ;-> ;; DELETE FROM `user` WHERE (`id` = ?) (1) [0 rows] | MITO.DAO:DELETE-DAO

获取主键值

  1. (mito:object-id me)
  2. ;=> 1

计数

  1. (mito:count-dao 'user)
  2. ;=> 1

单个查询

  1. (mito:find-dao 'user :id 1)
  2. ;-> ;; SELECT * FROM `user` WHERE (`id` = ?) LIMIT 1 (1) [1 row] | MITO.DB:RETRIEVE-BY-SQL
  3. ;=> #<USER {10077C6073}>

因此,对于泛型帮助其,有这么一种可能,就是通过给定键值来查找对象。

  1. (defgeneric find-user (key-name key-value)
  2. (:documentation "Retrieves an user from the data base by one of the unique
  3. keys."))
  4. (defmethod find-user ((key-name (eql :id)) (key-value integer))
  5. (mito:find-dao 'user key-value))
  6. (defmethod find-user ((key-name (eql :name)) (key-value string))
  7. (first (mito:select-dao 'user
  8. (sxql:where (:= :name key-value)))))

查找所有

使用 select-dao 宏。

获取所有用户:

  1. (mito:select-dao 'user)
  2. ;(#<USER {10077C6073}>)
  3. ;#<SXQL-STATEMENT: SELECT * FROM user>

通过关系查找

和上面看到的表之间的关系一样:

  1. (mito:find-dao 'tweet :user *user*)

自定义查询

通过 select-dao,可以编写更精确的查询语句,然后将查询语句传给 SxQL

示例:

  1. (select-dao 'tweet
  2. (where (:like :status "%Japan%")))

另一个例子:

  1. (select (:id :name :sex)
  2. (from (:as :person :p))
  3. (where (:and (:>= :age 18)
  4. (:< :age 65)))
  5. (order-by (:desc :age)))

同时也可以用常规的 Lisp 代码组合成查询语句:

  1. (defun find-tweets (&key user)
  2. (select-dao 'tweet
  3. (when user
  4. (where (:= :user user)))
  5. (order-by :object-created)))

select-dao 宏会将对的查询语句展开©。

闭包

参见 SxQL documentation.

以下是示例:

  1. (select-dao 'foo
  2. (where (:and (:> :age 20) (:<= :age 65))))
  1. (order-by :age (:desc :id))
  1. (group-by :sex)
  1. (having (:>= (:sum :hoge) 88))
  1. (limit 0 10)

join等的用法也一样。

操作符

  1. :not
  2. :is-null, :not-null
  3. :asc, :desc
  4. :distinct
  5. :=, :!=
  6. :<, :>, :<= :>=
  7. :a<, :a>
  8. :as
  9. :in, :not-in
  10. :like
  11. :and, :or
  12. :+, :-, :* :/ :%
  13. :raw

触发器

因为 insert-daoupdate-daodelete-dao 都被定义为泛型函数,所以可以在这些函数中定义 :beforeafter:around 方法,和之前的组合方法一样。

  1. (defmethod mito:insert-dao :before ((object user))
  2. (format t "~&Adding ~S...~%" (user-name object)))
  3. (mito:create-dao 'user :name "Eitaro Fukamachi" :email "e.arrows@gmail.com")
  4. ;-> Adding "Eitaro Fukamachi"...
  5. ; ;; INSERT INTO "user" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) ("Eitaro Fukamachi", "e.arrows@gmail.com", "2016-02-16 21:13:47", "2016-02-16 21:13:47") [0 rows] | MITO.DAO:INSERT-DAO
  6. ;=> #<USER {100835FB33}>

Inflation/Deflation

Inflation/Deflation 是将 Mito 和 RDBMS 中值相互转换的函数。

  1. (defclass user-report ()
  2. ((title :col-type (:varchar 100)
  3. :initarg :title
  4. :accessor report-title)
  5. (body :col-type :text
  6. :initarg :body
  7. :initform ""
  8. :accessor report-body)
  9. (reported-at :col-type :timestamp
  10. :initarg :reported-at
  11. :initform (local-time:now)
  12. :accessor report-reported-at
  13. :inflate #'local-time:universal-to-timestamp
  14. :deflate #'local-time:timestamp-to-universal))
  15. (:metaclass mito:dao-table-class))

预加载

ORM 最头痛的问题就是“N+1 查询”问题。

  1. ;; BAD EXAMPLE
  2. (use-package '(:mito :sxql))
  3. (defvar *tweets-contain-japan*
  4. (select-dao 'tweet
  5. (where (:like :status "%Japan%"))))
  6. ;; Getting names of tweeted users.
  7. (mapcar (lambda (tweet)
  8. (user-name (tweet-user tweet)))
  9. *tweets-contain-japan*)

上面例子中在每次迭代时查询用户时都会发送个类似 “SELECT * FROM user WHERE id = ?” 的查询语句。

为了避免这个性能问题,在查询中添加 includes,这样就只会发送一次查询语句,而不是 N 次:

  1. ;; GOOD EXAMPLE with eager loading
  2. (use-package '(:mito :sxql))
  3. (defvar *tweets-contain-japan*
  4. (select-dao 'tweet
  5. (includes 'user)
  6. (where (:like :status "%Japan%"))))
  7. ;-> ;; SELECT * FROM `tweet` WHERE (`status` LIKE ?) ("%Japan%") [3 row] | MITO.DB:RETRIEVE-BY-SQL
  8. ;-> ;; SELECT * FROM `user` WHERE (`id` IN (?, ?, ?)) (1, 3, 12) [3 row] | MITO.DB:RETRIEVE-BY-SQL
  9. ;=> (#<TWEET {1003513EC3}> #<TWEET {1007BABEF3}> #<TWEET {1007BB9D63}>)
  10. ;; No additional SQLs will be executed.
  11. (tweet-user (first *))
  12. ;=> #<USER {100361E813}>

模式版本控制

  1. $ ros install mito
  2. $ mito
  3. Usage: mito command [option...]
  4. Commands:
  5. generate-migrations
  6. migrate
  7. Options:
  8. -t, --type DRIVER-TYPE DBI driver type (one of "mysql", "postgres" or "sqlite3")
  9. -d, --database DATABASE-NAME Database name to use
  10. -u, --username USERNAME Username for RDBMS
  11. -p, --password PASSWORD Password for RDBMS
  12. -s, --system SYSTEM ASDF system to load (several -s's allowed)
  13. -D, --directory DIRECTORY Directory path to keep migration SQL files (default: "/Users/nitro_idiot/Programs/lib/mito/db/")
  14. --dry-run List SQL expressions to migrate

自省(Introspection)

Mito 也有自省函数。

可以在 (mito.class.column:...) 中调用函数来获取字段的信息了:

  • table-column-[class, name, info, not-null-p,...]
  • primary-key-p

(mito.class.table:...)也一样。

假设得到了类的属性列表:

  1. (ql:quickload "closer-mop")
  2. (closer-mop:class-direct-slots (find-class 'user))
  3. ;; (#<MITO.DAO.COLUMN:DAO-TABLE-COLUMN-CLASS NAME>
  4. ;; #<MITO.DAO.COLUMN:DAO-TABLE-COLUMN-CLASS EMAIL>)
  5. (defparameter user-slots *)

这样就能解决下面的问题了:

查询列字段类型

  1. (mito.class.column:table-column-type (first user-slots))
  2. ;; (:VARCHAR 64)

查询列属性是否可为空

  1. (mito.class.column:table-column-not-null-p
  2. (first user-slots))
  3. ;; T
  4. (mito.class.column:table-column-not-null-p
  5. (second user-slots))
  6. ;; NIL

测试

我们不会在生产环境下测试数据库的操作。因此需要为每个测试创建一个临时的数据库。

下面代码中的宏创建个名字随机的临时数据库、表格、执行代码,然后重新连接原有的数据库。

  1. (defpackage my-test.utils
  2. (:use :cl)
  3. (:import-from :my.models
  4. :*db*
  5. :*db-name*
  6. :connect
  7. :ensure-tables-exist
  8. :migrate-all)
  9. (:export :with-empty-db))
  10. (in-package my-test.utils)
  11. (defun random-string (length)
  12. ;; thanks 40ants/hacrm.
  13. (let ((chars "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"))
  14. (coerce (loop repeat length
  15. collect (aref chars (random (length chars))))
  16. 'string)))
  17. (defmacro with-empty-db (&body body)
  18. "Run `body` with a new temporary DB."
  19. `(let* ((*random-state* (make-random-state t))
  20. (prefix (concatenate 'string
  21. (random-string 8)
  22. "/"))
  23. ;; Save our current DB connection.
  24. (connection (when (mito.connection:connected-p)
  25. mito:*connection*)))
  26. (uiop:with-temporary-file (:pathname name :prefix prefix)
  27. ;; Bind our *db-name* to a new name, so as to create a new DB.
  28. (let* ((*db-name* name))
  29. ;; Always re-connect to our real DB even in case of error in body.
  30. (unwind-protect
  31. (progn
  32. ;; our functions to connect to the DB, create the tables and run the migrations.
  33. (connect)
  34. (ensure-tables-exist)
  35. (migrate-all)
  36. ,@body)
  37. (setf mito:*connection* connection))))))

使用方法如下:

  1. (prove:subtest "Creation in a temporary DB."
  2. (with-empty-db
  3. (let ((user (make-user :name "Cookbook")))
  4. (save-user user)
  5. (prove:is (name user)
  6. "Cookbook"
  7. "Test username in a temp DB."))))
  8. ;; Creation in a temporary DB
  9. ;; CREATE TABLE "user" (
  10. ;; id BIGSERIAL NOT NULL PRIMARY KEY,
  11. ;; name VARCHAR(64) NOT NULL,
  12. ;; email VARCHAR(128) NOT NULL,
  13. ;; created_at TIMESTAMP,
  14. ;; updated_at TIMESTAMP,
  15. ;; UNIQUE (email)
  16. ;; ) () [0 rows] | MITO.DB:EXECUTE-SQL
  17. ;; Test username in a temp DB.

更多