برنامه‌نویسی پایتون فهرست مطالب
فصل ۳۱

sqlite3 در پایتون

در فصل قبل زبان پایگاه داده را یاد گرفتیم. حالا همان دستورها را از داخل پایتون اجرا می‌کنیم. ماژول sqlite3 جزو کتابخانه استاندارد است و نصب نمی‌خواهد.

در این فصل می‌خوانید
  1. اتصال و مکان‌نما
  2. ساختن ساختار جدول‌ها
  3. درج داده با پارامتر
  4. خواندن داده
  5. تغییر و حذف
  6. پیوند از داخل پایتون
  7. پیدا کردن یک رکورد
  8. تراکنش
  9. پشتیبان‌گیری
  10. سطر به شکل دیکشنری
  11. تزریق SQL در عمل
  12. برنامه کامل

۱. اتصال و مکان‌نما

'''Opening a connection to a SQLite database.

sqlite3 is part of the standard library - there is nothing to install.
connect() CREATES the file when it does not exist yet.
'''

import sqlite3


def main() -> None:
    # The file is created next to the program, in the current folder.
    conn = sqlite3.connect('database.db')

    # SQLite ignores the foreign keys unless we switch them on,
    # and it forgets the setting at every new connection.
    conn.execute('PRAGMA foreign_keys = ON')

    # The cursor is the object that runs the queries and holds the results.
    cursor = conn.cursor()

    # A first query to prove that everything works.
    cursor.execute('SELECT sqlite_version()')
    print('SQLite version:', cursor.fetchone()[0])

    # Always close what you opened.
    conn.close()

    # ':memory:' creates a database that only lives in the RAM - perfect
    # for trying things out.
    temp = sqlite3.connect(':memory:')
    temp.close()

اتصال: نمونه 01_connect.py

مفهومیعنی
اتصالپل بین برنامه و فایل پایگاه داده
مکان‌نماشیئی که پرس‌وجو را اجرا می‌کند و نتیجه را نگه می‌دارد
commit()تثبیت تغییرها روی دیسک
close()بستن اتصال
':memory:'پایگاه داده موقتی در حافظه
PRAGMA در هر اتصال

SQLite تنظیم کلیدهای خارجی را در هر اتصال فراموش می‌کند. پس PRAGMA foreign_keys = ON باید بلافاصله بعد از هر connect نوشته شود. بهترین راه، ساختن یک تابع کمکی اتصال است که همیشه این کار را بکند؛ در بخش دوازدهم می‌بینیدش.

۲. ساختن ساختار جدول‌ها

SCHEMA = '''
    CREATE TABLE IF NOT EXISTS students (
        student_id INTEGER PRIMARY KEY AUTOINCREMENT,
        first_name TEXT NOT NULL,
        last_name  TEXT NOT NULL,
        email      TEXT UNIQUE NOT NULL
    );

    CREATE TABLE IF NOT EXISTS courses (
        course_id  INTEGER PRIMARY KEY AUTOINCREMENT,
        title      TEXT NOT NULL,
        teacher_id INTEGER,
        credit     INTEGER DEFAULT 3,
        FOREIGN KEY (teacher_id) REFERENCES teachers(teacher_id)
            ON DELETE SET NULL ON UPDATE CASCADE
    );

    CREATE TABLE IF NOT EXISTS enrollments (
        enrollment_id   INTEGER PRIMARY KEY AUTOINCREMENT,
        student_id      INTEGER NOT NULL,
        course_id       INTEGER NOT NULL,
        grade           REAL,
        enrollment_date TEXT DEFAULT (date('now')),
        FOREIGN KEY (student_id) REFERENCES students(student_id)
            ON DELETE CASCADE ON UPDATE CASCADE,
        UNIQUE (student_id, course_id)
    );
'''


def main() -> None:
    conn = sqlite3.connect('database.db')
    conn.execute('PRAGMA foreign_keys = ON')
    cursor = conn.cursor()

    cursor.executescript(SCHEMA)

    # Nothing is really written until commit() is called.
    conn.commit()
    conn.close()

ساختار: نمونه 02_create_tables.py

executescript چند دستور جداشده با نقطه‌ویرگول را با هم اجرا می‌کند و برای ساختن ساختار ایده‌آل است. برای پرس‌وجوهای عادی execute را به کار ببرید. عبارت IF NOT EXISTS باعث می‌شود اجرای دوباره برنامه خطا ندهد.

۳. درج داده با پارامتر

'''NEVER build the SQL with an f-string or with '+': always use '?' parameters.
The values are then sent apart from the query and can never become SQL code.
'''

