Adding data to the database

There are a thousand and one ways to get data into, and out of, a database. I confess that I am not a fan of ORMs such as Hibernate because over the long term, I think they're far more costly than writing your own SQL. ORMs are convenient during development, and they keep the code consistent, but they abstract SQL so far away that it can be difficult to diagnose what's happening when your data access isn't performing how you expect it to (whether it be performance, incorrect data retrieval, or something more sinister). Of course, ORMs have their value in the sense that they're (mostly) database agnostic, but never once in my career have I worked on anything wherein the backend database was a variable.

This being said, I also loath SQL strings in my code. They're ugly, they're hard to read, they remove much of the built-in SQL highlighting and support of many modern IDEs, they force you to create weird classes with nothing but SQL templates in them, and they just feel wrong.

For the most part, those two options have been our only options: either write your own SQL inside your Java/Clojure/C#/Python/Whatever code, or have some library mysteriously generate it for you. The risk/reward is six of one and a half dozen of the other.

It's for these reasons why I was so strongly attracted to YeSQL.

What is YeSQL?

YeSQL is a tiny Clojure library, which generates functions out of your own SQL. That's right. You write your own SQL code in a separate .sql file, and at runtime YeSQL pulls it in and turns it into a function. You then call that function, passing it an argument map, and voila! Data!

This allows you to keep your SQL separate from the rest of your Clojure code, but more importantly, it allows you to treat your SQL as SQL.

For example, pretend we have the following query in a file called foobars.sql:

SELECT *
FROM foo
WHERE
  severity = 'BAR'

