Qt資料庫之資料庫常用操作
前面的章節介紹了怎麼使用 Qt 連線訪問資料庫 SQLite
和 MySQL,在這一節裡將介紹訪問資料庫的常用操作細節,主要是關於QSqlDatabase
,QSqlQuery
的運用,以及資料庫訪問安全相關的SQL
注入攻擊
。
小提示
1
. 現在比較推薦資料庫設計時每個表都有一個無意義的主鍵,如id
。2
. 儘量不使用外來鍵,資料的邏輯關係使用上面提到的無意義的 id 來關聯,這樣的好處是資料遷移的時候不需要考慮外來鍵的因素而造成很多麻煩。資料的邏輯關係由程式來控制。3
. 因為沒有用外來鍵而不能級連刪除,如果擔心資料庫裡會留下一些垃圾資料,可以用定時任務在系統負載比較輕的時候刪除它們,例如晚上 3 點。4
. 如果時間需要根據不同的時區顯示,時間相關的欄位最好使用timestamp
而不是datetime
。例如開發一個會議相關的軟體,同時有中國,美國,德國人蔘加會議,如果大家看到開會時間是 2015-03-03 10:00:00,每個人都會自然的認為是自己當地的時間,那麼開會的時候就只有你自己一個人了。
準備資料
在開始講解之前,我們先來準備好需要用到的資料。建立資料庫 qt
,然後在此資料庫裡建立 3 張表 user
,blog
,comment
,以及在每一張表裡插入一些資料,細節如下。
建立 user
表
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(256) NOT NULL,
`password` varchar(256) NOT NULL,
`email` varchar(256) DEFAULT NULL,
`mobile` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
)
INSERT INTO `user` (`id`, `username`, `password`, `email`, `mobile`) VALUES
(1, 'Alice', 'passw0rd', NULL, NULL),
(2, 'Bob' , 'Passw0rd', NULL, NULL),
(3, 'Josh', 'Pa88w0rd', NULL, NULL);
id | username | password | mobile | |
---|---|---|---|---|
1 | Alice | passw0rd | NULL | NULL |
2 | Bob | Passw0rd | NULL | NULL |
3 | Josh | Pa88w0rd | NULL | NULL |
建立 blog
表
CREATE TABLE `blog` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`content` text NOT NULL,
`created_time` datetime NOT NULL,
`last_updated_time` datetime NOT NULL,
PRIMARY KEY (`id`)
)
INSERT INTO `blog` (`id`, `user_id`, `content`, `created_time`, `last_updated_time`) VALUES
(1, 1, 'Content of blog 1.', '2015-01-01 10:10:10', '2015-01-01 10:10:10'),
(2, 2, 'Content of blog 2.', '2015-02-02 20:20:20', '2015-02-02 20:20:20');
id | user_id | content | created_time | last_updated_time |
---|---|---|---|---|
1 | 1 | Content of blog 1. | 2015-01-01 10:10:10 | 2015-01-01 10:10:10 |
2 | 2 | Content of blog 2. | 2015-02-02 20:20:20 | 2015-02-02 20:20:20 |
建立 comment
表
CREATE TABLE `comment` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`blog_id` int(11) NOT NULL,
`content` text NOT NULL,
`created_time` datetime NOT NULL,
PRIMARY KEY (`id`)
)
INSERT INTO `comment` (`id`, `user_id`, `blog_id`, `content`, `created_time`) VALUES
(1, 1, 1, 'Very useful.', '2015-01-02 11:11:11'),
(2, 3, 2, 'Super', '2015-03-03 23:33:33');
id | user_id | blog_id | content | created_time |
---|---|---|---|---|
1 | 1 | 1 | Very useful. | 2015-01-02 11:11:11 |
2 | 3 | 2 | Super | 2015-03-03 23:33:33 |
QSqlDatabase
訪問資料庫前必須先和資料庫建立連線,Qt 裡用 QSqlDatabase
表示一個數據庫的連線(有點不習慣,既然表示的是連線,有沒有覺得如果叫 QSqlConnection 會更好?可惜 Qt 不是我們設計的!),每個連線都有自己的名字 connectionName,用同一個 connectionName 得取的 QSqlDatabase 物件都是表示同一個連線。有一點需要注意,如果要在多執行緒裡訪問資料庫,每個執行緒都要使用不同的資料庫連線
,即每個執行緒使用的
QSqlDatabase 的 connectionName 都不一樣,否則可能會遇到很多預料不到的事。
建立 QSqlDatabase 物件用靜態函式 QSqlDatabase QSqlDatabase::addDatabase(const QString &type, const QString &connectionName=QLatin1String(defaultConnection))
。第一個引數 type 是指定資料庫的型別,例如"QSQLITE"
, "QMYSQL"
, "QPSQL"
。第二個引數是
connectionName,可以是任意的字串,預設是"qt_sql_default_connection"
而不是空字串。
獲取 QSqlDatabase 物件用靜態函式 QSqlDatabase QSqlDatabase::database(const QString &connectionName=QLatin1String(defaultConnection), bool open=true)
。
使用預設的 connectionName 建立和獲取連線:
void createDefaultConnection() {
QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL");
db.setHostName("127.0.0.1");
db.setDatabaseName("qt"); // 如果是 SQLite 則為資料庫檔名
db.setUserName("root"); // 如果是 SQLite 不需要
db.setPassword("root"); // 如果是 SQLite 不需要
if (!db.open()) {
qDebug() << "Connect to MySql error: " << db.lastError().text();
return;
}
}
QSqlDatabase getDefaultConnection() {
return QSqlDatabase::database();
}
- 我們不提供 connectName,則 Qt 使用預設的 connectName
"qt_sql_default_connection"
- 呼叫 createDefaultConnection() 建立資料庫連線
- 呼叫 getConnectionByName() 取得資料庫連線,多次呼叫這條語句得到的都是同一個資料庫連線
使用自定義 connectionName 建立和獲取連線:
void createConnectionByName(const QString &connectionName) {
QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL", connectionName);
db.setHostName("127.0.0.1");
db.setDatabaseName("qt"); // 如果是 SQLite 則為資料庫檔名
db.setUserName("root"); // 如果是 SQLite 不需要
db.setPassword("root"); // 如果是 SQLite 不需要
if (!db.open()) {
qDebug() << "Connect to MySql error: " << db.lastError().text();
return;
}
}
QSqlDatabase getConnectionByName(const QString &connectionName) {
return QSqlDatabase::database(connectionName);
}
- 例如 connectName 為
MyConnection
- 呼叫 createConnectionByName(
MyConnection
) 建立資料庫連線 - 呼叫 getConnectionByName(
MyConnection
) 取得資料庫連線,多次呼叫這條語句得到的都是同一個資料庫連線
重複使用同一個 connectionName 建立資料庫連線並不會建立多個同名的連線,也不會發生錯誤,只是會刪除已經存在的連線,然後用此 connectionName 重新建立連線。這是很有用的,例如資料庫是安裝在其他電腦上,程式開始執行的時候資料庫連線是好用的,但是過了一會網路出問題導致資料庫連線斷開了,過一會網路恢復了,用這個資料庫連線就訪問不了資料庫了,這時還是使用同一個 connectionName
再次建立資料庫連線,就可以訪問資料庫了。但是不要因為使用同一個 connectionName 建立資料庫連線不會導致程式出錯,為了防止上面的情況於是每次使用資料庫連線的時候就重新建立一個
。建立資料庫連線是一個非常耗費資源和時間的操作,底層是 Socket 連線,這樣做雖然保證了程式的正確性,但卻是以效率為代價換回來的,並不是理想的方案。
以前一直擔心在函式之間傳遞 QSqlDatabase 的棧物件而不是指標或者引用會不會佔用很多棧空間,也有可能會不會由於呼叫它的複製建構函式而生成另一個 QSqlDatabase 物件導致什麼問題?首先sizeof(QSqlDatabase)
輸出 8,說明 QSqlDatabase 佔用 8 個位元組,開啟 QSqlDatabase 的原始碼也可以看到它只有 2
個指標的成員變數char *defaultConnection
和 QSqlDatabasePrivate *d
,所以 QSqlDatabase 佔用的空間不大。其次,它的複製建構函式建立的新物件和原來的物件共享char *defaultConnection
和 QSqlDatabasePrivate *d
,所以可以放心的在函式之間傳遞 QSqlDatabase 物件而不用擔心出什麼問題。
QSqlQuery
QSqlQuery 有兩個重要的建構函式,平時我們也基本上就用這兩種形式來構造 QSqlQuery 物件。
QSqlQuery::QSqlQuery(const QString &query = QString(), QSqlDatabase db = QSqlDatabase()):如果沒有傳入或者傳入一個無效的 QSqlDatabase 物件,則使用預設的 QSqlDatabase;如果 query 不是空字串,則會執行這個 query 的資料庫操作。例如QSqlQuery
query("SELECT * FROM user")
則就會使用預設的資料庫連線執行查詢操作,而 QSqlQuery query
則會使用預設的資料庫連線建立一個 QSqlQuery 物件,但是不執行任何操作。
QSqlQuery::QSqlQuery(QSqlDatabase db):使用指定的資料庫連線建立 QSqlQuery 物件,如果資料庫連線無效,則使用預設的資料庫連線,例如QSqlQuery query(getConnectionByName("MyConnection"))
查詢操作
已經有了資料庫和相關資料,瞭解了 QSqlDatabase 和 QSqlQuery,接下來舉例分析使用 QSqlQuery,可能遇到的問題以及解決辦法。為了簡單起見,都使用預設的資料庫連線,思考一下下面的例子裡怎麼把預設的資料庫連線換成我們自己指定的 connectionName 的連線呢?
1. 輸出 user 表裡所有的 id, username, password
/**
* 輸出 user 表裡所有的 id, username, password
*/
void outputIdUsernamePassword() {
QSqlQuery query("SELECT id, username, password FROM user");
while (query.next()) {
qDebug() << QString("Id: %1, Username: %2, Password: %3")
.arg(query.value("id").toInt())
.arg(query.value("username").toString())
.arg(query.value("password").toString());
}
}
輸出:
Id: 1, Username: Alice, Password: passw0rdId: 2, Username: Bob, Password: Passw0rdId: 3, Username: Josh, Password: Pa88w0rd
2. 輸出 username 為 Alice 的 user
1. 函式定義
/**
* 輸出 user 其 username 等於傳入的引數 username
* @param username
*/
void outputUser(const QString &username) {
QString sql = "SELECT * FROM user WHERE username='" + username + "'";
QSqlQuery query(sql);
while (query.next()) {
qDebug() << QString("Id: %1, Username: %2, Password: %3")
.arg(query.value("id").toInt())
.arg(query.value("username").toString())
.arg(query.value("password").toString());
}
}
2. 函式呼叫
outputUser("Alice");
輸出:
Id: 1, Username: Alice, Password: passw0rd
輸出結果和我們期待的一樣。但是思考一下,上面的程式有沒有什麼問題?
如果我們這麼呼叫函式 outputUser("Alice' OR '1=1")
,輸出如下:
Id: 1, Username: Alice, Password: passw0rdId: 2, Username: Bob, Password: Passw0rdId: 3, Username: Josh, Password: Pa88w0rd
是不是有什麼不對?outputUser("Alice' OR '1=1")
按我們的想法應該輸出 username 等於 Alice' OR '1=1
的記錄,但卻輸出了 user 表裡的所有記錄,完全和期望的不一樣,一定是有什麼地方出錯了,但是看上去都沒什麼問題呀,怎麼都找不到錯誤吧
!
這裡我們引入一個概念叫做 SQL 注入攻擊
:
SQL 注入攻擊指的是通過構建特殊的輸入作為引數傳入,而這些輸入大都是 SQL 語法裡的一些組合,通過執行 SQL 語句進而執行攻擊者所要的操作,其主要原因是程式沒有細緻地過濾使用者輸入的資料,致使非法資料侵入系統。
上面的程式在查詢一個使用者的時候卻輸出了所有使用者的資訊,就是一個 SQL 注入攻擊
的例子,原因是使用的 SQL 語句是用字串相加拼湊出來的
,以至於引數中的特殊字元'
沒有被轉義而拼成了 SELECT * FROM user WHERE username='Alice' OR '1=1'
,WHERE
條件裡有OR '1=1'
,所以條件永遠為真,於是輸出了所有的記錄,這樣做是很危險的,如果因此而洩漏了公司機密資訊,都不敢往下想了。
3. 怎麼避免SQL 注入攻擊
呢?
使用 Prepared Query
可以避免SQL 注入攻擊
。
/**
* 輸出 user 其 username 等於傳入的引數 username
* @param username
*/
void outputUserWithPreparedQuery(const QString &username) {
QString sql = "SELECT * FROM user WHERE username=:username";
QSqlQuery query; // [1] 可以傳入資料庫連線,但是不能傳入 SQL 語句
query.prepare(sql); // [2] 宣告使用 prepqred 的方式解析 SQL 語句
query.bindValue(":username", username); // [3] 把佔位符替換為傳入的引數
query.exec(); // [4] 執行資料庫操作
while (query.next()) { // [5]
qDebug() << QString("Id: %1, Username: %2, Password: %3")
.arg(query.value("id").toInt())
.arg(query.value("username").toString())
.arg(query.value("password").toString());
}
}
呼叫 outputUserWithPreparedQuery("Alice")
輸出:
Id: 1, Username: Alice, Password: passw0rd
呼叫 outputUserWithPreparedQuery("Alice' OR '1=1")
則沒有輸出,因為 user 表裡沒有 username 為Alice' OR '1=1
的記錄。太好了,SQL 注入攻擊
的問題很容易就解決了。
使用 Prepared Query
分為以下 5 步:
- 建立 QSqlQuery 物件:
QSqlQuery query;
- 呼叫
query.prepare(sql);
宣告要使用Prepared Query
的方式來解析 SQL 語句 - 呼叫
bindValue()
函式把佔位符
替換為傳入的引數:query.bindValue(":username", username);
- 所有的佔位符都替換好後,呼叫
query.exec();
執行 SQL 語句 - 遍歷查詢結果
佔位符的格式為::
後跟一個單詞,如 :username
。
思考一下下面幾個問題:
SELECT * FROM user WHERE username=:username AND password=:password
的佔位符是什麼呢?- SQL 裡的字串需要用
‘’
括起來嗎? - 如果傳入的引數是 QDateTime 型別,佔位符應該怎麼寫?
答案:
- 這條 SQL 語句裡有 2 個佔位符,分別為
:username
和:password
(不要想成是username AND password=:password) - 不需要。SQL 裡字串需要用
''
括起來,但是在這裡不需要,Qt 在替換引數的時候會智慧的根據引數的型別判斷是否需要加上''
,如果傳入的字串引數裡有'
,也會智慧的將其轉義,所以避免了SQL 注入攻擊
。 - 寫法也是一樣的,
:
後跟一個單詞,可以看看下面的這個例子
/**
* 輸出 2015-02-01 10:10:10 後建立的 blog.
*/
void outputBlogWithPreparedQuery() {
QDateTime dateTime = QDateTime::fromString("2015-02-01 10:10:10", "yyyy-MM-dd hh:mm:ss");
QSqlQuery query;
query.prepare("SELECT * FROM blog WHERE created_time>:createdTime");
query.bindValue(":createdTime", dateTime);
query.exec();
while (query.next()) {
qDebug() << QString("Id: %1, Content: %2, Created_Time: %3")
.arg(query.value("id").toInt())
.arg(query.value("content").toString())
.arg(query.value("created_time").toDateTime().toString("yyyy-MM-dd hh:mm:ss"));
}
}
輸出:
Id: 2, Content: Content of blog 2., Created_Time: 2015-02-02 20:20:20
什麼時候使用 Prepared Query 呢?插入時能使用嗎?刪除時能使用嗎?
- 如果傳入引數來構造 SQL 語句,為了避免
SQL 注入攻擊
,所以這時需要使用 - 使用 Prepared Query 的方式構造的 SQL 語句比用原始的字串相加的方式拼湊 SQL 看上去可讀性好很多
- 插入和刪除語句都能用
4. 曾經使用 LIKE 查詢的時候遇到點困難
查詢名字裡有 o
的所有使用者:SELECT * FROM user WHERE username LIKE '%o%'
,開始時使用
query.prepare("SELECT * FROM user WHERE username LIKE '%:match%'");
query.bindValue(":match", "o");
但是查詢結果為空,弄了好久後來才發現,使用 Prepared Query 構造 SQL 語句,LIKE 之後不能有 ''
,''
之間的內容由引數傳入,如下:
query.prepare("SELECT * FROM user WHERE username LIKE :match");
query.bindValue(":match", "%o%");
5. 多表查詢時欄位名衝突
列出所有的 user,如果 user 有 blog,則列出 blog:SELECT * FROM user LEFT JOIN blog ON user.id = blog.user_id
。在資料庫客戶端執行這條 SQL 語句結果如下圖:
有 2 個列名都叫 id,第一個 id 是 user 表的 id,第二個 id 是 blog 表的 id。執行下面的程式:
void outputUserAndBlog() {
QSqlQuery query("SELECT * FROM user LEFT JOIN blog ON user.id = blog.user_id");
while (query.next()) {
qDebug() << query.value("id").toInt();
}
}
輸出:
1
2
3
取到的是第一列的 id,即 user 表的 id,並沒有輸出 blog 表的 id。如果我們想取得 blog 表的 id 應該怎麼做呢?顯然上面的 SQL 不行,但是我們可以給查詢欄位重新命名
:
void outputUserAndBlog() {
QSqlQuery query("SELECT user.id as user_id, blog.id as blog_id, blog.content as content "
"FROM user "
"LEFT JOIN blog ON user.id = blog.user_id");
while (query.next()) {
qDebug() << QString("User_Id: %1, Blog_ID: %2, Blog_Content: %3")
.arg(query.value("user_id").toInt())
.arg(query.value("blog_id").toInt())
.arg(query.value("content").toString());
}
}
輸出:
User_Id: 1, Blog_ID: 1, Blog_Content: Content of blog 1.User_Id: 2, Blog_ID: 2, Blog_Content: Content of blog 2.User_Id: 3, Blog_ID: 0, Blog_Content:
通過給查詢欄位重新命名的方式,解決了多表查詢時欄位名衝突
的問題。
插入操作
/**
* 向 user 表裡插入一個 user
* @param username
* @param password
*/
void insertUser(const QString &username, const QString &password) {
QSqlQuery query;
query.prepare("INSERT INTO user (username, password) VALUES (:username, :password)");
query.bindValue(":username", username);
query.bindValue(":password", password);
query.exec();
}
呼叫 insertUser("Qter", "secret")
可以看到資料庫裡多出了剛才插入的資料。
id | username | password | mobile | |
---|---|---|---|---|
1 | Alice | passw0rd | NULL | NULL |
2 | Bob | Passw0rd | NULL | NULL |
3 | Josh | Pa88w0rd | NULL | NULL |
4 | Qter | secret | NULL | NULL |
刪除操作
/**
* 刪除名字等於傳入的 username 的 user
* @param username
*/
void deleteUser(const QString &username) {
QSqlQuery query;
query.prepare("DELETE FROM user WHERE username=:username");
query.bindValue(":username", username);
query.exec();
}
呼叫 deleteUser("Qter")
則刪除了 user 表裡 username 為 Qter
的所有記錄。