updated Continue tutorial to use db library

This commit is contained in:
Ryan Culpepper 2011-08-25 12:11:17 -06:00
parent 93222d4fba
commit 26145b4655
2 changed files with 115 additions and 127 deletions

View File

@ -2,6 +2,7 @@
@(require scribble/manual
(for-label racket)
(for-label web-server/servlet)
(for-label db)
"tutorial-util.rkt")
@title{Continue: Web Applications in Racket}
@ -964,110 +965,121 @@ So, in the next section, we'll talk about how to use an SQL database to store ou
web-server/scribblings/tutorial/dummy-sqlite)]
@(require (for-label web-server/scribblings/tutorial/dummy-sqlite))
Our next task is to employ an SQL database for the blog model. We'll be using SQLite with the @racketmodname[(planet jaymccarthy/sqlite:4)] PLaneT package. We add the following to the top of our model:
Our next task is to employ an SQL database for the blog model. We'll
be using SQLite with the @racketmodname[db] library. We add the
following to the top of our model:
@racketblock[
(require (prefix-in sqlite: (planet jaymccarthy/sqlite:4)))
(require db)
]
We now have the following bindings:
We will use the following bindings from the @racketmodname[db]
library: @racket[connection?], @racket[sqlite3-connect],
@racket[query-exec], @racket[query-list], and @racket[query-value].
@defthing[sqlite:db? (any/c . -> . boolean?)]
@defthing[sqlite:open (path? . -> . sqlite:db?)]
@defthing[sqlite:exec/ignore (sqlite:db? string? . -> . void)]
@defthing[sqlite:select (sqlite:db? string? . -> . (listof (vectorof (or/c integer? number? string? bytes? false/c))))]
@defthing[sqlite:insert (sqlite:db? string? . -> . integer?)]
The first thing we should do is decide on the relational structure of our model. We will use the following tables:
The first thing we should do is decide on the relational structure of
our model. We will use the following tables:
@verbatim{
CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT, body TEXT)
CREATE TABLE comments (pid INTEGER, content TEXT)
}
Each post will have an identifier, a title, and a body. This is the same as our old Racket structure,
except we've added the identifier. (Actually, there was always an identifier---the memory pointer---but now
we have to make it explicit in the database.)
Each post will have an identifier, a title, and a body. This is the
same as our old Racket structure, except we've added the
identifier. (Actually, there was always an identifier---the memory
pointer---but now we have to make it explicit in the database.)
Each comment is tied to a post by the post's identifier and has textual content. We could have chosen to
serialize comments with @racket[write] and add a new TEXT column to the posts table to store the value.
By adding a new comments table, we are more in accord with the relational style.
Each comment is tied to a post by the post's identifier and has
textual content. We could have chosen to serialize comments with
@racket[write] and add a new TEXT column to the posts table to store
the value. By adding a new comments table, we are more in accord with
the relational style.
A @racket[blog] structure will simply be a container for the database handle:
A @racket[blog] structure will simply be a container for the database
handle:
@defstruct*[blog ([db sqlite:db?])]
@defstruct*[blog ([db connection?])]
@bold{Exercise.} Write the @racket[blog] structure definition. (It does not need to be mutable or serializable.)
@bold{Exercise.} Write the @racket[blog] structure definition. (It
does not need to be mutable or serializable.)
We can now write the code to initialize a @racket[blog] structure:
@racketblock[
@code:comment{initialize-blog! : path? -> blog?}
@code:comment{Sets up a blog database (if it doesn't exist)}
(define (initialize-blog! home)
(define db (sqlite:open home))
(define db (sqlite3-connect #:database home #:mode 'create))
(define the-blog (blog db))
(with-handlers ([exn? void])
(sqlite:exec/ignore db
(string-append
"CREATE TABLE posts "
"(id INTEGER PRIMARY KEY,"
"title TEXT, body TEXT)"))
(query-exec db
(string-append
"CREATE TABLE posts "
"(id INTEGER PRIMARY KEY, title TEXT, body TEXT)"))
(blog-insert-post!
the-blog "First Post" "This is my first post")
(blog-insert-post!
the-blog "Second Post" "This is another post")
(sqlite:exec/ignore
db "CREATE TABLE comments (pid INTEGER, content TEXT)")
(query-exec db
"CREATE TABLE comments (pid INTEGER, content TEXT)")
(post-insert-comment!
the-blog (first (blog-posts the-blog))
"First comment!"))
the-blog)
]
@racket[sqlite:open] will create a database if one does not already exist at the @racket[home] path. But, we still need
to initialize the database with the table definitions and initial data.
With the @racket['create] mode, @racket[db:sqlite3-connect] will
create a database if one does not already exist at the @racket[home]
path. But, we still need to initialize the database with the table
definitions and initial data.
We used @racket[blog-insert-post!] and @racket[post-insert-comment!] to initialize the database. Let's see their implementation:
We used @racket[blog-insert-post!] and @racket[post-insert-comment!]
to initialize the database. Let's see their implementation:
@racketblock[
@code:comment{blog-insert-post!: blog? string? string? -> void}
@code:comment{Consumes a blog and a post, adds the post at the top of the blog.}
(define (blog-insert-post! a-blog title body)
(sqlite:insert
(query-exec
(blog-db a-blog)
(format "INSERT INTO posts (title, body) VALUES ('~a', '~a')"
title body)))
"INSERT INTO posts (title, body) VALUES (?, ?)"
title body))
@code:comment{post-insert-comment!: blog? post string -> void}
@code:comment{Consumes a blog, a post and a comment string. As a side-effect,}
@code:comment{adds the comment to the bottom of the post's list of comments.}
(define (post-insert-comment! a-blog p a-comment)
(sqlite:insert
(query-exec
(blog-db a-blog)
(format
"INSERT INTO comments (pid, content) VALUES ('~a', '~a')"
(post-id p) a-comment)))
"INSERT INTO comments (pid, content) VALUES (?, ?)"
(post-id p) a-comment))
]
@bold{Exercise.} Find the security hole common to these two functions.
@centerline{------------}
Note that the SQL queries above use SQL placeholders, written
@litchar{?}, instead of @racket[format] placeholders, written
@litchar{~a}. This way, the query is submitted as-is to SQLite, which
parses it and then applies the arguments. This approach ensures that
the arguments are treated as data.
If we had used @racket[format] to do simple string substitution
instead, a malicious user could submit a post with a title like,
@racket["null', 'null') and INSERT INTO accounts (username, password)
VALUES ('ur','hacked"] and get @racket[query-exec] to make two INSERTs
instead of one. This is called an SQL injection attack.
@centerline{------------}
A user could submit a post with a title like, @racket["null', 'null') and INSERT INTO accounts (username, password) VALUES ('ur','hacked"] and get our simple @racket[sqlite:insert] to make two INSERTs instead of one.
In @racket[post-insert-comment!], we used @racket[post-id], but we
have not yet defined the new @racket[post] structure. It @emph{seems}
like a @racket[post] should be represented by an integer id, because
the post table contains an integer as the identifying value.
This is called an SQL injection attack. It can be resolved by using
prepared statements that let SQLite do the proper quoting for us. Refer
to the SQLite package documentation for usage.
@centerline{------------}
In @racket[post-insert-comment!], we used @racket[post-id], but we have not yet defined the new @racket[post] structure.
It @emph{seems} like a @racket[post] should be represented by an integer id, because the post table contains an integer as the identifying value.
However, we cannot tell from this structure
what blog this posts belongs to, and therefore, what database; so, we could not extract the title or body values,
since we do not know what to query. Therefore, we should associate the blog with each post:
However, we cannot tell from this structure what blog this posts
belongs to, and therefore, what database; so, we could not extract the
title or body values, since we do not know what to query. Therefore,
we should associate the blog with each post:
@defstruct*[post ([blog blog?] [id integer?])]
@ -1079,37 +1091,32 @@ The only function that creates posts is @racket[blog-posts]:
@code:comment{blog-posts : blog -> (listof post?)}
@code:comment{Queries for the post ids}
(define (blog-posts a-blog)
(local [(define (row->post a-row)
(post
a-blog
(vector-ref a-row 0)))
(define rows (sqlite:select
(blog-db a-blog)
"SELECT id FROM posts"))]
(cond [(empty? rows)
empty]
[else
(map row->post (rest rows))])))
(local [(define (id->post an-id)
(post a-blog an-id))]
(map id->post
(query-list
(blog-db a-blog)
"SELECT id FROM posts"))))
]
@racket[sqlite:select] returns a list of vectors. The first element of the list is the name of the columns.
Each vector has one element for each column. Each element is a string representation of the value.
@racket[query-list] is used with queries that return a single
column (e.g., @racket["SELECT id FROM posts"]). It returns a list of
that column's values.
At this point we can write the functions that operate on posts:
@racketblock[
@code:comment{post-title : post -> string?}
@code:comment{Queries for the title}
(define (post-title a-post)
(vector-ref
(second
(sqlite:select
(blog-db (post-blog a-post))
(format
"SELECT title FROM posts WHERE id = '~a'"
(post-id a-post))))
0))
(query-value
(blog-db (post-blog a-post))
"SELECT title FROM posts WHERE id = ?"
(post-id a-post)))
]
@racket[query-value] is used with queries that return a single
value (that is, one row and one column).
@bold{Exercise.} Write the definition of @racket[post-body].
@bold{Exercise.} Write the definition of @racket[post-comments].
@ -1117,7 +1124,8 @@ At this point we can write the functions that operate on posts:
@centerline{------------}
The only change that we need to make to the application is to require the new model. The interface is exactly the same!
The only change that we need to make to the application is to require
the new model. The interface is exactly the same!
@centerline{------------}

View File

@ -1,8 +1,8 @@
#lang racket
(require (prefix-in sqlite: (planet jaymccarthy/sqlite:4)))
(require db)
;; A blog is a (make-blog db)
;; where db is an sqlite database handle
;; where db is an sqlite connection
(struct blog (db))
;; A post is a (make-post blog id)
@ -12,20 +12,19 @@
;; initialize-blog! : path? -> blog?
;; Sets up a blog database (if it doesn't exist)
(define (initialize-blog! home)
(define db (sqlite:open home))
(define db (sqlite3-connect #:database home #:mode 'create))
(define the-blog (blog db))
(with-handlers ([exn? void])
(sqlite:exec/ignore db
(string-append
"CREATE TABLE posts "
"(id INTEGER PRIMARY KEY,"
"title TEXT, body TEXT)"))
(query-exec db
(string-append
"CREATE TABLE posts "
"(id INTEGER PRIMARY KEY, title TEXT, body TEXT)"))
(blog-insert-post!
the-blog "First Post" "This is my first post")
(blog-insert-post!
the-blog "Second Post" "This is another post")
(sqlite:exec/ignore
db "CREATE TABLE comments (pid INTEGER, content TEXT)")
(query-exec db
"CREATE TABLE comments (pid INTEGER, content TEXT)")
(post-insert-comment!
the-blog (first (blog-posts the-blog))
"First comment!"))
@ -34,72 +33,53 @@
;; blog-posts : blog -> (listof post?)
;; Queries for the post ids
(define (blog-posts a-blog)
(local [(define (row->post a-row)
(post
a-blog
(vector-ref a-row 0)))
(define rows (sqlite:select
(blog-db a-blog)
"SELECT id FROM posts"))]
(cond [(empty? rows)
empty]
[else
(map row->post (rest rows))])))
(local [(define (id->post an-id)
(post a-blog an-id))]
(map id->post
(query-list
(blog-db a-blog)
"SELECT id FROM posts"))))
;; post-title : post -> string?
;; Queries for the title
(define (post-title a-post)
(vector-ref
(second
(sqlite:select
(blog-db (post-blog a-post))
(format "SELECT title FROM posts WHERE id = '~a'"
(post-id a-post))))
0))
(query-value
(blog-db (post-blog a-post))
"SELECT title FROM posts WHERE id = ?"
(post-id a-post)))
;; post-body : post -> string?
;; Queries for the body
(define (post-body p)
(vector-ref
(second
(sqlite:select
(blog-db (post-blog p))
(format "SELECT body FROM posts WHERE id = '~a'"
(post-id p))))
0))
(query-value
(blog-db (post-blog p))
"SELECT body FROM posts WHERE id = ?"
(post-id p)))
;; post-comments : post -> (listof string?)
;; Queries for the comments
(define (post-comments p)
(local [(define (row->comment a-row)
(vector-ref a-row 0))
(define rows
(sqlite:select
(blog-db (post-blog p))
(format
"SELECT content FROM comments WHERE pid = '~a'"
(post-id p))))]
(cond
[(empty? rows) empty]
[else (map row->comment (rest rows))])))
(query-list
(blog-db (post-blog p))
"SELECT content FROM comments WHERE pid = ?"
(post-id p)))
;; blog-insert-post!: blog? string? string? -> void
;; Consumes a blog and a post, adds the post at the top of the blog.
(define (blog-insert-post! a-blog title body)
(sqlite:insert
(query-exec
(blog-db a-blog)
(format "INSERT INTO posts (title, body) VALUES ('~a', '~a')"
title body)))
"INSERT INTO posts (title, body) VALUES (?, ?)"
title body))
;; post-insert-comment!: blog? post string -> void
;; Consumes a blog, a post and a comment string. As a side-efect,
;; adds the comment to the bottom of the post's list of comments.
(define (post-insert-comment! a-blog p a-comment)
(sqlite:insert
(query-exec
(blog-db a-blog)
(format
"INSERT INTO comments (pid, content) VALUES ('~a', '~a')"
(post-id p) a-comment)))
"INSERT INTO comments (pid, content) VALUES (?, ?)"
(post-id p) a-comment))
(provide blog? blog-posts
post? post-title post-body post-comments