YeSQL can pull this query in and wrap it in a Clojure function in two brain dead steps:

  1. Annotate the query with a name and optional doc string:
    -- name: get-foobars
    -- Gets all the really bad foos. All of them.
    SELECT *
    FROM foo
    WHERE
      severity='BAR'
  2. In your namespace, slurp in the SQL file:
    (require '[yesql.core :refer [defqueries])
    (defqueries "foobars.sql" [some database connection stuff])

That's it. Now our namespace will have access to a function that matches the name annotation we set on the query, in our case, get-foobars. We call that query the same way we call any other function in Clojure. Not only this, but any extra documentation we added to the query also gets pulled in as a docstring. Nifty!

The rest of this book will use YeSQL for all interactions with the database.

Note

You can find the docs for YeSQL 0.5.0-beta2 at https://github.com/krisajenkins/yesql/tree/v0.5.0-beta2.

Getting YeSQL

As with everything else Leiningen based, grabbing the YeSQL library is a one-step process. In our project.clj, add the following to the :dependencies list:

[yesql "0.5.0-beta2"]

That's all we have to do. There is no additional configuration or setup.

YeSQL 0.5.0-beta2 is a bit of a departure from 0.4.0, and is not backwards compatible. YeSQL 0.4.0 used positional arguments for all the generated functions, which were difficult to maintain after the fact (think about insert statements! Craziness!). 0.5.x does away with positional arguments and uses map arguments. This makes the code much cleaner and easier to use. We will be using YeSQL 0.5.x because, with luck, it will be stable by the time this book comes to light.

Adding a user to the database

The easiest way to get started with YeSQL is for us to complete our User Signup form. To get started, do the following:

  1. Create a new directory, src/hipstr/models, where we'll put our .sql files and other database like code.
  2. Create a new SQL file in the models directory called users.sql. This is where we will put our SQL for inserting and getting a user.

Create a new Clojure file alongside users.sql, called user_model.clj. This will be a (very) thin wrapper around the YeSQL generated query functions.

Inserting a user using SQL

Our users table is a simple one, which only requires three values from us at the time of inserting: username, email, and password (the others, created_at, updated_at, and user_id are autogenerated for us). As such, the SQL is straight forward. Open the users.sql file and enter the following:

-- name: insert-user<!
-- Inserts a new user into the Users table
-- Expects :username, :email, and :password
INSERT INTO users (username, email, password)
VALUES (:username, :email, :password)

The preceding code looks pretty standard. We're inserting a new row into the users table, and are providing each the username, email, and password columns a value. In the VALUES clause, we are specifying a :username, :email, and :password key, each of which will be bound to the map passed to the generated function.

SQL aside, we also used the name: annotation, which YeSQL uses for the name of the generated function, and a docstring, which YeSQL also uses to document the generated function. However, there's another interesting syntax, and that's the <! suffix we gave the name.

There are two special suffixes we can use in the names of our YeSQL queries. These suffixes tell YeSQL to return an appropriate result when mutable queries (that is, queries that insert/update/delete/alter the database) are executed. These suffixes are as follows:

  • <!: Query names ending in <! will return either the primary key, or the entire altered row, depending on the database driver. Using PostgreSQL, the entire row will be returned. Take a look at the following example:
    (insert-user<! {:username "TheDude"
                    :email "[email protected]"
                    :password "abc123"})
    => {:update_date #inst "2014-09-27T19:02:25.206296000-00:00"
        :create_date #inst "2014-09-27T19:02:25.206296000-00:00"
        :pass "abc123" :email "[email protected]"
        :username "TheDude", :user_id 13}
  • !: Query names ending in ! will return the number of rows affected by the query. For example, if we changed our insert-user<! to just insert-user!, our code would produce the following:
    (insert-user! {:username "TheDude"
                   :email "[email protected]"
                   :password "abc123"})
    => 1

These two special characters are convenient when we want to report appropriate outcomes to the client.

Inserting a user using Clojure

Now that we have the SQL file with the insert statement, we need to give our user_model.clj file some meat. Open up the user_model.clj file and add the following code:

(ns hipstr.models.user-model
  (:require [yesql.core :refer [defqueries]]))

(def db-spec {:classname "org.postgresql.Driver"  ;#1
              :subprotocol "postgresql"
              :subname     "//localhost/postgres"
              :user        "hipstr"
              :password    "p455w0rd"})

(defqueries "hipstr/models/users.sql"
            {:connection db-spec})    ; #2

The preceding code is all we need for YeSQL to generate a series of functions out of all the SQL we write in the users.sql file. At #1 we are defining our database connection. This should look pretty familiar to the JDBC connection maps that one provides when using vanilla clojure.jdbc (which is what YeSQL uses under the hood—there's no magic there).

Code at #2 is, however, where the magic happens. At #2 we tell YeSQL," Hey, go look at the users.sql file and generate a function for each query, and for each query use this database connection." In essence, our SQL becomes our data model, and the user_model namespace is a thin shim connecting our business to the data model.

Passing the connection to defqueries is a convenience and not necessary. If we left it out, however, we would have to provide the connection anytime we called the generated function, for example (insert-user<! {:username…} {db-spec}). This gets annoying, and it creates unnecessary clutter in our code.

Note

The database connection doesn't have to be defined as part of the user_model namespace, and indeed, it shouldn't, as all our models will connect to the same database. We will refactor this as part of Chapter 11, Environment Configurations and Deployment.

We can do a sanity check using the REPL to ensure that the preceding code actually works:

# lein repl
hipstr.repl=> (load "models/user_model")
hipstr.repl=> (ns hipstr.models.user-model)
hipstr.repl=> (:doc (meta insert-user<!))
>> "Inserts a new user into the Users table
Expects :username, :email, and :password"
hipstr.repl=> (insert-user<! {:username "test" :email "[email protected]" :password "abc123"})
>> {:updated_at #inst "2014-12-03T07:48:53...." :created_at #inst "2014-12-03T07:48:53..." :password "abc123" :email "[email protected]" :username "test" :user_id 1}

With the knowledge that it works, we can complete our Signup process.

Bringing it all together

So far, we've created a users table, a SQL query for inserting a user record, and a thin shim that generates Clojure functions from our SQL queries. It's time we bring these into our Signup workflow and get the user in the database.

Adjusting the route

As it stands, the only thing we actually have to do is adjust our /signup POST route. Open the hipstr.routes.home namespace and perform the following steps:

  1. Adjust the :require form to make use of our new user-model namespace:
    (:require [compojure.core :refer :all]
      …
      [hipstr.models.user-model :as u]))
  2. Adjust our signup-page-submit function such that we'll add the new user to the database if all validations pass:
    (defn signup-page-submit [user]
      (let [errors (v/validate-signup user)]
        (if (empty? errors)
          (do
            (u/insert-user<! user)
            (response/redirect "/signup-success"))
          (layout/render "signup.html"
                         (assoc user :errors errors)))))

That's all we need to do. We simply pass our user map to the function generated by YeSQL and our newly signed-up user is saved to the database. This works because our user form's field names map perfectly to the keys expected by the generated insert-user<! function. However, there is one more thing we'll want to do with our data before we save it.

Encrypting the password

We are good developers. We care (somewhat) about security (I hope). As such, we'll want to encrypt the password before we store it in the database. This is not something we want to worry about every time we add a new user, so we'll add a small function in our hipstr.models.user-model namespace, which will encrypt the password before we throw it in the database.

The easiest way to encrypt a password is to use crypto-password, a tiny encryption library written by James Reeves (yep, him again). To encrypt our user's password, do the following:

  1. Add the crypto-password dependency by adding the following code to our project.clj dependencies:
    [crypto-password "0.1.3"]
  2. Add crypto-password as a requirement in our hisptr.models.user-model namespace. The crypto-password lib supports 3 encryption schemes: PBKDF2, bcrypt, and scrypt. For the purpose of this exercise, we will use bcrypt:
    (ns hipstr.models.user-model
      (:require [yesql.core :refer [defqueries]]
        [crypto.password.bcrypt :as password]))
  3. Create a small function, add-user!, which our /signup POST route will call. This function will simply encrypt the :password key on the incoming user map before passing it off to the insert-user<! function generated by YeSQL.
    (defn add-user!
      "Saves a user to the database."
      [user]
      (let [new-user (->> (password/encrypt (:password user))
                          (assoc user :password)
                           insert-user<!)]
                          (dissoc new-user :pass)))
  4. Finally, adjust the /signup POST route back in our hipstr.routes.home namespace to use the new hipstr.models.user-model/add-user! function instead of directly calling the insert-user<! function:
    …
    (do
      (u/add-user! user)
      (response/redirect "/signup-success"))))

We are now storing a fully encrypted password in our database, and we barely had to do a thing. Go ahead and restart hipstr, and start accepting billions of users.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.21.46.78