| | 1 | = Advanced Application Development = |
| | 2 | |
| | 3 | == Transactions == |
| | 4 | |
| | 5 | A database transaction groups multiple operations into a single atomic unit: either all operations succeed and are committed, or all are rolled back if any error occurs. This prevents the database from being left in an inconsistent state. |
| | 6 | |
| | 7 | In the FRRUAS prototype (Python + psycopg2), we implement transactions using a context manager {{{get_transaction()}}} that automatically commits on success and rolls back on exception: |
| | 8 | |
| | 9 | {{{ |
| | 10 | @contextmanager |
| | 11 | def get_transaction(): |
| | 12 | p = _get_pool() |
| | 13 | conn = p.getconn() |
| | 14 | try: |
| | 15 | conn.autocommit = False |
| | 16 | yield conn |
| | 17 | conn.commit() |
| | 18 | except Exception: |
| | 19 | conn.rollback() |
| | 20 | raise |
| | 21 | finally: |
| | 22 | p.putconn(conn) |
| | 23 | }}} |
| | 24 | |
| | 25 | === Transaction 1: Make a Reservation (conflict check + insert) === |
| | 26 | |
| | 27 | When a user creates a reservation, two operations must happen atomically: checking for time conflicts and inserting the reservation. Without a transaction, a race condition exists: two users could both pass the conflict check simultaneously, and both insert overlapping reservations. |
| | 28 | |
| | 29 | {{{ |
| | 30 | # uc_reserve.py — make_reservation() |
| | 31 | |
| | 32 | with get_transaction() as conn: |
| | 33 | with conn.cursor() as cur: |
| | 34 | # Re-check conflicts inside the transaction to prevent |
| | 35 | # race conditions (another user inserting between our |
| | 36 | # check and insert) |
| | 37 | cur.execute(""" |
| | 38 | SELECT COUNT(*) FROM reservations |
| | 39 | WHERE resource_id = %s |
| | 40 | AND status IN ('approved', 'pending') |
| | 41 | AND start_time < %s AND end_time > %s |
| | 42 | """, (rid, end_dt, start_dt)) |
| | 43 | |
| | 44 | if cur.fetchone()[0] > 0: |
| | 45 | raise RuntimeError("Conflict: another reservation was just created for this time slot") |
| | 46 | |
| | 47 | cur.execute(""" |
| | 48 | INSERT INTO reservations |
| | 49 | (start_time, end_time, status, purpose, created_at, user_id, resource_id) |
| | 50 | VALUES (%s, %s, 'pending', %s, CURRENT_TIMESTAMP, %s, %s) |
| | 51 | RETURNING reservation_id, status, created_at |
| | 52 | """, (start_dt, end_dt, purpose, user_id, rid)) |
| | 53 | |
| | 54 | result = cur.fetchone() |
| | 55 | # Transaction committed automatically on successful exit |
| | 56 | }}} |
| | 57 | |
| | 58 | === Transaction 2: Approve or Reject a Reservation (conflict re-check + status update) === |
| | 59 | |
| | 60 | When an administrator approves a reservation, the system must verify no conflicting reservation was approved in the meantime. The conflict check and status update are wrapped in a single transaction to prevent two administrators from simultaneously approving overlapping reservations. |
| | 61 | |
| | 62 | {{{ |
| | 63 | # uc_approve.py — _review_reservation() |
| | 64 | |
| | 65 | with get_transaction() as conn: |
| | 66 | with conn.cursor() as cur: |
| | 67 | # For approvals, re-check conflicts within the transaction |
| | 68 | if new_status == "approved": |
| | 69 | cur.execute(""" |
| | 70 | SELECT COUNT(*) FROM reservations |
| | 71 | WHERE resource_id = %s |
| | 72 | AND reservation_id != %s |
| | 73 | AND status = 'approved' |
| | 74 | AND start_time < %s AND end_time > %s |
| | 75 | """, (res["resource_id"], res["reservation_id"], |
| | 76 | res["end_time"], res["start_time"])) |
| | 77 | |
| | 78 | if cur.fetchone()[0] > 0: |
| | 79 | raise RuntimeError("Conflict: another reservation was approved for this time slot") |
| | 80 | |
| | 81 | cur.execute(""" |
| | 82 | UPDATE reservations |
| | 83 | SET status = %s, approved_by = %s |
| | 84 | WHERE reservation_id = %s AND status = 'pending' |
| | 85 | RETURNING reservation_id, status |
| | 86 | """, (new_status, admin_user_id, res["reservation_id"])) |
| | 87 | |
| | 88 | result = cur.fetchone() |
| | 89 | # Transaction committed automatically |
| | 90 | }}} |
| | 91 | |
| | 92 | === Transaction 3: Register a New User (email uniqueness check + insert) === |
| | 93 | |
| | 94 | User registration checks email uniqueness and inserts the new user atomically. Without a transaction, two simultaneous registrations with the same email could both pass the uniqueness check and both insert, violating the UNIQUE constraint. |
| | 95 | |
| | 96 | {{{ |
| | 97 | # uc_users.py — register_user() |
| | 98 | |
| | 99 | with get_transaction() as conn: |
| | 100 | with conn.cursor() as cur: |
| | 101 | cur.execute("SELECT COUNT(*) FROM users WHERE email = %s", (email,)) |
| | 102 | |
| | 103 | if cur.fetchone()[0] > 0: |
| | 104 | raise ValueError(f"A user with email '{email}' already exists.") |
| | 105 | |
| | 106 | cur.execute(""" |
| | 107 | INSERT INTO users (first_name, last_name, email, password, type_id) |
| | 108 | VALUES (%s, %s, %s, %s, %s) |
| | 109 | RETURNING user_id, first_name, last_name, email |
| | 110 | """, (first_name, last_name, email, hashed, type_id)) |
| | 111 | |
| | 112 | result = cur.fetchone() |
| | 113 | # Transaction committed automatically |
| | 114 | }}} |
| | 115 | |
| | 116 | == Database Connection Pooling == |
| | 117 | |
| | 118 | Connection pooling reuses open database connections instead of creating a new one for every query. Opening a database connection involves TCP handshake, authentication, and session setup, which adds overhead. With pooling, these costs are paid once, and subsequent operations borrow pre-established connections from the pool. |
| | 119 | |
| | 120 | === Implementation === |
| | 121 | |
| | 122 | The prototype uses {{{psycopg2.pool.SimpleConnectionPool}}} to maintain a pool of reusable connections: |
| | 123 | |
| | 124 | {{{ |
| | 125 | # db.py |
| | 126 | |
| | 127 | from psycopg2 import pool |
| | 128 | |
| | 129 | _pool = None |
| | 130 | |
| | 131 | def _get_pool(): |
| | 132 | global _pool |
| | 133 | if _pool is None: |
| | 134 | _pool = pool.SimpleConnectionPool(minconn=2, maxconn=10, **DB_CONFIG) |
| | 135 | return _pool |
| | 136 | }}} |
| | 137 | |
| | 138 | '''Pool configuration:''' |
| | 139 | |
| | 140 | ||'''Parameter'''||'''Value'''||'''Meaning'''|| |
| | 141 | ||minconn||2||Minimum connections kept open and ready|| |
| | 142 | ||maxconn||10||Maximum concurrent connections allowed|| |
| | 143 | |
| | 144 | The pool is initialized lazily on first use and cleaned up when the application exits via {{{close_pool()}}}. |
| | 145 | |
| | 146 | === Connection Usage === |
| | 147 | |
| | 148 | Two context managers provide connections from the pool: |
| | 149 | |
| | 150 | '''Read operations''' use {{{get_connection()}}} with autocommit enabled: |
| | 151 | {{{ |
| | 152 | @contextmanager |
| | 153 | def get_connection(): |
| | 154 | p = _get_pool() |
| | 155 | conn = p.getconn() |
| | 156 | try: |
| | 157 | conn.autocommit = True |
| | 158 | yield conn |
| | 159 | finally: |
| | 160 | conn.autocommit = False |
| | 161 | p.putconn(conn) |
| | 162 | }}} |
| | 163 | |
| | 164 | '''Write operations''' use {{{get_transaction()}}} with explicit commit/rollback: |
| | 165 | {{{ |
| | 166 | @contextmanager |
| | 167 | def get_transaction(): |
| | 168 | p = _get_pool() |
| | 169 | conn = p.getconn() |
| | 170 | try: |
| | 171 | conn.autocommit = False |
| | 172 | yield conn |
| | 173 | conn.commit() |
| | 174 | except Exception: |
| | 175 | conn.rollback() |
| | 176 | raise |
| | 177 | finally: |
| | 178 | p.putconn(conn) |
| | 179 | }}} |
| | 180 | |
| | 181 | In both cases, the connection is automatically returned to the pool when the {{{with}}} block exits, ensuring no connection leaks. |
| | 182 | |
| | 183 | === Example: Read Operation Using Pool === |
| | 184 | |
| | 185 | {{{ |
| | 186 | # uc_browse.py — fetching resources |
| | 187 | |
| | 188 | with get_connection() as conn: |
| | 189 | with conn.cursor() as cur: |
| | 190 | cur.execute(""" |
| | 191 | SELECT r.resource_id, r.name, rt.type_name, l.building, l.room |
| | 192 | FROM resources r |
| | 193 | JOIN resource_types rt ON r.type_id = rt.type_id |
| | 194 | LEFT JOIN locations l ON r.location_id = l.location_id |
| | 195 | ORDER BY rt.type_name, r.name |
| | 196 | """) |
| | 197 | resources = cur.fetchall() |
| | 198 | # Connection returned to pool automatically |
| | 199 | }}} |
| | 200 | |
| | 201 | === Pool Lifecycle === |
| | 202 | |
| | 203 | {{{ |
| | 204 | # main.py |
| | 205 | |
| | 206 | def main(): |
| | 207 | try: |
| | 208 | while True: |
| | 209 | user = login() # uses pooled connections |
| | 210 | if user is None: |
| | 211 | break |
| | 212 | main_menu(...) # all operations use pooled connections |
| | 213 | except KeyboardInterrupt: |
| | 214 | pass |
| | 215 | finally: |
| | 216 | close_pool() # close all connections on exit |
| | 217 | }}} |