diff --git a/collects/web-server/scribblings/tutorial/dummy-sqlite.ss b/collects/web-server/scribblings/tutorial/dummy-sqlite.ss new file mode 100644 index 0000000000..90bd2722d0 --- /dev/null +++ b/collects/web-server/scribblings/tutorial/dummy-sqlite.ss @@ -0,0 +1,10 @@ +#lang scheme/base + +(define db? #f) +(define sqlite:open #f) +(define sqlite:exec/ignore #f) +(define sqlite:select #f) +(define sqlite:insert #f) + +(provide (all-defined-out)) + diff --git a/collects/web-server/scribblings/tutorial/tutorial.scrbl b/collects/web-server/scribblings/tutorial/tutorial.scrbl index 9f02c47460..f483484ed1 100644 --- a/collects/web-server/scribblings/tutorial/tutorial.scrbl +++ b/collects/web-server/scribblings/tutorial/tutorial.scrbl @@ -915,7 +915,179 @@ And our application is: This approach to persistence can work surprisingly well for simple applications. But as our application's needs grow, we will have to deal with concurrency issues, the lack of a simply query language over our data model, etc. -So, in a future tutorial, we'll talk about how to use an SQL database to store our blog model. +So, in the next section, we'll talk about how to use an SQL database to store our blog model. + +@section{Using an SQL database} +@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-10 + web-server/scribblings/tutorial/examples/model-3 + web-server/scribblings/tutorial/dummy-sqlite)] +@(require (for-label web-server/scribblings/tutorial/dummy-sqlite)) +@;@(require (prefix-in sqlite: (for-label (planet jaymccarthy/sqlite:3/sqlite)))) + +Our next task is to employ an SQL database for the blog model. We'll be using SQLite with the @schememodname[(planet jaymccarthy/sqlite:3/sqlite)] PLaneT package. We add the following to the top of our model: + +@schemeblock[ +(require (prefix-in sqlite: (planet jaymccarthy/sqlite:3/sqlite))) +] + +We now have the following bindings: + +@defthing[db? (any/c . -> . boolean?)] +@defthing[sqlite:open (path? . -> . db?)] +@defthing[sqlite:exec/ignore (db? string? . -> . void)] +@defthing[sqlite:select (db? string? . -> . (listof vector?))] +@defthing[sqlite:insert (db? string? . -> . integer?)] + + +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 Scheme 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 @scheme[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 @scheme[blog] structure will simply be a container for the database handle: + +@defstruct[blog ([db db?])] + +@bold{Exercise.} Write the @scheme[blog] structure definition. (It does not need to be mutable or serializable.) + +We can now write the code to initialize a @scheme[blog] structure: +@schemeblock[ +@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 the-blog (make-blog db)) + (with-handlers ([exn? (lambda (exn) (void))]) + (sqlite:exec/ignore 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)") + (post-insert-comment! + the-blog (first (blog-posts the-blog)) + "First comment!")) + the-blog) +] + +@scheme[sqlite:open] will create a database if one does not already exist at the @scheme[home] path. But, we still need +to initialize the database with the table definitions and initial data. + +We used @scheme[blog-insert-post!] and @scheme[post-insert-comment!] to initialize the database. Let's see their implementation: + +@schemeblock[ +@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 + (blog-db a-blog) + (format "INSERT INTO posts (title, body) VALUES ('~a', '~a')" + 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 + (blog-db a-blog) + (format + "INSERT INTO comments (pid, content) VALUES ('~a', '~a')" + (post-id p) a-comment))) +] + +@bold{Exercise.} Find the security hole common to these two functions. + +@centerline{------------} + +A user could submit a post with a title like, @scheme{null', 'null') and INSERT INTO accounts (username, password) VALUES ('ur','hacked} and get our simple @scheme[sqlite:insert] to make two INSERTs instead of one. + + 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 @scheme[post-insert-comment!], we used @scheme[post-id], but we have not yet defined the new @scheme[post] structure. +It @emph{seems} like a @scheme[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: + +@defstruct[post ([blog blog?] [id integer?])] + +@bold{Exercise.} Write the structure definition for posts. + +The only function that creates posts is @scheme[blog-posts]: + +@schemeblock[ +@code:comment{blog-posts : blog -> (listof post?)} +@code:comment{Queries for the post ids} +(define (blog-posts a-blog) + (map (compose (lambda (n) (make-post a-blog n)) + string->number + (lambda (v) (vector-ref v 0))) + (rest (sqlite:select (blog-db a-blog) + "SELECT id FROM posts")))) +] + +@scheme[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. + +At this point we can write the functions that operate on posts: +@schemeblock[ +@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)) +] + +@bold{Exercise.} Write the definition of @scheme[post-body]. + +@bold{Exercise.} Write the definition of @scheme[post-comments]. +(Hint: Use @scheme[blog-posts] as a template, not @scheme[post-title].) + +@centerline{------------} + +The only change that we need to make to the application is to require the new model. The interface is exactly the same! + +@centerline{------------} + +Our model is now: + +@external-file["model-3.ss"] + +And our application is: + +@schememod[ +web-server/insta + +(require "model-3.ss") + +.... +] @section{Leaving DrScheme} @@ -991,6 +1163,6 @@ refer to the PLT Web Server Reference Manual for details. @section{Moving Forward} -As you move forward on your own applications, you may find many useful packages on PLaneT. There are interfaces to +As you move forward on your own applications, you may find many useful packages on PLaneT. There are interfaces to other databases. Many tools for generating HTML, XML, and Javascript output. Etc. There is also an active community of users on the @scheme[plt-scheme] mailing list. We welcome new users! \ No newline at end of file