Qt編寫物聯網管理平臺38-多種數據庫支持

一、前言

本系統設計之初就要求支持多種不同的數據庫,比如sqlite、mysql、postgres、sqlserver等,甚至包括國產數據庫比如人大金倉kingbase等,(由於現在國產化的大力推進,國產數據庫也是必須要支持的),Qt中封裝的數據庫組件sql模塊,對所有數據庫都做了抽象層,這就爲應用程序做多種數據庫支持做了很好的前提準備,所以更多的在細節處理,比如數據庫日期範圍查詢,不同數據庫處理方式不一樣,這就需要在自己封裝的組件中做不同的處理,還有就是數據庫分頁,不同數據庫的分頁查詢語句是不一樣的,比如sqlite和mysql使用的是limit關鍵字,postgres和kongbase數據庫用的是limit offset關鍵字,sqlserver用的是top關鍵字,oracle最複雜需要用隱藏列做組合,這個都需要在自己封裝的輪子中做特殊處理,封裝成一個函數直接調用。

數據庫的幾點經驗總結:

  • 在sqlite數據庫中沒有數據類型概念,你設置的int依然可以存儲字符串。其他數據庫有數據類型區分。
  • 設計數據庫字段的時候儘量避開數據庫關鍵字和函數關鍵字,比如year、month、plan等,如果必須要則執行sql語句的時候對應字段要加上引號。
  • 除了sqlite數據庫外,其他數據庫都有長度要求,比如設置的2,你硬要插入長度3則會報錯,執行不成功。
  • varchar類型存儲字符串,gbk編碼下一個漢字2字節,utf8編碼下一個漢字3字節,所以一定要考慮好,比如內容 '啓用' 最好設置長度6字節。
  • 爲了儘可能減少數據庫文件體積,建議在已知字段長度的情況下儘量按照已知的長度設置。
  • 記錄行數比較多的數據庫表,經常需要查詢的表,必須設置索引,不然非常慢。
  • 小數據量的表比如就幾千條以下,而且幾乎不需要變動,則不必要設置索引,這樣可以加快插入速度。
  • 整型字段 INT、INTEGER 不能設置長度,也不需要設置長度,不然在sqlserver等數據庫執行的時候會報錯。
  • 建議給每個表設置個主鍵字段,尤其是需要建索引的表必須有主鍵字段。
  • linux上安裝mysql客戶端命令:apt-get install libmysqlclient-dev 。

關於Qt數據庫開發的一些冷知識。

  • Qt即支持庫的形式直接和數據庫通信,也支持ODBC數據源的形式和各種數據庫通信,這樣就涵蓋了所有的情況。
  • Qt數據庫程序打包發佈,所有前提:注意區分32/64位,你的程序是32位的就必須帶上32位的庫,64位的必須帶上64位的庫,這點Qt的庫也是這個要求。mysql發佈最簡單,帶上一個mysql的動態庫文件就行(windows上的是libmysql.dll),非常簡單。sqlserver不用帶,因爲是微軟的親兒子,一般操作系統自帶。postgres需要帶上libpq.dll、libintl-8.dll、libiconv-2.dll、libeay32.dll、ssleay32.dll這幾個文件就行。oracle需要帶上oci.dll、oraociei11.dll(這個文件很大有130MB+),如果不行建議直接安裝個oracle client客戶端軟件,然後對應bin目錄設置到環境變量就好。
  • 打包發佈後測試下來,發現32位的程序也可以正常連接64位的mysql,64位的程序也可以正常連接32位的mysql,因此判斷只要和程序的庫的位數一致就行(編譯的時候也是這個規則,32位的Qt程序編譯數據庫插件也要用32位的數據庫鏈接庫。),不需要和具體的數據庫的位數一致,測試過mysql、sqlserver、postgresql數據庫都是類似規則。
  • 大量測試對比下來,通過odbc數據源的方式和直連數據庫的方式批量插入大量數據記錄,直連方式速度更快,約5%左右,所以建議儘量採用此方式,是在沒有此方式的環境才採用odbc數據源的方式,Qt默認自帶odbc數據庫插件。
  • 不同數據庫在執行sql腳本的時候,會自動將表名或者字段名轉成大寫或小寫,mysql會將表名轉成小寫、postgresql會將表名和字段名轉成小寫、oracle會將表名和字段名轉成大寫。這就導致使用QSqlTableModel調用setTable設置數據庫表名的時候,一定要和數據庫中的表名一致,區分大小寫,所以就是在對postgresql和oracle數據庫的時候一定要注意,本人就是在這裏卡了很久,差點要把這巨大的屎盆扣在Qt的BUG上。