# executemany() runs the same query once per tuple of the list.
students_data = [
    ('Ali', 'Rezaei', 'alireza@kadoosedu.ir'),
    ('Maryam', 'Ahmadi', 'maryam@kadoosedu.ir'),
    ('Sara', 'Karimi', 'sara@kadoosedu.ir'),
]
cursor.executemany(
    'INSERT OR IGNORE INTO students (first_name, last_name, email) '
    'VALUES (?, ?, ?)',
    students_data,
)
conn.commit()

# We must not guess the ids: we ask the database for them.
# NOTE: the comma in ('Mohammadi',) is what makes it a TUPLE.
cursor.execute('SELECT teacher_id FROM teachers WHERE last_name = ?',
               ('Mohammadi',))
teacher1 = cursor.fetchone()[0]

# A single insert, and the id it produced.
cursor.execute(
    'INSERT OR IGNORE INTO students (first_name, last_name, email) '
    'VALUES (?, ?, ?)',
    ('Omid', 'Shabani', 'omid@kadoosedu.ir'),
)
print('The new student got the id', cursor.lastrowid)
conn.commit()

درج: نمونه 03_insert_data.py

شرح مثال

۴. خواندن داده

'''
    fetchone()   -> one row, or None
    fetchall()   -> every remaining row, in a list
    fetchmany(n) -> n rows
    looping over the cursor -> one row at a time, without loading everything
'''

def show_table(table_name: str) -> None:
    '''Print the content of one table.

    The table NAME cannot be a '?' parameter (parameters are only for values),
    so this function must only ever be called with a name WE choose, never with
    something typed by a user.
    '''
    conn = sqlite3.connect('database.db')
    cursor = conn.cursor()

    cursor.execute(f'SELECT * FROM {table_name}')

    # cursor.description holds the column names.
    columns = [column[0] for column in cursor.description]
    print(' | '.join(columns))

    for row in cursor:               # looping is the memory-friendly way
        print(row)

    conn.close()

خواندن: نمونه 04_select_all.py

پارامتر فقط برای مقدار است

علامت سؤال فقط جای مقدار می‌نشیند، نه جای نام جدول یا نام ستون. برای آنها ناچارید از f-string استفاده کنید، و دقیقاً به همین دلیل نام جدول هرگز نباید از کاربر گرفته شود. اگر لازم شد، فقط از یک فهرست ثابت و مجاز انتخابش کنید.

۵. تغییر و حذف

'''cursor.rowcount tells us how many rows were really affected - use it to know
whether the WHERE matched anything.
'''

cursor.execute(
    'UPDATE students SET email = ? WHERE email = ?',
    ('new_alireza@kadoosedu.ir', 'alireza@kadoosedu.ir'),
)
print('rows updated:', cursor.rowcount)
conn.commit()

cursor.execute(
    'DELETE FROM enrollments WHERE grade < ?',
    (16,),
)
print('rows deleted:', cursor.rowcount)
conn.commit()

تغییر و حذف: نمونه 05_update_delete.py

صفت rowcount می‌گوید چند سطر واقعاً تحت تأثیر قرار گرفته‌اند. اگر صفر بود یعنی شرط شما با هیچ سطری جور در نیامده است، و این معمولاً چیزی است که باید به کاربر بگویید.

۶. پیوند از داخل پایتون

QUERY = '''
    SELECT s.first_name || ' ' || s.last_name AS student,
           c.title                            AS course,
           e.grade                            AS grade
    FROM enrollments e
    JOIN students s ON e.student_id = s.student_id
    JOIN courses  c ON e.course_id  = c.course_id
    ORDER BY student, course
'''

پرس‌وجوی پیوندی: نمونه 06_join_query.py

نکته مهم این است که هیچ چیز تازه‌ای نیست. SQL دقیقاً همان SQL فصل قبل است؛ فقط شکل اجرایش عوض شده. رشته سه‌گیومه‌ای، یک پرس‌وجوی بلند را خوانا نگه می‌دارد.

۷. پیدا کردن یک رکورد

'''fetchone() returns ONE row, or None when nothing matched.

Forgetting to test for None is the most common bug of this session:
"TypeError: 'NoneType' object is not subscriptable".
'''

def find_student(email: str) -> tuple | None:
    'Return the row of a student, or None.'
    conn = sqlite3.connect('database.db')
    cursor = conn.cursor()

    cursor.execute('SELECT * FROM students WHERE email = ?', (email,))
    student = cursor.fetchone()

    conn.close()
    return student


