diff --git a/rails-to-caveman.asd b/rails-to-caveman.asd index 8c73239..c51d218 100644 --- a/rails-to-caveman.asd +++ b/rails-to-caveman.asd @@ -26,6 +26,7 @@ (:file "web" :depends-on ("view")) (:file "view" :depends-on ("config")) (:file "db" :depends-on ("config")) + (:file "model" :depends-on ("db")) ; <--- Chapter 4. (:file "config")))) :description "" :in-order-to ((test-op (test-op "rails-to-caveman-test")))) diff --git a/src/config.lisp b/src/config.lisp index 3cad47d..38a8ab1 100644 --- a/src/config.lisp +++ b/src/config.lisp @@ -2,14 +2,14 @@ (defpackage rails-to-caveman.config (:use :cl) (:import-from :envy - :config-env-var + :config-env-var :defconfig) (:export :config - :*application-root* + :*application-root* :*static-directory* - :*template-directory* + :*template-directory* :appenv - :developmentp + :developmentp :productionp)) (in-package :rails-to-caveman.config) @@ -20,16 +20,47 @@ (defparameter *template-directory* (merge-pathnames #P"templates/" *application-root*)) (defconfig :common - `(:databases ((:maindb :sqlite3 :database-name ":memory:")))) + #| CHANGED DATABASE FROM IN-MEMORY(CHAPTER 4) + ============================================== + Changed the database name from ':memory:' to 'rails-to-caveman' + in Chapter 4. Be default, Caveman sets the database to + 'in-memory'. You need to override this everytime you want to + your a persistent database in your app. + + You can name your database whatever you want. I have just went + with 'rails-to-caveman' because the tutorial said so. + |# + + #| EXCERPT TAKEN FROM TUTORIAL (CHAPTER 4): + ============================================ + Initially: I set the database-name to “your-app”, but later when I + tried to access the database, I got angry with CANTOPEN. So I + changed the name to “your_app” and it worked fine. Apparently, words + separated by . Dashes Are Capitalized At The Beginning . The Name + Specified In The Code Here Is "Your-App", But The Registered File + Name Is "Your-App" However MITO:CONNECT-TOPLEVEL, If You + Specify "Your-App": Database-Name, The Case conversion of the + implicit reason is not performed and the file "your-app" is searched + for in a case-sensitive manner, resulting in an error. Here, for + safety, the database name is defined in camel case without symbols. + + The database file is created in the current directory. If you don't + like this, you can specify the absolute path for: + DATABASE-NAME. *APPLICATION-ROOT* The variable is created DEFCONFIG, + the argument to is :DATABASE-NAME(DATABASE-PATH), and there is + also a "db "directory in the project directory, so I don't wonder if + it will do it well. I think it's unfriendly to have no documentation. + |# + `(:databases ((:maindb :sqlite3 :database-name "rails-to-caveman.db")))) (defconfig |development| - '()) + '()) (defconfig |production| - '()) + '()) (defconfig |test| - '()) + '()) (defun config (&optional key) (envy:config #.(package-name *package*) key)) diff --git a/src/db.lisp b/src/db.lisp index 9ed7289..1023c75 100644 --- a/src/db.lisp +++ b/src/db.lisp @@ -18,6 +18,13 @@ (defun db (&optional (db :maindb)) (apply #'connect-cached (connection-settings db))) +;; ORIGINAL VERSION BEFORE CHAPTER 4 +;; (defmacro with-connection (conn &body body) +;; `(let ((*connection* ,conn)) +;; ,@body)) + +;; REPLACE ABOVE IN CHAPTER 4 +;; This replaces 'datafly' with 'mito'. (defmacro with-connection (conn &body body) - `(let ((*connection* ,conn)) + `(let ((mito.connection:*connection* ,conn)) ; <--- This! ,@body)) diff --git a/src/model.lisp b/src/model.lisp new file mode 100644 index 0000000..6f176df --- /dev/null +++ b/src/model.lisp @@ -0,0 +1,236 @@ +;; (in-package :cl-user) ; Not sure if this needs to exist (Chapter 4) +(defpackage :rails-to-caveman.model + (:use #:cl + #:rails-to-caveman.db) + (:export #:seeds + #:ids)) +(in-package #:rails-to-caveman.model) + +;;; Defines the USER table class for the database. +;;; Will be used by mito (an ORM). +(defclass user () + ((number + :col-type + :integer + :initarg + :number + :reader number-of) + (name :col-type (:varchar 64) + :initarg + :name + :reader name-of) + (full-name :col-type + (or (:varchar 128) :null) + :initarg + :full-name + :reader full-name-of) + (email :col-type + (or :null :text) + :initarg + :email + :accessor email-of) + (birthday :col-type + (or :null :date) + :initarg + :birthday + :reader birthday-of) + (sex :col-type + :integer + :initarg + :sex + :initform 1 + :reader sex-of) + + #| CHANGED :ACCESSOR VALUE FROM TUTORIAL (CHAPTER 4) + =================================================== + The Chapter 4 tutorial has the :accessor value set to + 'administratorp'. Unfortunately, this causes initialisation + argument errors when trying to seed the database. To fix this, I + had to remove the 'p' part from that line. At the time of + writing, I do not know if that will have a negative effect on + future tutorials. + + I have left a note in the 'seeds' function below highlighting the + change to the :accessor value. + |# + (administrator :col-type + :boolean + :initarg + :administrator + :initform nil + :accessor administrator)) + (:metaclass mito:dao-table-class)) + + +(defun seeds() + ;; '#(' are ARRAY LITERALS. I keep forgetting this and need to look + ;; it up. + (let ((names + #("Taro" "Jiro" "Hana" "John" "Mike" "Sophy" "Bill" "Alex" "Mary" "Tom")) + (fnames ; First Names + #("Hippo" "Darling" "Lopez" "Jerry")) + (gnames ; Given Names + #("Orange" "Fox" "Snake"))) + (with-connection (db) + (dotimes (x 10) + (mito:create-dao 'user + :number (+ x 10) + :name (aref names x) + :full-name (format nil "~A ~A" + (aref fnames (rem x 4)) + (aref gnames (rem x 3))) + :email (format nil "~A@example.com" (aref names x)) + :birthday "1981-12-01" + :sex (nth (rem x 3) '(1 1 2)) + ;; Removed 'p' from end of :administrator -- + ;; so the code differs from the code in the + ;; tutorial (Chapter 4). I had to change it + ;; because it produced errors when trying to + ;; seed the database (using 'seeds' + ;; function. I have, also, left a note in the + ;; 'user' class definition highlighting this. + :administrator (zerop 0)))))) + + +(defun rebuild () + "Drops the current database table, recreates it and populates it using seeded data." + (with-connection (db) + (mito:recreate-table 'user)) + (seeds)) + +(defun ids () + "Produces a list of all the Id's in the database. Part of Chapter 4 +tutorial and is a port of the 'ids method' in the Ruby on Rails book +this tutorial was translated/ported from." + (rails-to-caveman.db:with-connection (rails-to-caveman.db:db) + (mapcar #'mito:object-id + (mito:retrieve-dao 'rails-to-caveman.model::user)))) + + +#| STAND ALONE BITS OF CODE +=========================== +The code below is to use in a live coding environment. Think of it as +a persistent scratch pad -- for when you close Emacs but still want +those little snippets of code which do not have a place in the main +code base but are useful for little tests/proof-of-concepts. + +When you have loaded the system in SLIME, use c-c c-c to run the code +below. +|# + +(with-connection (db) ; Creates a table in the database called 'user'. + (mito:ensure-table-exists 'user)) + +;; Retrieves the record with the specified Id. Change ':id' to +;; retrieve different entries from the database. +;; NOTE: EVAL. THIS IN SLIME BEFORE USING (DESCRIBE *) FURTHER DOWN +;; THE FILE. +(rails-to-caveman.db:with-connection (rails-to-caveman.db:db) + (mito:find-dao 'rails-to-caveman.model::user :id 3)) + +;; Retrieves the database entry with the specified name. +(rails-to-caveman.db:with-connection (rails-to-caveman.db:db) + (mito:find-dao 'rails-to-caveman.model::user :name "Taro")) + +#| SQLITE DATABASE BOOLEAN TYPES +================================ +Use multiple columns to specify with data you want to retrieve. +Because SQLite database does not have a Boolean type, + +:administrator : 0 = false +1 = true +:sex 1 = male +2 = female + +SQLite type quirk and how the tutorial decided to model the data in +the database. +|# +(rails-to-caveman.db:with-connection (rails-to-caveman.db:db) + (mito:find-dao 'rails-to-caveman.model::user :sex 1 :administrator 1)) + +;; A common strategy to deal with SQLite Boolean types is to set +;; constants and refer to them instead of passing hard coded 1 & 0. +(defconstant +false+ 0) +(defconstant +true+ 1) + +(rails-to-caveman.db:with-connection (rails-to-caveman.db:db) + (mito:retrieve-dao 'rails-to-caveman.model::user :administrator +false+)) + +(ids) ; Lists out all the Id's (in SLIME) in the 'users' table. + +(seeds) ; Populates the database with seeded data (in 'users') + +;; Drops the current table, creates a new one and populates it with +;; seeded data -- in the 'users' tables. +(rebuild) + +#| SEEING THE CONTENTS OF A DATABASE ENTRY IN SLIME. +==================================================== +This is a bit picky. First of all, you need to retrieve a database +entry (in SLIME) before checking the contents. Search for 'NOTE: +EVAL.' for the code. After you have retrieved the entry from the +database, calling (describe *) will produce something like the +following, + +[standard-object] Slots with +:INSTANCE allocation: +CREATED-AT = @yyyy-mm-ddThh:mm:ss.ms+tz +UPDATED-AT = @yyyy-mm-ddThh:mm:ss.ms+tz +SYNCED = T +ID = 3 +NUMBER = 12 +NAME = "Hana" +FULL-NAME = "高橋 花子" +EMAIL = "Hana@example.com" +BIRTHDAY = @yyyy-mm-ddThh:mm:ss.ms+tz +SEX = 2 +ADMINISTRATOR = NIL + +THIS IS SPECIFIC TO SBCL -- OTHER IMPLEMENTATIONS MIGHT PRODUCE +DIFFERENT OUTPUTS +|# +(describe *) + + +#| BUILD QUERIES WITH SXQL AND MITO +=================================== +If you want to build a complex query, use it in combination +MITO.DAO:SELECT-DAO with sxql . + +The one in the previous section MITO:RETRIEVE-DAOis equivalent to the +following code. + +COPY THE CODE BELOW INTO SLIME OR WORK THEM INTO A FUNCTION. +|# +(rails-to-caveman.db:with-connection (rails-to-caveman.db:db) + (mito:select-dao 'rails-to-caveman.model::user + (sxql:where '(:= :administrator 0)))) + +(rails-to-caveman.db:with-connection (rails-to-caveman.db:db) + (mito:select-dao 'rails-to-caveman.model::user + (sxql:where + '(:and (:= :name "Taro") + (:< :number 20))))) + +(rails-to-caveman.db:with-connection (rails-to-caveman.db:db) + (mito:select-dao 'rails-to-caveman.model::user + (sxql:where '(:= :sex 2)) + (sxql:order-by :number))) + +(rails-to-caveman.db:with-connection (rails-to-caveman.db:db) + (mito:select-dao 'rails-to-caveman.model::user + (sxql:where '(:= :sex 2)) + (sxql:order-by (:desc :number)))) + +(rails-to-caveman.db:with-connection (rails-to-caveman.db:db) + (mito:select-dao 'rails-to-caveman.model::user + (sxql:where + `(:or ,@(mapcar (lambda(num) + `(:= :number ,num)) + '(15 17 19)))))) + +(rails-to-caveman.db:with-connection (rails-to-caveman.db:db) + (mito:select-dao 'rails-to-caveman.model::user + (sxql:where + `(:and (:<= 12 :number) + (:<= :number 14))))