void DbHelper::bindTable(const QString &dbType, QSqlTableModel *model, const QString &table)
{
    //postgresql全部小寫,oracle全部大寫,這兩個數據庫嚴格區分表名字段名的大小寫臥槽
    QString flag = dbType.toUpper();
    if (flag == "POSTGRESQL") {
        model->setTable(table.toLower());
    } else if (flag == "ORACLE") {
        model->setTable(table.toUpper());
    } else {
        model->setTable(table);
    }
}
  • Qt支持不指定數據庫名打開數據庫,因爲有時候是要在連接數據庫服務器後,執行sql語句創建數據庫。數據庫都還沒存在怎麼連接呢,測試發現sqlite、mysql、sqlserver、postgresql都支持這個特性。在刪除和創建數據庫的前提是該數據庫沒有被其他程序佔用,比如其他程序已經打開了該數據庫則會執行失敗。這裏我就折磨過很多次,爲什麼執行失敗呢?後面發現第三方數據庫工具已經打開了該數據庫,把工具關掉就ok了。
QSqlDatabase database = QSqlDatabase::addDatabase("QMYSQL");
//database.setDatabaseName("dbtool");
database.setHostName("127.0.0.1");
database.setPort(3306);
database.setUserName("root");
database.setPassword("root");

if (database.open()) {
    QSqlQuery query(database);
    qDebug() << "刪除數據庫" << query.exec("drop database dbtool");
    qDebug() << "創建數據庫" << query.exec("create database dbtool");
    if (query.exec("select * from userinfo")) {
        while (query.next()) {
            qDebug() << "查詢數據庫" << query.value(0);
        }
    }
} else {
     qDebug() << "打開數據庫" << database.lastError().text();
}
  • 用QSqlQueryModel+QTableView顯示數據,int類型的數據,如果超過100萬,會變成科學計數顯示,這就很惱火了,肯定不是自己想要的結果。找遍網絡搜索,終於找到一個同樣問題的哥們,需要對這一列加個空的委託就行。後面發現空委託也不行,超過1000萬條又屌樣了,需要終極大法重載數據模型顯示。
ui->tableView->setItemDelegateForColumn(0, new QItemDelegate);

