MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

SQLite连接与语句管理实践

2022-07-087.0k 阅读

SQLite 连接管理

SQLite 连接概述

SQLite 是一款轻型的嵌入式数据库,在很多应用场景中,它不需要独立的服务器进程,直接读写磁盘文件。在使用 SQLite 时,首先要建立与数据库的连接,这个连接负责管理与数据库文件的交互,包括事务控制、缓存管理等重要功能。

在不同的编程语言中,连接 SQLite 数据库的方式各有不同,但基本概念是一致的。例如在 Python 中,通过 sqlite3 模块来建立连接;在 Java 中,使用 JDBC 驱动程序来连接 SQLite 数据库。

Python 中 SQLite 连接的建立与关闭

Python 的 sqlite3 模块提供了简单易用的接口来操作 SQLite 数据库。以下是建立和关闭 SQLite 连接的基本代码示例:

import sqlite3

# 建立连接
conn = sqlite3.connect('example.db')

# 关闭连接
conn.close()

在上述代码中,sqlite3.connect('example.db') 方法尝试连接到名为 example.db 的 SQLite 数据库文件。如果该文件不存在,SQLite 会自动创建一个新的数据库文件。连接成功后,返回一个 Connection 对象,通过这个对象可以执行 SQL 语句、管理事务等操作。当操作完成后,调用 conn.close() 方法关闭连接,释放相关资源。

连接选项与参数

在建立 SQLite 连接时,还可以设置一些连接选项和参数,以满足不同的应用需求。例如,在 Python 中可以通过传递 isolation_level 参数来控制事务的隔离级别。

import sqlite3

# 设置隔离级别为 None,即自动提交模式
conn = sqlite3.connect('example.db', isolation_level=None)

# 普通连接模式
conn2 = sqlite3.connect('example.db')

在自动提交模式下(isolation_level=None),每执行一条 SQL 语句都会立即提交到数据库,而在普通连接模式下,需要手动调用 conn.commit() 方法来提交事务。

连接池的概念与应用

在一些高并发的应用场景中,频繁地建立和关闭数据库连接会带来性能开销。为了提高性能,可以使用连接池技术。连接池是一个预先创建好的数据库连接的集合,应用程序需要连接时,从连接池中获取一个连接,使用完毕后再将连接放回连接池,而不是直接关闭。

虽然 Python 的 sqlite3 模块本身没有内置连接池功能,但可以使用第三方库如 DBUtils 来实现连接池。以下是使用 DBUtils 库创建 SQLite 连接池的示例:

from dbutils.pooled_db import PooledDB
import sqlite3

# 创建连接池
pool = PooledDB(sqlite3, 5, database='example.db')

# 从连接池获取连接
conn = pool.connection()

# 使用连接执行操作
cursor = conn.cursor()
cursor.execute('SELECT * FROM your_table')
result = cursor.fetchall()

# 关闭游标和连接,连接会放回连接池
cursor.close()
conn.close()

在上述代码中,PooledDB 类创建了一个包含 5 个 SQLite 连接的连接池。应用程序通过 pool.connection() 方法从连接池中获取连接,使用完毕后调用 conn.close() 方法将连接放回连接池,而不是真正关闭连接。这样可以大大减少连接创建和销毁的开销,提高应用程序的性能。

SQLite 语句管理

SQL 语句分类与 SQLite 支持

SQL 语句主要分为数据定义语言(DDL)、数据操作语言(DML)和数据控制语言(DCL)。SQLite 全面支持 DDL 和 DML 语句,对于 DCL 语句的支持相对有限,因为 SQLite 是一个嵌入式数据库,通常用于单机或小型应用场景,安全控制方面不像大型数据库那样复杂。

数据定义语言(DDL)

  • 创建表(CREATE TABLE):使用 CREATE TABLE 语句可以在 SQLite 数据库中创建新的表。例如,创建一个名为 employees 的表,包含 idnamesalary 字段:
CREATE TABLE employees (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    salary REAL
);

