samedi 28 novembre 2015

Single left join query with an extra level of indirection

I have three tables: users, sessions and usersessions. The users-table contains a list of all users in the system, the sessions table is a list of all the current web client sessions, and usersessions is a table which keeps track of which sessions which have user logins.

Note the distinction between sessions.id (primary key integer) and sessid (essentially the HTTP cookie).

CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL UNIQUE,
  passwd TEXT NOT NULL
);
CREATE TABLE sessions (
  id INTEGER PRIMARY KEY,
  sessid TEXT UNIQUE NOT NULL,
  last_access DATETIME NOT NULL DEFAULT current_timestamp,
  ip TEXT NOT NULL
);
CREATE TABLE usersessions (
  id INTEGER PRIMARY KEY,
  sess_id INTEGER UNIQUE NOT NULL,
  user_id INTEGER NOT NULL,
  FOREIGN KEY(sess_id) REFERENCES sessions(id),
  FOREIGN KEY(user_id) REFERENCES users(id),
  UNIQUE(sess_id, user_id)
);

I use the following test data:

users:
id          name        passwd                                  
----------  ----------  ----------------------------------------
1           frank       8843d7f92416211de9ebb963ff4ce28125932878
2           elena       8843d7f92416211de9ebb963ff4ce28125932878
3           drake       8843d7f92416211de9ebb963ff4ce28125932878

sessions:
id          sessid      last_access          ip          
----------  ----------  -------------------  ------------
1           mysess      2015-11-28 18:27:46  172.16.0.128
2           unusedsess  2015-11-28 18:27:46  10.0.0.1    

usersessions:
id          sess_id     user_id   
----------  ----------  ----------
1           1           2         

When a connection to the web server occurs, I want to query the database (using the session id from the cookie) to get the session's id (the primary key) and the logged in user id and name (if there is one).

I've found plenty of answers to similar questions, but these tend to only involve two tables; i.e. like:

SELECT s.id AS sess_id,us.user_id AS user_id,s.ip
  FROM sessions AS s
  LEFT OUTER JOIN usersessions AS us ON us.sess_id=s.id;

This will return:

sess_id     user_id     ip          
----------  ----------  ------------
1           2           172.16.0.128
2                       10.0.0.1    

This is in principle the information I want; the NULL in user_id for sess_id=1 tells me that session 2 is not a logged in session. However, what I want is to also get the user name for logged in sessions (and NULL otherwise); i.e. I want the result:

sess_id     user_id     user    ip          
----------  ----------  ------  ------------
1           2           elena   172.16.0.128
2                               10.0.0.1    

The queries I have come up with always end up filtering out the non-login sessions because of the NULL (us.user_id != u.id).

I have trivially solved this by doing two queries, but my question is if this can be done in a single query? I essentially want to set the "user" column to (conceptually) "name column from users table for u.id=us.user_id or NULL if there is no match".

I know there are other completely different solutions, like merging the tables usersessions and sessions so that user_id is a NULL:able column in sessions, or simply making two queries, etc (and these methods are exactly what I've been using throughout the years to "solve" similar situations), but I'm specifically wondering if there's a way, with the table configuration as specified above, to check if a session is valid, if it is a logged in session (and get the user name if it is), in one query?

Aucun commentaire:

Enregistrer un commentaire