Методы execute, executemany, executescript, commit, rollback

На предыдущих занятиях мы с вами рассмотрели основы языка SQL для взаимодействия с СУБД SQLite. Теперь поговорим о методах пакета sqlite3, то есть, об API данной СУБД. Некоторые моменты мы уже затрагивали и отмечали, что для базовой работы с БД можно использовать менеджер контекста:

import sqlite3 as sq
 
with sq.connect("cars.db") as con:
    cur = con.cursor()
 
    cur.execute("""CREATE TABLE IF NOT EXISTS cars (
        car_id INTEGER PRIMARY KEY AUTOINCREMENT,
        model TEXT,
        price INTEGER
    )""")

Когда контекстный менеджер завершает свою работу, он автоматически выполняет два метода:

  • con.commit() – применение всех изменений в таблицах БД;
  • con.close() – закрытие соединения с БД.

Это необходимые действия для сохранения внесенных изменений в БД.

Методы execute, executemany и executescript

Давайте добавим в таблицу cars несколько записей. В самом простом случае это можно сделать так:

cur.execute("INSERT INTO cars VALUES(1,'Audi',52642)")
cur.execute("INSERT INTO cars VALUES(2,'Mercedes',57127)")
cur.execute("INSERT INTO cars VALUES(3,'Skoda',9000)")
cur.execute("INSERT INTO cars VALUES(4,'Volvo',29000)")
cur.execute("INSERT INTO cars VALUES(5,'Bentley',350000)")

В результате, таблица будет содержать данные:

Однако, когда мы программируем на Python, то данные, как правило, хранятся в каких-либо коллекциях, например, так:

cars = [
    ('Audi', 52642),
    ('Mercedes', 57127),
    ('Skoda', 9000),
    ('Volvo', 29000),
    ('Bentley', 350000)
]

И мы бы хотели брать значения из этого списка и передавать их в SQL-запрос. Для этого запрос следует записывать в виде следующего шаблона:

cur.execute("INSERT INTO cars VALUES(NULL, ?, ?)", cars[0])

Здесь вместо знаков вопроса будут подставлены соответствующие данные из первого кортежа списка. Соответственно, весь набор ранее приведенных строчек, можно заменить циклом:

for car in cars:
    cur.execute("INSERT INTO cars VALUES(NULL, ?, ?)", car)

Или, еще проще, воспользоваться методом executemany, который специально для этого и существует:

cur.executemany("INSERT INTO cars VALUES(NULL, ?, ?)", cars)

Сразу же здесь отмечу, что помимо знаков вопроса можно использовать именованные параметры (плейсхолдеры). Для этого в запросе перед ними ставится двоеточие, а затем, указывается словарь, где имя – это ключ, вместо которого будет подставлено его значение:

cur.execute("UPDATE cars SET price = :Price WHERE model LIKE 'A%'", {'Price': 0})

Далее, если нужно выполнить несколько отдельных SQL-команд, то можно передать их СУБД с помощью метода executescript:

cur.executescript("""DELETE FROM cars WHERE model LIKE 'A%';
    UPDATE cars SET price = price+1000
""")

Мы здесь сначала удаляем все записи, у которых модель начинается на букву A, а затем у оставшихся записей увеличиваем цену на 1000. Причем, команды должны отделяться друг от друга точкой с запятой.

У этого метода есть одно ограничение: здесь нельзя использовать шаблоны запросов, как мы это делали в предыдущих методах. В executescript буквально записываются SQL-запросы как есть со всеми данными.

Методы commit и rollback

Давайте теперь реализуем соединение с БД через блок обработки исключений try/except/finally:

con = None
try:
    con = sq.connect("cars.db")
    cur = con.cursor()
 
    cur.executescript("""CREATE TABLE IF NOT EXISTS cars (
            car_id INTEGER PRIMARY KEY AUTOINCREMENT,
            model TEXT,
            price INTEGER
        );
        BEGIN;
        INSERT INTO cars VALUES(NULL,'Audi',52642);
        INSERT INTO cars VALUES(NULL,'Mercedes',57127);
        INSERT INTO cars VALUES(NULL,'Skoda',9000);
        INSERT INTO cars VALUES(NULL,'Volvo',29000);
        INSERT INTO cars VALUES(NULL,'Bentley',350000);
        UPDATE cars SET price = price+1000
    """)
    con.commit()
 