在上述语句中,INTEGER 表示整数类型,TEXT 表示文本类型,REAL 表示浮点数类型。PRIMARY KEY AUTOINCREMENT 表示 id 字段是主键,并且会自动递增。NOT NULL 约束表示 name 字段不能为空。

  • 修改表(ALTER TABLE)ALTER TABLE 语句用于修改已存在表的结构。例如,向 employees 表中添加一个新的 department 字段:
ALTER TABLE employees ADD COLUMN department TEXT;
  • 删除表(DROP TABLE)DROP TABLE 语句用于删除整个表及其数据。例如,删除 employees 表:
DROP TABLE employees;

数据操作语言(DML)

  • 插入数据(INSERT INTO)INSERT INTO 语句用于向表中插入新的数据行。例如,向 employees 表中插入一条记录:
INSERT INTO employees (name, salary, department) VALUES ('John Doe', 5000.0, 'HR');

也可以一次插入多条记录:

INSERT INTO employees (name, salary, department) VALUES 
('Jane Smith', 6000.0, 'IT'),
('Bob Johnson', 5500.0, 'Finance');
  • 查询数据(SELECT)SELECT 语句是最常用的查询语句,用于从表中检索数据。例如,查询 employees 表中所有员工的信息:
SELECT * FROM employees;

查询特定字段并按 salary 降序排列:

SELECT name, salary FROM employees ORDER BY salary DESC;
  • 更新数据(UPDATE)UPDATE 语句用于修改表中已存在的数据。例如,将 employees 表中 id 为 1 的员工的 salary 增加 1000:
UPDATE employees SET salary = salary + 1000 WHERE id = 1;
  • 删除数据(DELETE FROM)DELETE FROM 语句用于从表中删除数据行。例如,删除 employees 表中 department'HR' 的所有员工记录:
DELETE FROM employees WHERE department = 'HR';

预处理语句(Prepared Statements)

预处理语句是一种在执行前先编译的 SQL 语句,它使用占位符来代替实际的值。使用预处理语句有两个主要优点:安全性和性能优化。

安全性

在不使用预处理语句的情况下,如果直接将用户输入拼接到 SQL 语句中,容易受到 SQL 注入攻击。例如,以下代码存在 SQL 注入风险:

import sqlite3

user_input = "'; DROP TABLE employees; --"
conn = sqlite3.connect('example.db')
cursor = conn.cursor()
query = "SELECT * FROM employees WHERE name = '" + user_input + "'"
cursor.execute(query)

如果恶意用户输入 '; DROP TABLE employees; --,上述代码将执行删除 employees 表的操作,因为拼接后的 SQL 语句变为 SELECT * FROM employees WHERE name = ''; DROP TABLE employees; --'

而使用预处理语句可以有效避免这种风险:

import sqlite3

user_input = "'; DROP TABLE employees; --"
conn = sqlite3.connect('example.db')
cursor = conn.cursor()
query = "SELECT * FROM employees WHERE name =?"
cursor.execute(query, (user_input,))

在上述代码中,? 是占位符,user_input 的值作为参数传递给 execute 方法,SQLite 会对输入进行正确的转义,从而防止 SQL 注入攻击。

性能优化

预处理语句在执行多次时性能更好。因为 SQLite 只需要编译一次预处理语句,然后每次执行时只需替换占位符的值即可,而不需要每次都重新编译整个 SQL 语句。例如,批量插入数据时使用预处理语句:

import sqlite3

data = [
    ('Alice', 4500.0, 'Marketing'),
    ('Eve', 4800.0, 'Marketing')
]

conn = sqlite3.connect('example.db')
cursor = conn.cursor()
query = "INSERT INTO employees (name, salary, department) VALUES (?,?,?)"
cursor.executemany(query, data)
conn.commit()

在上述代码中,executemany 方法用于批量执行预处理语句,提高了插入数据的效率。

事务管理

事务是一组 SQL 语句的逻辑单元,这些语句要么全部成功执行,要么全部回滚(撤销)。SQLite 支持事务,通过事务可以确保数据的一致性和完整性。

开启事务

在 SQLite 中,默认情况下,每个 SQL 语句都在一个单独的事务中执行。但可以通过 BEGINBEGIN TRANSACTION 语句显式开启一个事务。例如:

BEGIN;
-- 一系列 SQL 语句
INSERT INTO employees (name, salary, department) VALUES ('Tom', 5200.0, 'Engineering');
UPDATE employees SET salary = salary + 200 WHERE department = 'Engineering';
COMMIT;

在上述代码中,BEGIN 语句开启了一个事务,随后的 INSERTUPDATE 语句都在这个事务中执行。最后,COMMIT 语句提交事务,将所有操作永久保存到数据库中。

回滚事务

如果在事务执行过程中发生错误,可以使用 ROLLBACK 语句回滚事务,撤销所有未提交的操作。例如:

BEGIN;
-- 一系列 SQL 语句
INSERT INTO employees (name, salary, department) VALUES ('Jerry', 5300.0, 'Engineering');
-- 假设这里发生错误,比如违反了某个约束
ROLLBACK;

在上述代码中,如果 INSERT 语句之后发生错误,执行 ROLLBACK 语句将撤销 INSERT 操作,数据库状态恢复到事务开始前的状态。

事务隔离级别

SQLite 支持不同的事务隔离级别,虽然其默认隔离级别是 DEFERRED,但也可以设置为 IMMEDIATEEXCLUSIVE

  • DEFERRED:这是默认的隔离级别。在这种级别下,直到第一个修改数据的语句(如 INSERTUPDATEDELETE)执行时,才会获取排它锁。
  • IMMEDIATE:在事务开始时就获取排它锁,这样可以防止其他事务同时修改数据库,但允许其他事务读取数据。
  • EXCLUSIVE:在事务开始时就获取排它锁,并且阻止其他事务读取或修改数据库。

在 Python 中,可以通过设置 isolation_level 参数来改变事务隔离级别:

import sqlite3

# 设置为 IMMEDIATE 隔离级别
conn = sqlite3.connect('example.db', isolation_level='IMMEDIATE')

SQLite 连接与语句管理的高级应用

多线程环境下的 SQLite 连接

在多线程应用程序中使用 SQLite 连接需要特别注意。SQLite 本身是线程安全的,但连接对象不是线程安全的。这意味着不能在多个线程中共享同一个连接对象。

一种常见的解决方案是为每个线程创建独立的连接对象。例如,在 Python 中使用 threading 模块:

import sqlite3
import threading

def worker():
    conn = sqlite3.connect('example.db')
    cursor = conn.cursor()
    cursor.execute('INSERT INTO employees (name, salary, department) VALUES (?,?,?)', ('Thread Worker', 4000.0, 'Threading'))
    conn.commit()
    conn.close()

threads = []
for _ in range(5):
    t = threading.Thread(target=worker)
    threads.append(t)
    t.start()

for t in threads:
    t.join()

在上述代码中,每个线程都创建了自己的 SQLite 连接,并独立执行插入操作。这样可以避免多线程共享连接带来的问题。

与其他技术集成时的连接与语句管理

SQLite 经常与其他技术如 Web 框架、移动应用开发框架等集成。在这些场景下,连接与语句管理需要与框架的特性相结合。

与 Flask Web 框架集成

Flask 是一个流行的 Python Web 框架。在 Flask 应用中使用 SQLite 时,可以通过扩展如 Flask - SQLite3 来简化连接管理。以下是一个简单的示例:

from flask import Flask
from flask_sqlite3 import SQLite3

app = Flask(__name__)
db = SQLite3(app)

@app.route('/')
def index():
    cur = db.execute('SELECT * FROM employees')
    rows = cur.fetchall()
    return str(rows)

在上述代码中,Flask - SQLite3 扩展自动管理 SQLite 连接,并提供了方便的 execute 方法来执行 SQL 语句。

与 Android 开发集成

在 Android 开发中,SQLite 是默认的本地数据库。通过 Android 的 SQLiteOpenHelper 类可以方便地管理数据库连接、创建表和执行 SQL 语句。以下是一个简单的示例:

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class MyDatabaseHelper extends SQLiteOpenHelper {
    private static final String DATABASE_NAME = "my_database.db";
    private static final int DATABASE_VERSION = 1;

    public MyDatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        String createTable = "CREATE TABLE employees (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, salary REAL)";
        db.execSQL(createTable);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // 处理数据库升级逻辑
    }
}