def main() -> None:
    for email in ('zahra@kadoosedu.ir', 'nobody@kadoosedu.ir'):
        student = find_student(email)

        if student:
            print('Student found:', student)
        else:
            print(f'No student with the email {email}.')

جست‌وجوی یک رکورد: نمونه 08_find_one.py

این پرتکرارترین باگ کل فصل است: fetchone() اگر چیزی پیدا نکند None می‌دهد، و بعد student[0] برنامه را با TypeError می‌کشد. همیشه بررسی کنید.

۸. تراکنش

تراکنش مجموعه‌ای از عملیات است که باید به صورت یک واحد انجام شوند؛ یا همه عملیات با موفقیت انجام می‌شوند یا هیچ‌کدام نباید اثر دائمی داشته باشند. مثال کلاسیک آن انتقال وجه است: اگر مبلغ از یک حساب کم شود اما به حساب مقصد اضافه نشود، یکپارچگی داده‌ها از بین می‌رود.

try:
    cursor.execute(
        'INSERT INTO students (first_name, last_name, email) '
        'VALUES (?, ?, ?)',
        ('Sara', 'Hosseini', 'sara2@kadoosedu.ir'),
    )
    new_student_id = cursor.lastrowid

    cursor.execute('SELECT course_id FROM courses WHERE title = ?',
                   ('Python Programming',))
    row = cursor.fetchone()
    if row is None:
        raise sqlite3.Error('the course does not exist')

    cursor.execute(
        'INSERT INTO enrollments (student_id, course_id, grade) '
        'VALUES (?, ?, ?)',
        (new_student_id, row[0], 20),
    )

    # commit() makes everything permanent, in one single step.
    conn.commit()
    print('Transaction done.')

except sqlite3.Error as e:
    # rollback() undoes everything since the last commit.
    conn.rollback()
    print('Rollback happened:', e)

finally:
    conn.close()

تراکنش: نمونه 09_transaction.py

شرح مثال

۹. پشتیبان‌گیری

'''conn.backup() copies a database safely, even while it is being used -
much better than copying the file with the operating system.
'''

def main() -> None:
    source = sqlite3.connect('database.db')
    backup = sqlite3.connect('backup.db')

    try:
        source.backup(backup)
        print('backup.db was written.')
    finally:
        # The connections are closed even if the backup failed.
        backup.close()
        source.close()

پشتیبان‌گیری: نمونه 10_backup.py

کپی کردن فایل پایگاه داده با دستور سیستم‌عامل خطرناک است: اگر همان لحظه برنامه‌ای در حال نوشتن باشد، کپی ناقص و خراب می‌شود. متد backup این را درست انجام می‌دهد.

۱۰. سطر به شکل دیکشنری

'''By default a row is a TUPLE, so we read it with row[0], row[1]...
That is short but unreadable, and it breaks when a column is added.

sqlite3.Row lets us read a row by COLUMN NAME.
'''

# Default: tuples.
cursor.execute('SELECT student_id, first_name, email FROM students')
for row in cursor.fetchall()[:3]:
    print(row, '->', row[1])

# With sqlite3.Row: access by name (and still by index).
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute('SELECT student_id, first_name, email FROM students')
for row in cursor.fetchall()[:3]:
    print(row['first_name'], row['email'], '| id =', row['student_id'])

# A row can be turned into a real dictionary, ready for JSON.
cursor.execute('SELECT * FROM students LIMIT 2')
students = [dict(row) for row in cursor.fetchall()]
print(students)

row_factory: نمونه 12_row_factory.py

همیشه row_factory را روشن کنید

خواندن row[3] شش ماه بعد هیچ معنایی ندارد، و اگر کسی ستونی به جدول اضافه کند، کل کدتان بی‌سروصدا غلط می‌شود. row['email'] همیشه درست است. به‌علاوه با dict(row) می‌توانید بلافاصله به JSON تبدیلش کنید.

۱۱. تزریق SQL در عمل

def unsafe_search(cursor: sqlite3.Cursor, name: str) -> list:
    'NEVER DO THIS: the value is glued into the SQL text.'
    query = f"SELECT * FROM students WHERE first_name = '{name}'"
    print('  SQL sent:', query)
    cursor.execute(query)
    return cursor.fetchall()


def safe_search(cursor: sqlite3.Cursor, name: str) -> list:
    'The right way: the value travels apart from the query.'
    cursor.execute('SELECT * FROM students WHERE first_name = ?', (name,))
    return cursor.fetchall()

حمله و دفاع: نمونه 13_sql_injection.py

خودتان امتحان کنید