except sq.Error as e:
    if con: con.rollback()
    print("Ошибка выполнения запроса")
finally:
    if con: con.close()

В чем преимущество такого подхода? Смотрите, мы здесь сами «вручную» вызываем методы commit и close. Если операции с таблицами прошли успешно, то они будут сохранены, если же возникли какие-либо ошибки (исключения), то будет вызван метод rollback, который откатывает состояние БД в состояние отметки BEGIN, то есть, все внесенные изменения применены не будут.

Например, укажем в команде UDPATE неверное имя таблицы:

UPDATE cars2 SET price = price+1000

При запуске программы произойдет ошибка выполнения запроса и состояние БД не изменится, то есть, все новые добавленные записи не появятся в таблице cars. А вот если вместо rollback указать commit, то увидим добавление записей. То есть, при использовании менеджера контекста в данном случае не выполнилась бы только последняя команда, но отката состояния БД не произошло бы. Вот так более тонко можно управлять состоянием таблиц в БД.

Если при работе с БД предполагается сохранять вносимые изменения сразу после выполнения SQL-запроса, то это можно сделать с помощью метода connect, установив в нем параметр isolation_level=None:

with sq.connect("cars.db", isolation_level=None) as con:
    cur = con.cursor()
 
    cur.executescript("""INSERT INTO cars VALUES(NULL,'Audi',52642);
        INSERT INTO cars VALUES(NULL,'Mercedes',57127);
        INSERT INTO cars VALUES(NULL,'Skoda',9000);
        INSERT INTO cars VALUES(NULL,'Volvo',29000);
        INSERT INTO cars VALUES(NULL,'Bentley',350000);
    """)

Однако так делать без особой надобности не стоит, т.к. это уменьшает скорость работы с БД из-за постоянной записи данных непосредственно в файл. Без изменения этого параметра все изменения сохраняются в памяти, а потому работа происходит куда быстрее.

Свойство lastrowid

Давайте теперь представим, что у нас есть еще одна таблица cust, которая содержит покупателей машин. Причем, если происходит покупка по «trade-in», то прежняя машина владельца добавляется в конец таблицы cars, а в таблице cust появляется запись с именем покупателя, идентификатором машины сданной в «trade-in» и id новой купленной машины:

Чтобы реализовать SQL-запрос добавления записи в таблицу cust, нам нужно знать car_id автомобиля сданного в «trade-in». Предположим, что Федор еще не совершил покупку и таблица cars не содержит запись с его сданным автомобилем. Добавим ее. Выполним следующий запрос вот в такой программе:

with sq.connect("cars.db") as con:
    cur = con.cursor()
 
    cur.executescript("""CREATE TABLE IF NOT EXISTS cars (
            car_id INTEGER PRIMARY KEY AUTOINCREMENT,
            model TEXT,
            price INTEGER
        );
        CREATE TABLE IF NOT EXISTS cust(name TEXT, tr_in INTEGER, buy INTEGER);        
    """)
 
    cur.execute("INSERT INTO cars VALUES(NULL,'Запорожец', 1000)")

Мы здесь создаем еще одну таблицу cust с тремя полями и, затем, добавляем в таблицу cars автомобиль «Запорожец», который сдает покупатель Федор. Как теперь нам узнать car_id этой записи? Для этого можно воспользоваться специальным свойством:

last_row_id = cur.lastrowid

которое содержит значение rowid последней добавленной записи. В нашем случае поля car_id и rowid будут совпадать, поэтому воспользуемся этим значением и сформируем еще один запрос на добавление записи во вторую таблицу:

buy_car_id = 2
 
cur.execute("INSERT INTO cust VALUES('Федор', ?, ?)", (last_row_id, buy_car_id))

Теперь, при выполнении нашей программы в таблице cust увидим искомую запись. Вот так используется свойство lastrowid.

На этом завершим это занятие. На следующем продолжим рассматривать функционал API для работы с СУБД SQLite.