954 lines
33 KiB
HTML
954 lines
33 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta name="generator" content=
|
||
"HTML Tidy for HTML5 for Linux version 5.2.0">
|
||
<title>Database Access and Persistence</title>
|
||
<meta charset="utf-8">
|
||
<meta name="description" content="A collection of examples of using Common Lisp">
|
||
<meta name="viewport" content=
|
||
"width=device-width, initial-scale=1">
|
||
<link rel="icon" href=
|
||
"assets/cl-logo-blue.png"/>
|
||
<link rel="stylesheet" href=
|
||
"assets/style.css">
|
||
<script type="text/javascript" src=
|
||
"assets/highlight-lisp.js">
|
||
</script>
|
||
<script type="text/javascript" src=
|
||
"assets/jquery-3.2.1.min.js">
|
||
</script>
|
||
<script type="text/javascript" src=
|
||
"assets/jquery.toc/jquery.toc.min.js">
|
||
</script>
|
||
<script type="text/javascript" src=
|
||
"assets/toggle-toc.js">
|
||
</script>
|
||
|
||
<link rel="stylesheet" href=
|
||
"assets/github.css">
|
||
|
||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
|
||
</head>
|
||
<body>
|
||
<h1 id="title-xs"><a href="index.html">The Common Lisp Cookbook</a> – Database Access and Persistence</h1>
|
||
<div id="logo-container">
|
||
<a href="index.html">
|
||
<img id="logo" src="assets/cl-logo-blue.png"/>
|
||
</a>
|
||
|
||
<div id="searchform-container">
|
||
<form onsubmit="duckSearch()" action="javascript:void(0)">
|
||
<input id="searchField" type="text" value="" placeholder="Search...">
|
||
</form>
|
||
</div>
|
||
|
||
<div id="toc-container" class="toc-close">
|
||
<div id="toc-title">Table of Contents</div>
|
||
<ul id="toc" class="list-unstyled"></ul>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="content-container">
|
||
<h1 id="title-non-xs"><a href="index.html">The Common Lisp Cookbook</a> – Database Access and Persistence</h1>
|
||
|
||
<!-- Announcement we can keep for 1 month or more. I remove it and re-add it from time to time. -->
|
||
<p class="announce">
|
||
📹 <a href="https://www.udemy.com/course/common-lisp-programming/?couponCode=6926D599AA-LISP4ALL">NEW! Learn Lisp in videos and support our contributors with this 40% discount.</a>
|
||
</p>
|
||
<p class="announce-neutral">
|
||
📕 <a href="index.html#download-in-epub">Get the EPUB and PDF</a>
|
||
</p>
|
||
|
||
|
||
<div id="content"
|
||
<p>The
|
||
<a href="https://github.com/CodyReichert/awesome-cl#database">Database section on the Awesome-cl list</a>
|
||
is a resource listing popular libraries to work with different kind of
|
||
databases. We can group them roughly in four categories:</p>
|
||
|
||
<ul>
|
||
<li>wrappers to one database engine (cl-sqlite, postmodern, cl-redis,…),</li>
|
||
<li>interfaces to several DB engines (clsql, sxql,…),</li>
|
||
<li>persistent object databases (bknr.datastore (see chap. 21 of “Common Lisp Recipes”), ubiquitous,…),</li>
|
||
<li><a href="https://en.wikipedia.org/wiki/Object-relational_mapping">Object Relational Mappers</a> (Mito),</li>
|
||
</ul>
|
||
|
||
<p>and other DB-related tools (pgloader).</p>
|
||
|
||
<p>We’ll begin with an overview of Mito. If you must work with an
|
||
existing DB, you might want to have a look at cl-dbi and clsql. If you
|
||
don’t need a SQL database and want automatic persistence of Lisp
|
||
objects, you also have a choice of libraries.</p>
|
||
|
||
<h2 id="the-mito-orm-and-sxql">The Mito ORM and SxQL</h2>
|
||
|
||
<p>Mito is in Quicklisp:</p>
|
||
|
||
<pre><code class="language-lisp">(ql:quickload "mito")
|
||
</code></pre>
|
||
|
||
<h3 id="overview">Overview</h3>
|
||
|
||
<p><a href="https://github.com/fukamachi/mito">Mito</a> is “an ORM for Common Lisp
|
||
with migrations, relationships and PostgreSQL support”.</p>
|
||
|
||
<ul>
|
||
<li>it <strong>supports MySQL, PostgreSQL and SQLite3</strong>,</li>
|
||
<li>when defining a model, it adds an <code>id</code> (serial primary key),
|
||
<code>created_at</code> and <code>updated_at</code> fields by default like Ruby’s
|
||
ActiveRecord or Django,</li>
|
||
<li>handles DB <strong>migrations</strong> for the supported backends,</li>
|
||
<li>permits DB <strong>schema versioning</strong>,</li>
|
||
<li>is tested under SBCL and CCL.</li>
|
||
</ul>
|
||
|
||
<p>As an ORM, it allows to write class definitions, to specify relationships, and
|
||
provides functions to query the database. For custom queries, it relies on
|
||
<a href="https://github.com/fukamachi/sxql">SxQL</a>, an SQL generator that provides the
|
||
same interface for several backends.</p>
|
||
|
||
<p>Working with Mito generally involves these steps:</p>
|
||
|
||
<ul>
|
||
<li>connecting to the DB</li>
|
||
<li>writing <a href="clos.html">CLOS</a> classes to define models</li>
|
||
<li>running migrations to create or alter tables</li>
|
||
<li>creating objects, saving same in the DB,</li>
|
||
</ul>
|
||
|
||
<p>and iterating.</p>
|
||
|
||
<h3 id="connecting-to-a-db">Connecting to a DB</h3>
|
||
|
||
<p>Mito provides the function <code>connect-toplevel</code> to establish a
|
||
connection to RDBMs:</p>
|
||
|
||
<pre><code class="language-lisp">(mito:connect-toplevel :mysql :database-name "myapp" :username "fukamachi" :password "c0mon-1isp")
|
||
</code></pre>
|
||
|
||
<p>The driver type can be of <code>:mysql</code>, <code>:sqlite3</code> and <code>:postgres</code>.</p>
|
||
|
||
<p>With sqlite you don’t need the username and password:</p>
|
||
|
||
<pre><code class="language-lisp">(mito:connect-toplevel :sqlite3 :database-name "myapp")
|
||
</code></pre>
|
||
|
||
<p>As usual, you need to create the MySQL or PostgreSQL database beforehand.
|
||
Refer to their documentation.</p>
|
||
|
||
<p>Connecting sets <code>mito:*connection*</code> to the new connection and returns it.</p>
|
||
|
||
<p>Disconnect with <code>disconnect-toplevel</code>.</p>
|
||
|
||
<p>You might make good use of a wrapper function:</p>
|
||
|
||
<pre><code class="language-lisp">(defun connect ()
|
||
"Connect to the DB."
|
||
(mito:connect-toplevel :sqlite3 :database-name "myapp"))
|
||
</code></pre>
|
||
|
||
<h3 id="models">Models</h3>
|
||
|
||
<h4 id="defining-models">Defining models</h4>
|
||
|
||
<p>In Mito, you can define a class which corresponds to a database table with the <code>deftable</code> macro:</p>
|
||
|
||
<pre><code class="language-lisp">(mito:deftable user ()
|
||
((name :col-type (:varchar 64))
|
||
(email :col-type (or (:varchar 128) :null)))
|
||
</code></pre>
|
||
<p>Alternatively, you can specify <code>(:metaclass mito:dao-table-class)</code> in a regular class definition.</p>
|
||
|
||
<p>The <code>deftable</code> macro automatically adds some slots: a primary key named <code>id</code> if there’s no primary key, and <code>created_at</code> and <code>updated_at</code> for recording timestamps. Specifying <code>(:auto-pk nil)</code> and <code>(:record-timestamps nil)</code> in the <code>deftable</code> form will disable these behaviours. A <code>deftable</code> class will also come with initializers, named after the slot, and accessors, of form <code><class-name>-<slot-name></code>, for each named slot. For example, for the <code>name</code> slot in the above table definition, the initarg <code>:name</code> will be added to the constuctor, and the accessor <code>user-name</code> will be created.</p>
|
||
|
||
<p>You can inspect the new class:</p>
|
||
|
||
<pre><code class="language-lisp">(mito.class:table-column-slots (find-class 'user))
|
||
;=> (#<MITO.DAO.COLUMN:DAO-TABLE-COLUMN-CLASS MITO.DAO.MIXIN::ID>
|
||
; #<MITO.DAO.COLUMN:DAO-TABLE-COLUMN-CLASS COMMON-LISP-USER::NAME>
|
||
; #<MITO.DAO.COLUMN:DAO-TABLE-COLUMN-CLASS COMMON-LISP-USER::EMAIL>
|
||
; #<MITO.DAO.COLUMN:DAO-TABLE-COLUMN-CLASS MITO.DAO.MIXIN::CREATED-AT>
|
||
; #<MITO.DAO.COLUMN:DAO-TABLE-COLUMN-CLASS MITO.DAO.MIXIN::UPDATED-AT>)
|
||
</code></pre>
|
||
|
||
<p>The class inherits <code>mito:dao-class</code> implicitly.</p>
|
||
|
||
<pre><code class="language-lisp">(find-class 'user)
|
||
;=> #<MITO.DAO.TABLE:DAO-TABLE-CLASS COMMON-LISP-USER::USER>
|
||
|
||
(c2mop:class-direct-superclasses *)
|
||
;=> (#<STANDARD-CLASS MITO.DAO.TABLE:DAO-CLASS>)
|
||
</code></pre>
|
||
|
||
<p>This may be useful when you define methods which can be applied for
|
||
all table classes.</p>
|
||
|
||
<p>For more information on using the Common Lisp Object System, see the
|
||
<a href="clos.html">clos</a> page.</p>
|
||
|
||
<h4 id="creating-the-tables">Creating the tables</h4>
|
||
|
||
<p>After defining the models, you must create the tables:</p>
|
||
|
||
<pre><code class="language-lisp">(mito:ensure-table-exists 'user)
|
||
</code></pre>
|
||
|
||
<p>So a helper function:</p>
|
||
|
||
<pre><code class="language-lisp">(defun ensure-tables ()
|
||
(mapcar #'mito:ensure-table-exists '(user foo bar)))
|
||
</code></pre>
|
||
|
||
<p>See
|
||
<a href="https://github.com/fukamachi/mito#generating-table-definitions">Mito’s documentation</a>
|
||
for a couple more ways.</p>
|
||
|
||
<p>When you alter the model you’ll need to run a DB migration, see the next section.</p>
|
||
|
||
<h4 id="fields">Fields</h4>
|
||
|
||
<h5 id="fields-types">Fields types</h5>
|
||
|
||
<p>Field types are:</p>
|
||
|
||
<p><code>(:varchar <integer>)</code> ,</p>
|
||
|
||
<p><code>:serial</code>, <code>:bigserial</code>, <code>:integer</code>, <code>:bigint</code>, <code>:unsigned</code>,</p>
|
||
|
||
<p><code>:timestamp</code>, <code>:timestamptz</code>,</p>
|
||
|
||
<p><code>:bytea</code>,</p>
|
||
|
||
<h5 id="optional-fields">Optional fields</h5>
|
||
|
||
<p>Use <code>(or <real type> :null)</code>:</p>
|
||
|
||
<pre><code class="language-lisp"> (email :col-type (or (:varchar 128) :null))
|
||
</code></pre>
|
||
|
||
<h5 id="field-constraints">Field constraints</h5>
|
||
|
||
<p><code>:unique-keys</code> can be used like so:</p>
|
||
|
||
<pre><code class="language-lisp">(mito:deftable user ()
|
||
((name :col-type (:varchar 64))
|
||
(email :col-type (:varchar 128))
|
||
(:unique-keys email))
|
||
</code></pre>
|
||
|
||
<p>We already saw <code>:primary-key</code>.</p>
|
||
|
||
<p>You can change the table name with <code>:table-name</code>.</p>
|
||
|
||
<h4 id="relationships">Relationships</h4>
|
||
|
||
<p>You can define a relationship by specifying a foreign class with <code>:col-type</code>:</p>
|
||
|
||
<pre><code class="language-lisp">(mito:deftable tweet ()
|
||
((status :col-type :text)
|
||
;; This slot refers to USER class
|
||
(user :col-type user))
|
||
|
||
(table-definition (find-class 'tweet))
|
||
;=> (#<SXQL-STATEMENT: CREATE TABLE tweet (
|
||
; id BIGSERIAL NOT NULL PRIMARY KEY,
|
||
; status TEXT NOT NULL,
|
||
; user_id BIGINT NOT NULL,
|
||
; created_at TIMESTAMP,
|
||
; updated_at TIMESTAMP
|
||
; )>)
|
||
</code></pre>
|
||
|
||
<p>Now you can create or retrieve a <code>TWEET</code> by a <code>USER</code> object, not a <code>USER-ID</code>.</p>
|
||
|
||
<pre><code class="language-lisp">(defvar *user* (mito:create-dao 'user :name "Eitaro Fukamachi"))
|
||
(mito:create-dao 'tweet :user *user*)
|
||
|
||
(mito:find-dao 'tweet :user *user*)
|
||
</code></pre>
|
||
|
||
<p>Mito doesn’t add foreign key constraints for referring tables.</p>
|
||
|
||
<h5 id="one-to-one">One-to-one</h5>
|
||
|
||
<p>A one-to-one relationship is simply represented with a simple foreign
|
||
key on a slot (as <code>:col-type user</code> in the <code>tweet</code> class). Besides, we
|
||
can add a unicity constraint, as with <code>(:unique-keys email)</code>.</p>
|
||
|
||
<h5 id="one-to-many-many-to-one">One-to-many, many-to-one</h5>
|
||
|
||
<p>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.</p>
|
||
|
||
<p>The relationship is defined with a foreign key on the “many” side
|
||
linking back to the “one” side. Here the <code>tweet</code> class defines a
|
||
<code>user</code> foreign key, so a tweet can only have one user. You didn’t need
|
||
to edit the <code>user</code> class.</p>
|
||
|
||
<p>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.</p>
|
||
|
||
<h5 id="many-to-many">Many-to-many</h5>
|
||
|
||
<p>A many-to-many relationship needs an intermediate table, which will be
|
||
the “many” side for the two tables it is the intermediary of.</p>
|
||
|
||
<p>And, thanks to the join table, we can store more information about the relationship.</p>
|
||
|
||
<p>Let’s define a <code>book</code> class:</p>
|
||
|
||
<pre><code class="language-lisp">(mito:deftable book ()
|
||
((title :col-type (:varchar 128))
|
||
(ean :col-type (or (:varchar 128) :null))))
|
||
</code></pre>
|
||
|
||
<p>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:</p>
|
||
|
||
<pre><code class="language-lisp">(mito:deftable user-books ()
|
||
((user :col-type user)
|
||
(book :col-type book)))
|
||
</code></pre>
|
||
|
||
<p>Each time we want to add a book to a user’s collection (say in
|
||
a <code>add-book</code> function), we create a new <code>user-books</code> object.</p>
|
||
|
||
<p>But someone may very well own many copies of one book. This is an
|
||
information we can store in the join table:</p>
|
||
|
||
<pre><code class="language-lisp">(mito:deftable user-books ()
|
||
((user :col-type user)
|
||
(book :col-type book)
|
||
;; Set the quantity, 1 by default:
|
||
(quantity :col-type :integer)))
|
||
</code></pre>
|
||
|
||
<h4 id="inheritance-and-mixin">Inheritance and mixin</h4>
|
||
|
||
<p>A subclass of DAO-CLASS is allowed to be inherited. This may be useful
|
||
when you need classes which have similar columns:</p>
|
||
|
||
<pre><code class="language-lisp">(mito:deftable user ()
|
||
((name :col-type (:varchar 64))
|
||
(email :col-type (:varchar 128)))
|
||
(:unique-keys email))
|
||
|
||
(mito:deftable temporary-user (user)
|
||
((registered-at :col-type :timestamp)))
|
||
|
||
(mito:table-definition 'temporary-user)
|
||
;=> (#<SXQL-STATEMENT: CREATE TABLE temporary_user (
|
||
; id BIGSERIAL NOT NULL PRIMARY KEY,
|
||
; name VARCHAR(64) NOT NULL,
|
||
; email VARCHAR(128) NOT NULL,
|
||
; registered_at TIMESTAMP NOT NULL,
|
||
; created_at TIMESTAMP,
|
||
; updated_at TIMESTAMP,
|
||
; UNIQUE (email)
|
||
; )>)
|
||
</code></pre>
|
||
|
||
<p>If you need a ‘template’ for tables which aren’t related to any
|
||
database tables, you can use <code>DAO-TABLE-MIXIN</code> in a <code>defclass</code> form. The <code>has-email</code>
|
||
class below will not create a table.</p>
|
||
|
||
<pre><code class="language-lisp">(defclass has-email ()
|
||
((email :col-type (:varchar 128)
|
||
:initarg :email
|
||
:accessor object-email))
|
||
(:metaclass mito:dao-table-mixin)
|
||
(:unique-keys email))
|
||
;=> #<MITO.DAO.MIXIN:DAO-TABLE-MIXIN COMMON-LISP-USER::HAS-EMAIL>
|
||
|
||
(mito:deftable user (has-email)
|
||
((name :col-type (:varchar 64))))
|
||
;=> #<MITO.DAO.TABLE:DAO-TABLE-CLASS COMMON-LISP-USER::USER>
|
||
|
||
(mito:table-definition 'user)
|
||
;=> (#<SXQL-STATEMENT: CREATE TABLE user (
|
||
; id BIGSERIAL NOT NULL PRIMARY KEY,
|
||
; name VARCHAR(64) NOT NULL,
|
||
; email VARCHAR(128) NOT NULL,
|
||
; created_at TIMESTAMP,
|
||
; updated_at TIMESTAMP,
|
||
; UNIQUE (email)
|
||
; )>)
|
||
</code></pre>
|
||
|
||
<p>See more examples of use in <a href="https://github.com/fukamachi/mito-auth/">mito-auth</a>.</p>
|
||
|
||
<h4 id="troubleshooting">Troubleshooting</h4>
|
||
|
||
<h5 id="cannot-change-class-objects-into-class-metaobjects">“Cannot CHANGE-CLASS objects into CLASS metaobjects.”</h5>
|
||
|
||
<p>If you get the following error message:</p>
|
||
|
||
<pre><code>Cannot CHANGE-CLASS objects into CLASS metaobjects.
|
||
[Condition of type SB-PCL::METAOBJECT-INITIALIZATION-VIOLATION]
|
||
See also:
|
||
The Art of the Metaobject Protocol, CLASS [:initialization]
|
||
</code></pre>
|
||
|
||
<p>it is certainly because you first wrote a class definition and <em>then</em>
|
||
added the Mito metaclass and tried to evaluate the class definition
|
||
again.</p>
|
||
|
||
<p>If this happens, you must remove the class definition from the current package:</p>
|
||
|
||
<pre><code class="language-lisp">(setf (find-class 'foo) nil)
|
||
</code></pre>
|
||
|
||
<p>or, with the Slime inspector, click on the class and find the “remove” button.</p>
|
||
|
||
<p>More info <a href="https://stackoverflow.com/questions/38811931/how-to-change-classs-metaclass">here</a>.</p>
|
||
|
||
<h3 id="migrations">Migrations</h3>
|
||
|
||
<p>We can run database migrations manually, as shown below, or we can
|
||
automatically run migrations after a change to the model
|
||
definitions. To enable automatic migrations, set <code>mito:*auto-migration-mode*</code> to <code>t</code>.</p>
|
||
|
||
<p>The first step is to create the tables, if needed:</p>
|
||
|
||
<pre><code class="language-lisp">(ensure-table-exists 'user)
|
||
</code></pre>
|
||
|
||
<p>then alter the tables:</p>
|
||
|
||
<pre><code class="language-lisp">(mito:migrate-table 'user)
|
||
</code></pre>
|
||
|
||
<p>You can check the SQL generated code with <code>migration-expressions
|
||
'class</code>. For example, we create the <code>user</code> table:</p>
|
||
|
||
<pre><code class="language-lisp">(ensure-table-exists 'user)
|
||
;-> ;; CREATE TABLE IF NOT EXISTS "user" (
|
||
; "id" BIGSERIAL NOT NULL PRIMARY KEY,
|
||
; "name" VARCHAR(64) NOT NULL,
|
||
; "email" VARCHAR(128),
|
||
; "created_at" TIMESTAMP,
|
||
; "updated_at" TIMESTAMP
|
||
; ) () [0 rows] | MITO.DAO:ENSURE-TABLE-EXISTS
|
||
</code></pre>
|
||
|
||
<p>There are no changes from the previous user definition:</p>
|
||
|
||
<pre><code class="language-lisp">(mito:migration-expressions 'user)
|
||
;=> NIL
|
||
</code></pre>
|
||
|
||
<p>Now let’s add a unique <code>email</code> field:</p>
|
||
|
||
<pre><code class="language-lisp">(mito:deftable user ()
|
||
((name :col-type (:varchar 64))
|
||
(email :col-type (:varchar 128)))
|
||
(:unique-keys email))
|
||
</code></pre>
|
||
|
||
<p>The migration will run the following code:</p>
|
||
|
||
<pre><code class="language-lisp">(mito:migration-expressions 'user)
|
||
;=> (#<SXQL-STATEMENT: ALTER TABLE user ALTER COLUMN email TYPE character varying(128), ALTER COLUMN email SET NOT NULL>
|
||
; #<SXQL-STATEMENT: CREATE UNIQUE INDEX unique_user_email ON user (email)>)
|
||
</code></pre>
|
||
|
||
<p>so let’s apply it:</p>
|
||
|
||
<pre><code class="language-lisp">(mito:migrate-table 'user)
|
||
;-> ;; ALTER TABLE "user" ALTER COLUMN "email" TYPE character varying(128), ALTER COLUMN "email" SET NOT NULL () [0 rows] | MITO.MIGRATION.TABLE:MIGRATE-TABLE
|
||
; ;; CREATE UNIQUE INDEX "unique_user_email" ON "user" ("email") () [0 rows] | MITO.MIGRATION.TABLE:MIGRATE-TABLE
|
||
;-> (#<SXQL-STATEMENT: ALTER TABLE user ALTER COLUMN email TYPE character varying(128), ALTER COLUMN email SET NOT NULL>
|
||
; #<SXQL-STATEMENT: CREATE UNIQUE INDEX unique_user_email ON user (email)>)
|
||
</code></pre>
|
||
|
||
<h3 id="queries">Queries</h3>
|
||
|
||
<h4 id="creating-objects">Creating objects</h4>
|
||
|
||
<p>We can create user objects with the regular <code>make-instance</code>:</p>
|
||
|
||
<pre><code class="language-lisp">(defvar me
|
||
(make-instance 'user :name "Eitaro Fukamachi" :email "e.arrows@gmail.com"))
|
||
;=> USER
|
||
</code></pre>
|
||
|
||
<p>To save it in DB, use <code>insert-dao</code>:</p>
|
||
|
||
<pre><code class="language-lisp">(mito:insert-dao me)
|
||
;-> ;; 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
|
||
;=> #<USER {10053C4453}>
|
||
</code></pre>
|
||
|
||
<p>Do the two steps above at once:</p>
|
||
|
||
<pre><code class="language-lisp">(mito:create-dao 'user :name "Eitaro Fukamachi" :email "e.arrows@gmail.com")
|
||
</code></pre>
|
||
|
||
<p>You should not export the <code>user</code> class and create objects outside of
|
||
its package (it is good practice anyway to keep all database-related
|
||
operations in say a <code>models</code> package and file). You should instead use
|
||
a helper function:</p>
|
||
|
||
<pre><code class="language-lisp">(defun make-user (&key name)
|
||
(make-instance 'user :name name))
|
||
</code></pre>
|
||
|
||
<h4 id="updating-fields">Updating fields</h4>
|
||
|
||
<pre><code class="language-lisp">(setf (slot-value me 'name) "nitro_idiot")
|
||
;=> "nitro_idiot"
|
||
</code></pre>
|
||
|
||
<p>and save it:</p>
|
||
|
||
<pre><code class="language-lisp">(mito:save-dao me)
|
||
</code></pre>
|
||
|
||
<h4 id="deleting">Deleting</h4>
|
||
|
||
<pre><code class="language-lisp">(mito:delete-dao me)
|
||
;-> ;; DELETE FROM `user` WHERE (`id` = ?) (1) [0 rows] | MITO.DAO:DELETE-DAO
|
||
|
||
;; or:
|
||
(mito:delete-by-values 'user :id 1)
|
||
;-> ;; DELETE FROM `user` WHERE (`id` = ?) (1) [0 rows] | MITO.DAO:DELETE-DAO
|
||
</code></pre>
|
||
|
||
<h4 id="get-the-primary-key-value">Get the primary key value</h4>
|
||
|
||
<pre><code class="language-lisp">(mito:object-id me)
|
||
;=> 1
|
||
</code></pre>
|
||
|
||
<h4 id="count">Count</h4>
|
||
|
||
<pre><code class="language-lisp">(mito:count-dao 'user)
|
||
;=> 1
|
||
</code></pre>
|
||
|
||
<h4 id="find-one">Find one</h4>
|
||
|
||
<pre><code class="language-lisp">(mito:find-dao 'user :id 1)
|
||
;-> ;; SELECT * FROM `user` WHERE (`id` = ?) LIMIT 1 (1) [1 row] | MITO.DB:RETRIEVE-BY-SQL
|
||
;=> #<USER {10077C6073}>
|
||
</code></pre>
|
||
|
||
<p>So here’s a possibility of generic helpers to find an object by a given key:</p>
|
||
|
||
<pre><code class="language-lisp">(defgeneric find-user (key-name key-value)
|
||
(:documentation "Retrieves an user from the data base by one of the unique
|
||
keys."))
|
||
|
||
(defmethod find-user ((key-name (eql :id)) (key-value integer))
|
||
(mito:find-dao 'user key-value))
|
||
|
||
(defmethod find-user ((key-name (eql :name)) (key-value string))
|
||
(first (mito:select-dao 'user
|
||
(sxql:where (:= :name key-value)))))
|
||
</code></pre>
|
||
|
||
<h4 id="find-all">Find all</h4>
|
||
|
||
<p>Use the macro <code>select-dao</code>.</p>
|
||
|
||
<p>Get a list of all users:</p>
|
||
|
||
<pre><code class="language-lisp">(mito:select-dao 'user)
|
||
;(#<USER {10077C6073}>)
|
||
;#<SXQL-STATEMENT: SELECT * FROM user>
|
||
</code></pre>
|
||
|
||
<h4 id="find-by-relationship">Find by relationship</h4>
|
||
|
||
<p>As seen above:</p>
|
||
|
||
<pre><code class="language-lisp">(mito:find-dao 'tweet :user *user*)
|
||
</code></pre>
|
||
|
||
<h4 id="custom-queries">Custom queries</h4>
|
||
|
||
<p>It is with <code>select-dao</code> that you can write more precise queries by
|
||
giving it <a href="https://github.com/fukamachi/sxql">SxQL</a> statements.</p>
|
||
|
||
<p>Example:</p>
|
||
|
||
<pre><code class="language-lisp">(select-dao 'tweet
|
||
(where (:like :status "%Japan%")))
|
||
</code></pre>
|
||
|
||
<p>another:</p>
|
||
|
||
<pre><code class="language-lisp">(select (:id :name :sex)
|
||
(from (:as :person :p))
|
||
(where (:and (:>= :age 18)
|
||
(:< :age 65)))
|
||
(order-by (:desc :age)))
|
||
</code></pre>
|
||
|
||
<p>You can compose your queries with regular Lisp code:</p>
|
||
|
||
<pre><code class="language-lisp">(defun find-tweets (&key user)
|
||
(select-dao 'tweet
|
||
(when user
|
||
(where (:= :user user)))
|
||
(order-by :object-created)))
|
||
</code></pre>
|
||
|
||
<p><code>select-dao</code> is a macro that expands to the right thing©.</p>
|
||
|
||
<div class="info-box info">
|
||
<strong>Note:</strong> if you didn't <code>use</code> SXQL, then write <code>(sxql:where …)</code> and <code>(sxql:order-by …)</code>.
|
||
</div>
|
||
<p><br /></p>
|
||
|
||
<p>You can compose your queries further with the backquote syntax.</p>
|
||
|
||
<p>Imagine you receive a <code>query</code> string, maybe composed of
|
||
space-separated words, and you want to search for books that have
|
||
either one of these words in their title or in their author’s
|
||
name. Searching for “bob adventure” would return a book that has
|
||
“adventure” in its title and “bob” in its author name, or both in the
|
||
title.</p>
|
||
|
||
<p>For the example sake, an author is a string, not a link to another table:</p>
|
||
|
||
<pre><code class="language-lisp">(mito:deftable book ()
|
||
((title :col-type (:varchar 128))
|
||
(author :col-type (:varchar 128))
|
||
(ean :col-type (or (:varchar 128) :null))))
|
||
</code></pre>
|
||
|
||
<p>You want to add a clause that searches on both fields for each word.</p>
|
||
|
||
<pre><code class="language-lisp">(defun find-books (&key query (order :desc))
|
||
"Return a list of books. If a query string is given, search on both the title and the author fields."
|
||
(mito:select-dao 'book
|
||
(when (str:non-blank-string-p query)
|
||
(sxql:where
|
||
`(:and
|
||
,@(loop for word in (str:words query)
|
||
:collect `(:or (:like :title ,(str:concat "%" word "%"))
|
||
(:like :authors ,(str:concat "%" word "%")))))))
|
||
(sxql:order-by `(,order :created-at))))
|
||
</code></pre>
|
||
|
||
<p>By the way, we are still using a <code>LIKE</code> statement, but with a non-small dataset you’ll want to use your database’s full text search engine.</p>
|
||
|
||
<h4 id="clauses">Clauses</h4>
|
||
|
||
<p>See the <a href="https://github.com/fukamachi/sxql#sql-clauses">SxQL documentation</a>.</p>
|
||
|
||
<p>Examples:</p>
|
||
|
||
<pre><code class="language-lisp">(select-dao 'foo
|
||
(where (:and (:> :age 20) (:<= :age 65))))
|
||
</code></pre>
|
||
|
||
<pre><code class="language-lisp">(order-by :age (:desc :id))
|
||
</code></pre>
|
||
|
||
<pre><code class="language-lisp">(group-by :sex)
|
||
</code></pre>
|
||
|
||
<pre><code class="language-lisp">(having (:>= (:sum :hoge) 88))
|
||
</code></pre>
|
||
|
||
<pre><code class="language-lisp">(limit 0 10)
|
||
</code></pre>
|
||
|
||
<p>and <code>join</code>s, etc.</p>
|
||
|
||
<h4 id="operators">Operators</h4>
|
||
|
||
<pre><code class="language-lisp">:not
|
||
:is-null, :not-null
|
||
:asc, :desc
|
||
:distinct
|
||
:=, :!=
|
||
:<, :>, :<= :>=
|
||
:a<, :a>
|
||
:as
|
||
:in, :not-in
|
||
:like
|
||
:and, :or
|
||
:+, :-, :* :/ :%
|
||
:raw
|
||
</code></pre>
|
||
|
||
<h3 id="triggers">Triggers</h3>
|
||
|
||
<p>Since <code>insert-dao</code>, <code>update-dao</code> and <code>delete-dao</code> are defined as generic
|
||
functions, you can define <code>:before</code>, <code>:after</code> or <code>:around</code> methods to those, like regular <a href="clos.html#qualifiers-and-method-combination">method combination</a>.</p>
|
||
|
||
<pre><code class="language-lisp">(defmethod mito:insert-dao :before ((object user))
|
||
(format t "~&Adding ~S...~%" (user-name object)))
|
||
|
||
(mito:create-dao 'user :name "Eitaro Fukamachi" :email "e.arrows@gmail.com")
|
||
;-> Adding "Eitaro Fukamachi"...
|
||
; ;; 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
|
||
;=> #<USER {100835FB33}>
|
||
</code></pre>
|
||
|
||
<h3 id="inflationdeflation">Inflation/Deflation</h3>
|
||
|
||
<p>Inflation/Deflation is a function to convert values between Mito and RDBMS.</p>
|
||
|
||
<pre><code class="language-lisp">(mito:deftable user-report ()
|
||
((title :col-type (:varchar 100))
|
||
(body :col-type :text
|
||
:initform "")
|
||
(reported-at :col-type :timestamp
|
||
:initform (local-time:now)
|
||
:inflate #'local-time:universal-to-timestamp
|
||
:deflate #'local-time:timestamp-to-universal)))
|
||
</code></pre>
|
||
|
||
<h3 id="eager-loading">Eager loading</h3>
|
||
|
||
<p>One of the pains in the neck to use ORMs is the “N+1 query” problem.</p>
|
||
|
||
<pre><code class="language-lisp">;; BAD EXAMPLE
|
||
|
||
(use-package '(:mito :sxql))
|
||
|
||
(defvar *tweets-contain-japan*
|
||
(select-dao 'tweet
|
||
(where (:like :status "%Japan%"))))
|
||
|
||
;; Getting names of tweeted users.
|
||
(mapcar (lambda (tweet)
|
||
(user-name (tweet-user tweet)))
|
||
*tweets-contain-japan*)
|
||
</code></pre>
|
||
|
||
<p>This example sends a query to retrieve a user like “SELECT * FROM user
|
||
WHERE id = ?” at each iteration.</p>
|
||
|
||
<p>To prevent this performance issue, add <code>includes</code> to the above query
|
||
which only sends a single WHERE IN query instead of N queries:</p>
|
||
|
||
<pre><code class="language-lisp">;; GOOD EXAMPLE with eager loading
|
||
|
||
(use-package '(:mito :sxql))
|
||
|
||
(defvar *tweets-contain-japan*
|
||
(select-dao 'tweet
|
||
(includes 'user)
|
||
(where (:like :status "%Japan%"))))
|
||
;-> ;; SELECT * FROM `tweet` WHERE (`status` LIKE ?) ("%Japan%") [3 row] | MITO.DB:RETRIEVE-BY-SQL
|
||
;-> ;; SELECT * FROM `user` WHERE (`id` IN (?, ?, ?)) (1, 3, 12) [3 row] | MITO.DB:RETRIEVE-BY-SQL
|
||
;=> (#<TWEET {1003513EC3}> #<TWEET {1007BABEF3}> #<TWEET {1007BB9D63}>)
|
||
|
||
;; No additional SQLs will be executed.
|
||
(tweet-user (first *))
|
||
;=> #<USER {100361E813}>
|
||
</code></pre>
|
||
|
||
<h3 id="schema-versioning">Schema versioning</h3>
|
||
|
||
<pre><code>$ ros install mito
|
||
$ mito
|
||
Usage: mito command [option...]
|
||
|
||
Commands:
|
||
generate-migrations
|
||
migrate
|
||
|
||
Options:
|
||
-t, --type DRIVER-TYPE DBI driver type (one of "mysql", "postgres" or "sqlite3")
|
||
-d, --database DATABASE-NAME Database name to use
|
||
-u, --username USERNAME Username for RDBMS
|
||
-p, --password PASSWORD Password for RDBMS
|
||
-s, --system SYSTEM ASDF system to load (several -s's allowed)
|
||
-D, --directory DIRECTORY Directory path to keep migration SQL files (default: "/Users/nitro_idiot/Programs/lib/mito/db/")
|
||
--dry-run List SQL expressions to migrate
|
||
</code></pre>
|
||
|
||
<h3 id="introspection">Introspection</h3>
|
||
|
||
<p>Mito provides some functions for introspection.</p>
|
||
|
||
<p>We can access the information of <strong>columns</strong> with the functions in
|
||
<code>(mito.class.column:...)</code>:</p>
|
||
|
||
<ul>
|
||
<li><code>table-column-[class, name, info, not-null-p,...]</code></li>
|
||
<li><code>primary-key-p</code></li>
|
||
</ul>
|
||
|
||
<p>and likewise for <strong>tables</strong> with <code>(mito.class.table:...)</code>.</p>
|
||
|
||
<p>Given we get a list of slots of our class:</p>
|
||
|
||
<pre><code class="language-lisp">(ql:quickload "closer-mop")
|
||
|
||
(closer-mop:class-direct-slots (find-class 'user))
|
||
;; (#<MITO.DAO.COLUMN:DAO-TABLE-COLUMN-CLASS NAME>
|
||
;; #<MITO.DAO.COLUMN:DAO-TABLE-COLUMN-CLASS EMAIL>)
|
||
|
||
(defparameter user-slots *)
|
||
</code></pre>
|
||
|
||
<p>We can answer the following questions:</p>
|
||
|
||
<h4 id="what-is-the-type-of-this-column-">What is the type of this column ?</h4>
|
||
|
||
<pre><code class="language-lisp">(mito.class.column:table-column-type (first user-slots))
|
||
;; (:VARCHAR 64)
|
||
</code></pre>
|
||
|
||
<h4 id="is-this-column-nullable-">Is this column nullable ?</h4>
|
||
|
||
<pre><code class="language-lisp">(mito.class.column:table-column-not-null-p
|
||
(first user-slots))
|
||
;; T
|
||
(mito.class.column:table-column-not-null-p
|
||
(second user-slots))
|
||
;; NIL
|
||
</code></pre>
|
||
|
||
<h3 id="testing">Testing</h3>
|
||
|
||
<p>We don’t want to test DB operations against the production one. We
|
||
need to create a temporary DB before each test.</p>
|
||
|
||
<p>The macro below creates a temporary DB with a random name, creates the
|
||
tables, runs the code and connects back to the original DB connection.</p>
|
||
|
||
<pre><code class="language-lisp">(defpackage my-test.utils
|
||
(:use :cl)
|
||
(:import-from :my.models
|
||
:*db*
|
||
:*db-name*
|
||
:connect
|
||
:ensure-tables-exist
|
||
:migrate-all)
|
||
(:export :with-empty-db))
|
||
|
||
(in-package my-test.utils)
|
||
|
||
(defun random-string (length)
|
||
;; thanks 40ants/hacrm.
|
||
(let ((chars "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"))
|
||
(coerce (loop repeat length
|
||
collect (aref chars (random (length chars))))
|
||
'string)))
|
||
|
||
(defmacro with-empty-db (&body body)
|
||
"Run `body` with a new temporary DB."
|
||
`(let* ((*random-state* (make-random-state t))
|
||
(prefix (concatenate 'string
|
||
(random-string 8)
|
||
"/"))
|
||
;; Save our current DB connection.
|
||
(connection mito:*connection*))
|
||
(uiop:with-temporary-file (:pathname name :prefix prefix)
|
||
;; Bind our *db-name* to a new name, so as to create a new DB.
|
||
(let* ((*db-name* name))
|
||
;; Always re-connect to our real DB even in case of error in body.
|
||
(unwind-protect
|
||
(progn
|
||
;; our functions to connect to the DB, create the tables and run the migrations.
|
||
(connect)
|
||
(ensure-tables-exist)
|
||
(migrate-all)
|
||
,@body)
|
||
|
||
(setf mito:*connection* connection))))))
|
||
</code></pre>
|
||
|
||
<p>Use it like this:</p>
|
||
|
||
<pre><code class="language-lisp">(prove:subtest "Creation in a temporary DB."
|
||
(with-empty-db
|
||
(let ((user (make-user :name "Cookbook")))
|
||
(save-user user)
|
||
|
||
(prove:is (name user)
|
||
"Cookbook"
|
||
"Test username in a temp DB."))))
|
||
;; Creation in a temporary DB
|
||
;; CREATE TABLE "user" (
|
||
;; id BIGSERIAL NOT NULL PRIMARY KEY,
|
||
;; name VARCHAR(64) NOT NULL,
|
||
;; email VARCHAR(128) NOT NULL,
|
||
;; created_at TIMESTAMP,
|
||
;; updated_at TIMESTAMP,
|
||
;; UNIQUE (email)
|
||
;; ) () [0 rows] | MITO.DB:EXECUTE-SQL
|
||
;; ✓ Test username in a temp DB.
|
||
</code></pre>
|
||
|
||
<h2 id="see-also">See also</h2>
|
||
|
||
<ul>
|
||
<li>
|
||
<p><a href="https://sites.google.com/site/sabraonthehill/postmodern-examples/exploring-a-database">exploring an existing (PostgreSQL) database with postmodern</a></p>
|
||
</li>
|
||
<li><a href="https://github.com/fukamachi/mito-attachment">mito-attachment</a></li>
|
||
<li><a href="https://github.com/fukamachi/mito-auth">mito-auth</a></li>
|
||
<li><a href="https://github.com/fukamachi/can/">can</a> a role-based access right control library</li>
|
||
<li>an advanced <a href="drafts/defmodel.lisp.html">“defmodel” macro</a>.</li>
|
||
</ul>
|
||
|
||
<!-- # todo: Generating models for an existing DB -->
|
||
|
||
|
||
<p class="page-source">
|
||
Page source: <a href="https://github.com/LispCookbook/cl-cookbook/blob/master/databases.md">databases.md</a>
|
||
</p>
|
||
</div>
|
||
|
||
<script type="text/javascript">
|
||
|
||
// Don't write the TOC on the index.
|
||
if (window.location.pathname != "/cl-cookbook/") {
|
||
$("#toc").toc({
|
||
content: "#content", // will ignore the first h1 with the site+page title.
|
||
headings: "h1,h2,h3,h4"});
|
||
}
|
||
|
||
$("#two-cols + ul").css({
|
||
"column-count": "2",
|
||
});
|
||
$("#contributors + ul").css({
|
||
"column-count": "4",
|
||
});
|
||
</script>
|
||
|
||
|
||
|
||
<div>
|
||
<footer class="footer">
|
||
<hr/>
|
||
© 2002–2021 the Common Lisp Cookbook Project
|
||
</footer>
|
||
|
||
</div>
|
||
<div id="toc-btn">T<br>O<br>C</div>
|
||
</div>
|
||
|
||
<script text="javascript">
|
||
HighlightLisp.highlight_auto({className: null});
|
||
</script>
|
||
|
||
<script type="text/javascript">
|
||
function duckSearch() {
|
||
var searchField = document.getElementById("searchField");
|
||
if (searchField && searchField.value) {
|
||
var query = escape("site:lispcookbook.github.io/cl-cookbook/ " + searchField.value);
|
||
window.location.href = "https://duckduckgo.com/?kj=b2&kf=-1&ko=1&q=" + query;
|
||
// https://duckduckgo.com/params
|
||
// kj=b2: blue header in results page
|
||
// kf=-1: no favicons
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<script async defer data-domain="lispcookbook.github.io/cl-cookbook" src="https://plausible.io/js/plausible.js"></script>
|
||
|
||
</body>
|
||
</html>
|