این برنامه را اجرا کنید و در پاسخ سؤال، عبارت ' OR '1'='1 را تایپ کنید. نسخه ناامن همه دانشجویان را برمی‌گرداند و نسخه امن هیچ‌کدام را. یک بار دیدن این تفاوت، بهتر از ده صفحه توضیح است.

cursor.execute('... WHERE name = ?' % name)          # wrong
cursor.execute(f'... WHERE name = {name}')           # wrong
cursor.execute('... WHERE name = ?', (name,))        # right

سه شکل، فقط یکی درست

۱۲. برنامه کامل

'''A complete little application on top of sqlite3.'''

import sqlite3

DB_NAME = 'students.db'

SCHEMA = '''
    CREATE TABLE IF NOT EXISTS students (
        student_id INTEGER PRIMARY KEY AUTOINCREMENT,
        first_name TEXT NOT NULL,
        last_name  TEXT NOT NULL,
        email      TEXT UNIQUE NOT NULL
    );
'''


def get_connection() -> sqlite3.Connection:
    '''Return a ready-to-use connection.

    Every function of the program goes through here, so the PRAGMA and the
    row_factory are never forgotten.
    '''
    conn = sqlite3.connect(DB_NAME)
    conn.execute('PRAGMA foreign_keys = ON')
    conn.row_factory = sqlite3.Row
    return conn


def add_student(first_name: str, last_name: str, email: str) -> bool:
    'Insert one student. Return False when the email already exists.'
    try:
        with get_connection() as conn:
            conn.execute(
                'INSERT INTO students (first_name, last_name, email) '
                'VALUES (?, ?, ?)',
                (first_name, last_name, email),
            )
        return True
    except sqlite3.IntegrityError:
        # Raised by the UNIQUE constraint on the email.
        return False


def list_students() -> list[sqlite3.Row]:
    'Return every student, sorted by last name.'
    conn = get_connection()
    try:
        return conn.execute(
            'SELECT * FROM students ORDER BY last_name'
        ).fetchall()
    finally:
        conn.close()

برنامه کامل: نمونه 14_students_app.py

الگوهایی که باید از این برنامه یاد بگیرید

فایل‌های تمرینی این فصل

پوشه S31-sqlite3-in-python شامل نمونه‌های 00_docstrings.py تا 14_students_app.py. آنها را به ترتیب اجرا کنید، چون هر کدام روی داده‌ای که قبلی ساخته کار می‌کند.

خلاصه فصل

ابزاروظیفه آن
sqlite3.connect(path)اتصال. فایل نبود، می‌سازدش
PRAGMA foreign_keys = ONدر هر اتصال لازم است
conn.cursor()ساختن مکان‌نما
executescriptاجرای چند دستور، برای ساختار
execute(sql, params)اجرای یک پرس‌وجو با پارامتر
executemanyاجرای یک پرس‌وجو برای چند تاپل
fetchone fetchall fetchmanyگرفتن نتیجه
lastrowidشناسه سطری که در همین مرحله درج شد
rowcountچند سطر تحت تأثیر قرار گرفت
commit و rollbackتثبیت یا برگرداندن تراکنش
row_factory = sqlite3.Rowدسترسی به ستون با نام
conn.backup(other)پشتیبان امن
IntegrityErrorنقض محدودیت. باید مدیریت شود
پارامتر ?تنها راه امن. برای مقدار، نه برای نام جدول

تمرین‌های فصل سی و یک

  1. پایگاه داده library.db با جدول books بسازید: شناسه، عنوان، نویسنده، سال و موجود بودن.
  2. توابع add_book() و list_books() و delete_book() را بنویسید.
  3. تابع search_books(text) را بنویسید که با LIKE و پارامتر ? در عنوان و نویسنده جست‌وجو کند.
  4. جدول‌های members و borrowings را با کلیدهای خارجی درست اضافه کنید.
  5. پرس‌وجویی بنویسید که هر کتاب امانت‌داده‌شده را همراه نام امانت‌گیرنده نشان دهد.
  6. تراکنشی بنویسید که کتابی را امانت دهد: هم رکورد امانت را درج کند و هم کتاب را ناموجود علامت بزند، یا هیچ‌کدام.
  7. sqlite3.Row را روشن کنید و توابع نمایش را با نام ستون‌ها بازنویسی کنید.
  8. تابع backup() بنویسید و پیش از هر حذف صدایش بزنید.
  9. با کد خودتان نشان دهید چرا f'... WHERE title = {title}' خطرناک است.
فصل قبل: پایگاه داده و SQL فصل بعد: ORM و SQLAlchemy