//下面是終極大法
QVariant SqlQueryModel::data(const QModelIndex &index, int role) const
{
    QVariant value = QSqlQueryModel::data(index, role);
    //超過100萬的數值會被科學計數顯示需要這裏轉成字符串顯示
    if (role == Qt::DisplayRole) {
        int result = value.toInt();
        if (result >= 1000000) {
            value = QString::number(result);
        }
    }
    return value
}
  • mysql數據庫有多種數據庫引擎,其中MyIsam不支持數據庫事務,默認一般是這個引擎,所以當你使用Qt中的transaction方法後commit提交時候,會發現不成功,其實事實上又是成功的,去數據庫裏面查看對應的結果又是正確的。有兩個辦法,第一就是將數據庫引擎改成InnoDB,第二就是在提交後做個錯誤判斷 if (database.commit() || !database.lastError().isValid()) ,錯誤不可用也說明是成功的。
  • 如果採用odbc數據源通信,則只需設置數據庫名稱setDatabaseName、設置用戶名稱setUserName、設置用戶密碼setPassword這三個參數即可,因爲數據源配置的時候就已經設置好對應的主機地址和端口以及關聯的數據庫名稱,所以在用odbc數據源通信的時候只需要再次驗證用戶信息即可。這裏特別要注意的是setDatabaseName設置數據庫名稱要填寫數據源配置的名稱。
  • 經過大量的對比測試,包括插入、刪除、批量、查詢、分頁等操作,千萬量級數據,在Qt數據庫部分響應速度這塊,友好度排名依次是 sqlite > postgresql > oracle > mysql > odbc 。千萬量級以上是 postgresql > oracle > mysql > sqlite > odbc 。億級別以上是 oracle > postgresql > 其他。以上測試均建立在初學者水平基礎上,至於分庫分表、聯合查詢、緩存、內存數據庫等各種高級知識點沒用上。
  • mysql主要有兩個版本,mysql5.7和mysql8,官方說是8比5要快很多,個人測試下來,5.7比8要快很多,無論是查詢,還是批量插入數據,不知道爲何,網上搜索的也是這個結果(https://www.coder4.com/archives/7596),大家都說8慢了很多。
  • mysql有個分支叫mariadb,比mysql更純正,據說各方面都吊打mysql(https://blog.csdn.net/x275920/article/details/123847792),個人對比測試下來也是確實批量插入和查詢性能要好不少,並且完全兼容mysql,甚至庫文件直接重命名也可以直接使用,比如將libmariadb.dll改成libmysql.dll可以直接使用,而且體積還小了八倍,這個好,發佈的時候又少了好幾兆。
  • 如果是Qt+mysql程序,發佈的時候帶的庫版本要和插件對應數據庫版本一致,否則可能沒有數據庫事務特性,database.driver()->hasFeature(QSqlDriver::Transactions)爲假。
  • QSqlTableModel封裝的非常好,並不會一次性加載所有數據,而是隨着滾動條的拉動加載需要的數據,測試一億條的表,速度非常快,和幾千條的錶速度一樣。
  • 在連接網絡數據庫的時候,如果你本地網絡設置了代理,比如使用了代理上github等網站,就會發現Qt的數據庫程序連不上,你需要設置下不使用本地代理設置 QNetworkProxyFactory::setUseSystemConfiguration(false) 。這個地方如果不仔細會找問題找到你懷疑人生。

二、功能特點

2.1 軟件模塊

  1. 設備監控模塊,包括數據監控(表格形式展示)、設備面板(面板形式展示)、地圖監控(地圖形式展示)、曲線監控(曲線形式展示)。
  2. 數據查詢模塊,包括報警記錄、運行記錄、操作記錄。
  3. 系統設置模塊,包括基本設置、端口管理、控制器管理、探測器管理、報警聯動、類型設置等。
  4. 其他設置模塊,包括用戶管理、地圖管理、位置調整、組態設計、設備調試等。

2.2 基礎功能

  1. 設備數據採集,支持串口、網絡,串口可設置串口號、波特率,網絡可設置IP地址、通訊端口。
  2. 每個端口支持採集週期時間,默認1秒鐘一個設備。
  3. 支持設置通訊超時次數,默認3次。
  4. 支持最大重連時間,用於重新讀取離線的設備。
  5. 控制器信息,能夠添加控制器名稱,選擇控制器地址、控制器型號,設置該控制器下面的探測器數量。
  6. 探測器信息,能夠添加位號、探測器型號、氣體種類、氣體符號、高報值、低報值、緩衝值、清零值、是否啓用、報警聲音、背景地圖、存儲週期、數值換算小數點位數、報警延時時間、報警的類型(HH,LL,HL)等。
  7. 類型管理可配置控制器型號、探測器型號、氣體種類、氣體符號等。
  8. 地圖支持導入和刪除,所有的探測器在地圖上的位置可自由拖動保存。
  9. 端口信息、控制器信息、探測器信息、類型信息、用戶信息等,都支持導入、導出、導出到excel、打印。
  10. 運行記錄、報警記錄、操作記錄,都支持多條件組合查詢,比如時間段、控制器、探測器等,所有記錄支持導出到excel/pdf和打印。
  11. 運行記錄、報警記錄、操作記錄都可刪除指定時間範圍內的數據。
  12. 系統設置可選擇對應表最大保存記錄數,自動清理早期數據,留出足夠的空間存儲重要的數據。
  13. 報警短信轉發,支持多個接收手機號碼,可設定發送間隔,比如即時發送或者6個小時發送一次所有的報警信息,短信內容過長,自動拆分多條短信。
  14. 報警郵件轉發,支持多個接收郵箱,可設定發送間隔,比如即時發送或者6個小時發送一次所有的報警信息,支持附件發送。
  15. 設置軟件的中文標題、英文標題、logo路徑、版權所有等。
  16. 開關設置開機運行、報警聲音、自動登錄、記住密碼等。
  17. 報警聲音可設置播放次數,界面風格樣式提供18套皮膚文件選擇。
  18. 用戶管理,包括用戶權限配置,不同用戶可以有不同模塊的權限。
  19. 用戶登錄和用戶退出,可以記住密碼和自動登錄,超過三次報錯提示並關閉程序。
  20. 四種監控模式,設備面板監控、地圖監控、表格數據監控、曲線數據監控,可自由切換,四種模式下都實時展示採集到的數據,報警閃爍等。
  21. 報警繼電器聯動,一個位號可以跨串口聯動多個模塊和繼電器號,支持多對多。

2.3 特色功能

  1. 通信協議支持modbus_com、modbus_tcp_rtu,後期拓展mqtt等協議。
  2. 數據源除了真實的硬件設備採集,還可選數據庫採集,這樣用戶可以安排其他程序員比如java程序員將前端採集好的數據放到數據庫,本系統直接從數據庫採集即可。數據庫採集模式可以作爲通用的系統使用,更適合多人多系統協作。
  3. 智能跳過超時的設備,加快對在線設備的採集速度,當設備數量很多的時候尤其有用。
  4. 對智能跳過的超時的設備,在設定的重連時間自動採集一次,以便探測設備是否又重新上線。
  5. 每個探測器可控是否啓用,不啓用則不會採集,也不會在界面顯示,相當於運行階段臨時關閉。
  6. 探測器可設置緩衝值和報警延時時間,在該值附近波動產生的報警,不計入報警,只有持續處於報警值且超過報警延時時間纔算真正報警,這樣可以規避很多波動導致的誤報。
  7. 探測器可設置存儲週期,按照設定的時間來存儲一條運行記錄,可以按照重要程度對重要性高的設定存儲週期短一些,不重要的設定大一些,這樣可以節省不少的存儲空間,也保證了重要的數據及時存儲。
  8. 探測器可設置清零值,在一些高精度高靈敏的設備可能出廠的時候默認值未必是0,需要設定清零值來表示初始值。
  9. 探測器可設置小數點,用於計算後的真實數據控制小數點點位顯示,相當於除以10、除以100、除以1000,這樣大部分的探測器數據直接通過小數點位設置控制真實換算後的值,極個別的需要特殊轉換的可以在通信協議中約定。
  10. 探測器報警的類型支持多種,有些設備是高於某個值高報,低於某個值低報,而有些設備是在最小值最大值範圍內是高報,低於最小值低報,高於最大值正常。這樣可以分情況處理,涵蓋各種報警類型。
  11. 原創數據導入、導出、打印機制,跨平臺不依賴任何組件,瞬間導出數據。
  12. 導出到excel的記錄支持所有excel、wps等表格文件版本,不依賴excel等軟件。
  13. 高報顏色、低報顏色、正常顏色、默認值顏色等,都可以自由設置。
  14. 支持雲端數據同步,將本地採集到的數據實時同步到雲端。
  15. 支持網絡轉發和網絡接收,網絡接收開啓後,軟件從udp接收數據進行解析。網絡轉發支持多個目標IP,這樣就實現了本地採集的軟件,自由將數據轉到客戶端,隨時查看採集到的數據。
  16. 自動記住用戶最後停留的界面以及其他配置信息,重啓後自動應用。
  17. 報警自動切換到對應的地圖,探測器按鈕閃爍,表格數據對應顏色顯示。
  18. 雙擊探測器圖標,彈出對應探測器詳細信息,可以根據需要定製回控操作。
  19. 數據庫支持多種,包括sqlite、mysql、sqlserver、postgresql、oracle、人大金倉等。
  20. 本地設備採集到的數據實時上傳到雲端,以便手機APP或者web等其他方式提取。
  21. 自帶設備模擬工具,支持不同型號的多個設備數據模擬,同時還帶數據庫數據模擬,以便在沒有設備的時候測試數據。
  22. 標準modbus協議,各種控制器類型、探測器類型、種類、符號等全部自定義,非常靈活和強大,通信協議示例數據非常完整,通用各種modbus協議系統,適用於各種應用場景接入。
  23. 同時集成了串口通信、網絡通信、數據庫通信、數據導入導出打印、通信協議解析、界面UI、全局換膚等衆多組件和知識點,非常適合新手入門和進階。
  24. 支持xp、win7、win10、、win11、linux、mac、各種國產系統(UOS、中標麒麟、銀河麒麟等)、嵌入式linux等系統。
  25. 註釋完整,項目結構清晰,超級詳細完整的使用開發手冊,精確到每個代碼文件的功能說明,不斷持續迭代版本。

三、體驗地址

  1. 國內站點:https://gitee.com/feiyangqingyun
  2. 國際站點:https://github.com/feiyangqingyun
  3. 個人主頁:https://blog.csdn.net/feiyangqingyun
  4. 知乎主頁:https://www.zhihu.com/people/feiyangqingyun
  5. 產品主頁:https://blog.csdn.net/feiyangqingyun/article/details/97565652
  6. 在線文檔:https://feiyangqingyun.gitee.io/qwidgetdemo/iotsystem/
  7. 體驗地址:https://pan.baidu.com/s/1ZxG-oyUKe286LPMPxOrO2A 提取碼:o05q 文件名:bin_iotsystem.zip。
  8. 文章導航:https://qtchina.blog.csdn.net/article/details/121330922

四、效果圖






五、相關代碼

void frmConfigDb::on_btnConnect_clicked()
{
    {
        //初始化數據庫連接信息結構體數據
        DbInfo dbInfo;
        initDbInfo(dbInfo, connName);

        QString dbType = AppConfig::LocalDbType.toUpper();
        if (dbType == "SQLITE") {
            dbInfo.dbName = DbHelper::getDbDefaultFile(connFlag);
            if (QFile(dbInfo.dbName).size() <= 4) {
                QUIHelper::showMessageBoxError("數據庫文件不存在!", 5);
                return;
            }
        }

        QSqlDatabase database;
        if (DbHelper::initDatabase(true, dbType, database, dbInfo)) {
            if (database.open()) {
                database.close();
                QUIHelper::showMessageBoxInfo("打開數據庫成功!", 3);
            } else {
                QString error = database.lastError().text();
                QUIHelper::showMessageBoxError("打開數據庫失敗!\n" + error, 3);
            }
        } else {
            QString error = database.lastError().text();
            QUIHelper::showMessageBoxError("連接數據庫失敗!\n" + error, 3);
        }
    }

    QSqlDatabase::removeDatabase(connName);
}

void frmConfigDb::on_btnInit_clicked()
{
    if (QUIHelper::showMessageBoxQuestion("確定要初始化數據庫嗎? 會全部清空數據並且不可還原!") != QMessageBox::Yes) {
        return;
    }

    QString sqlName = QString("%1/db/%2.sql").arg(QUIHelper::appPath()).arg(connFlag);
    QFile file(sqlName);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        QUIHelper::showMessageBoxError("數據庫腳本文件打開失敗!", 3);
        return;
    }

    QElapsedTimer time;
    time.start();
    {
        //初始化數據庫連接信息結構體數據
        DbInfo dbInfo;
        initDbInfo(dbInfo, connName);

        QString dbType = AppConfig::LocalDbType.toUpper();
        if (dbType == "SQLITE") {
            dbInfo.dbName = DbHelper::getDbDefaultFile(connFlag);
            //如果文件存在則先刪除原來的數據庫文件,貌似win上不行
            QFile file(dbInfo.dbName);
            if (file.exists()) {
                bool ok = file.remove();
                if (!ok) {
                    qDebug() << TIMEMS << "remove error" << dbInfo.dbName;
                }

                //清空所有表
                QStringList tables = QSqlDatabase::database().tables();
                foreach (QString table, tables) {
                    DbHelper::clearTable(table, dbType);
                    qDebug() << TIMEMS << "clearTable" << table;
                }

                //關閉默認數據庫連接
                QSqlDatabase::database().close();
            }
        }

        //初始化數據庫連接並打開數據庫
        QSqlDatabase database;
        if (!DbHelper::initDatabase(true, dbType, database, dbInfo)) {
            QString error = database.lastError().text();
            QUIHelper::showMessageBoxError("連接數據庫失敗!\n" + error, 3);
            return;
        }
        if (!database.open()) {
            QString error = database.lastError().text();
            QUIHelper::showMessageBoxError("打開數據庫失敗!\n" + error, 3);
            return;
        }

        QSqlQuery query(QSqlDatabase::database(connName));

        //第一步:刪除原有數據庫
        QString sql = QString("DROP DATABASE %1;").arg(dbInfo.dbName);
        qDebug() << TIMEMS << "sql:" << sql << "result:" << query.exec(sql);

        //第二步:新建數據庫
        sql = QString("CREATE DATABASE %1;").arg(dbInfo.dbName);
        qDebug() << TIMEMS << "sql:" << sql << "result:" << query.exec(sql);

        //第三步:切換到新建的數據庫庫並執行建表語句
        database.close();
        if (!DbHelper::initDatabase(false, dbType, database, dbInfo)) {
            QString error = database.lastError().text();
            QUIHelper::showMessageBoxError("連接數據庫失敗!\n" + error, 3);
            return;
        }
        if (!database.open()) {
            QString error = database.lastError().text();
            QUIHelper::showMessageBoxError("打開數據庫失敗!\n" + error, 3);
            return;
        }

        //將執行出錯的sql語句輸出到文件方便查看
        QString fileName2 = QString("%1/db/error.sql").arg(QUIHelper::appPath());
        QFile file2(fileName2);

        QSqlQuery query2(QSqlDatabase::database(connName));

        sql = "BEGIN;";
        qDebug() << TIMEMS << "sql:" << sql << "result:" << query2.exec(sql);

        while (!file.atEnd()) {
            sql = QString::fromUtf8(file.readLine());
            sql.replace("\n", "");

            //有些數據庫不支持的語句跳過去
            if (DbHelper::existNoSupportSql(sql)) {
                continue;
            }

            //重新糾正sql語句
            DbHelper::checkSql(dbType, sql);

            if (!query2.exec(sql)) {
                //打印及輸出錯誤信息
                QString error = query2.lastError().text();
                qDebug() << TIMEMS << "sql:" << sql << error;

                //沒打開則先打開
                if (!file2.isOpen()) {
                    file2.open(QFile::WriteOnly | QFile::Append);
                }

                QString msg = QString("時間[%1]  語句: %2  錯誤: %3\n").arg(DATETIME).arg(sql).arg(error);
                file2.write(msg.toUtf8());
            }
        }

        sql = "COMMIT;";
        qDebug() << TIMEMS << "sql:" << sql << "result:" << query2.exec(sql);
        database.close();

        //sqlite數據庫的話再執行下壓縮減少體積
        if (dbType == "SQLITE") {
            DbHelper::execSql("VACUUM;");
        }
    }

    QSqlDatabase::removeDatabase(connName);
    double ms = time.elapsed();
    QString info = QString("數據庫腳本執行成功, 總共用時 %1 秒!\n記得重新啓動程序!").arg(QString::number(ms / 1000, 'f', 1));
    QUIHelper::showMessageBoxInfo(info, 3);
}

void frmConfigDb::on_cboxDbType_activated(int)
{
    //自動切換默認端口號和其他信息
    QString hostPort, userName, userPwd;
    QString dbType = ui->cboxDbType->currentText().toUpper();
    DbHelper::getDbDefaultInfo(dbType, hostPort, userName, userPwd);
    if (!hostPort.isEmpty() && AppConfig::LocalAutoInfo) {
        QStringList hostInfo = ui->txtHostInfo->text().split(":");
        ui->txtHostInfo->setText(hostInfo.at(0) + ":" + hostPort);
        ui->txtUserName->setText(userName);
        ui->txtUserPwd->setText(userPwd);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章