在上述代码中,MyDatabaseHelper 类继承自 SQLiteOpenHelper,通过重写 onCreate 方法可以创建数据库表。在 Android 应用中,可以通过这个类来获取数据库连接并执行 SQL 语句。

性能优化技巧

在使用 SQLite 进行大规模数据处理或高并发操作时,需要采取一些性能优化技巧。

批量操作

如前文所述,使用 executemany 方法进行批量插入、更新等操作可以显著提高性能。例如,批量插入数据:

import sqlite3

data = [(f'Employee_{i}', i * 1000.0, 'Batch') for i in range(1000)]

conn = sqlite3.connect('example.db')
cursor = conn.cursor()
query = "INSERT INTO employees (name, salary, department) VALUES (?,?,?)"
cursor.executemany(query, data)
conn.commit()

通过批量操作,减少了 SQL 语句的编译次数和数据库交互次数,从而提高了性能。

索引优化

合理创建索引可以大大加快查询速度。例如,如果经常根据 name 字段查询 employees 表,可以为 name 字段创建索引:

CREATE INDEX idx_name ON employees (name);

索引可以加快数据的查找速度,但也会增加插入、更新和删除操作的开销,因为数据库需要同时维护索引结构。所以在创建索引时需要权衡利弊。

数据库文件管理

定期对 SQLite 数据库文件进行 VACUUM 操作可以回收未使用的空间,优化数据库性能。在 Python 中,可以通过以下方式执行 VACUUM:

import sqlite3

conn = sqlite3.connect('example.db')
conn.execute('VACUUM')
conn.close()

VACUUM 操作会重建数据库文件,将所有数据重新写入一个新文件,并删除旧文件中未使用的空间。

错误处理与调试

在使用 SQLite 连接和执行 SQL 语句时,可能会遇到各种错误。正确处理这些错误并进行调试是确保应用程序稳定运行的关键。

错误类型与处理

SQLite 操作可能会引发多种类型的错误,如 sqlite3.OperationalError(操作错误,如数据库文件不存在、表不存在等)、sqlite3.IntegrityError(完整性错误,如违反唯一约束、外键约束等)。在 Python 中,可以通过 try - except 块来捕获和处理这些错误:

import sqlite3

try:
    conn = sqlite3.connect('example.db')
    cursor = conn.cursor()
    cursor.execute('INSERT INTO employees (name, salary, department) VALUES (?,?,?)', ('Invalid Data', -1, 'Error'))
    conn.commit()
except sqlite3.IntegrityError as e:
    print(f"Integrity error: {e}")
    conn.rollback()
finally:
    conn.close()

在上述代码中,try - except 块捕获 IntegrityError 错误,打印错误信息并回滚事务,最后关闭连接。

调试技巧

当遇到 SQLite 操作问题时,可以使用一些调试技巧。例如,在 Python 中可以启用 sqlite3 模块的日志记录功能:

import sqlite3
import logging

logging.basicConfig(level = logging.DEBUG)
conn = sqlite3.connect('example.db')
cursor = conn.cursor()
cursor.execute('SELECT * FROM employees')

启用日志记录后,sqlite3 模块会输出详细的调试信息,包括 SQL 语句的执行情况、错误信息等,有助于定位问题。

另外,可以在 SQLite 命令行工具中执行 SQL 语句,观察执行结果和错误提示。例如,通过 sqlite3 example.db 进入 SQLite 命令行,然后执行 SQL 语句进行调试。

在实际应用中,还可以使用数据库可视化工具,如 SQLiteStudio、DB Browser for SQLite 等,直观地查看数据库结构、数据内容以及执行 SQL 语句的结果,辅助调试和开发。通过合理运用这些错误处理和调试技巧,可以快速解决 SQLite 连接与语句管理过程中遇到的问题,提高开发效率和应用程序的稳定性。