QxORM & QT 使用过程记录

以下内容均基于 Windows

背景知识:只知道 C++ 语法,不知道 C++ 的工程实践

如何编译 QxORM

  • 下载源码
  • 使用 Qt 打开源码目录下的 *.pro 文件
  • 在 Qt 的 项目 → 构建和运行 选择一个编译器,如 Desktop Qt 6.7.0 MinGW 64-bit
  • 注意上方的 构建设置 栏目下的 编辑构建设置,其拥有多个构建方法,如 DebugRelease
  • 构建目录 调整为 QxORM 的源码根目录 + /lib,依次对 DebugRelease 构建方法进行调整
  • 在左下方的 💻 那,依次选择 DebugRelease 点击 🔨 ,一共需要 🔨 两次

完成:lib 目录下出现 libQxOrmd.alibQxOrm.a 文件

如何将 QxORM 加入 Qt 项目中

将 Qt 项目的 *.pro 文件加入以下语句

  • 第一行表示在 release 构建的情况下,使用文件名结尾不带 d 的 QxORM
  • 第二行表示在 debug 构建的情况下,使用文件名结尾带 d 的 QxORM
  • 第 4 5 行引入头文件
1
2
3
4
5
win32:CONFIG(release, debug|release): LIBS += -L$$PWD/../../Qt/QxOrm/lib/ -lQxOrm
else:win32:CONFIG(debug, debug|release): LIBS += -L$$PWD/../../Qt/QxOrm/lib/ -lQxOrmd

INCLUDEPATH += $$PWD/../../Qt/QxOrm/include
DEPENDPATH += $$PWD/../../Qt/QxOrm/include

所有出现的 $$PWD/../../Qt/QxOrm/ 都要更改为自己的 QxORM 与 Qt 项目的相对目录,$$PWD 指代 Qt 项目的根路径。

QxORM 需要使用数据库驱动,所以还需要在第一行进行修改(添加 sql)

1
QT       += core gui sql

甚至看到有教程说要把 QxORM 的源码全部拖到 Qt 项目中的 😲

还好过往的开发经验告诉我,当一个问题的找不到靠谱的解决方案时,往往是这个问题过于简单和常见,搜索 “Qt 添加库”,放弃 QxORM 关键词,即可得到答案。

如何创建一个新的实体

注意:如果实体创建的步骤有遗漏,QxORM 并不会给出非常清晰的提示,很容易摸不着头脑,所以要确定自己没有遗漏任何步骤

直接使用 QtCreator 建立类,下面省略了 #IFDEF 等 QtCreator 会生成的代码

  1. 头文件内注册

    classname.h
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    #include <QxORM.h>

    class ClassName {}

    // qx::trait::no_base_class_defined 是对于当前实体没有父类的情况
    QX_REGISTER_HPP_EXPORT_DLL(ClassName, qx::trait::no_base_class_defined, 1)
    // 如果有父类实体,则直接使用父类实体的类名,如 BaseEntity
    // QX_REGISTER_HPP_EXPORT_DLL(ClassName, BaseEntity, 1)

    // 可选 触发器
    namespace qx {
    namespace dao {
    namespace detail {

    template <>
    struct QxDao_Trigger<ClassName>
    {
    static inline void onBeforeInsert(ClassName * t, qx::dao::detail::IxDao_Helper * dao)
    { if (t) { t->onBeforeInsert(dao); } }
    static inline void onBeforeUpdate(ClassName * t, qx::dao::detail::IxDao_Helper * dao)
    { if (t) { t->onBeforeUpdate(dao); } }
    static inline void onBeforeDelete(ClassName * t, qx::dao::detail::IxDao_Helper * dao)
    { Q_UNUSED(t); Q_UNUSED(dao); }
    static inline void onBeforeFetch(ClassName * t, qx::dao::detail::IxDao_Helper * dao)
    { Q_UNUSED(t); Q_UNUSED(dao); }
    static inline void onAfterInsert(ClassName * t, qx::dao::detail::IxDao_Helper * dao)
    { Q_UNUSED(t); Q_UNUSED(dao); }
    static inline void onAfterUpdate(ClassName * t, qx::dao::detail::IxDao_Helper * dao)
    { Q_UNUSED(t); Q_UNUSED(dao); }
    static inline void onAfterDelete(ClassName * t, qx::dao::detail::IxDao_Helper * dao)
    { Q_UNUSED(t); Q_UNUSED(dao); }
    static inline void onAfterFetch(ClassName * t, qx::dao::detail::IxDao_Helper * dao)
    { Q_UNUSED(t); Q_UNUSED(dao); }

    };

    } // namespace detail
    } // namespace dao
    } // qx
  2. CPP 文件内注册

    classname.cpp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 不要遗漏这一句
    QX_REGISTER_CPP_EXPORT_DLL(ClassName)

    namespace qx
    {
    template <> void register_class(QxClass<Category>& t)
    {
    // 依次声明所有要在数据表中存储的属性
    // 如不声明,则不存储
    // 如果有父类,无需在此声明父类的属性
    }
    }

QxORM 自动更新创建时间和更新时间 created_at updated_at

使用 Trigger 触发器实现,使用父类来减少重复代码。

先定义一个 BaseEntity 类,然后依次定义 idcreated_atupdated_at 字段,再添加触发器:

  • onBeforeInsert 插入前更新 created_atQDateTime::currentDateTime()
  • onBeforeUpdate 更新前更新 updated_atQDateTime::currentDateTime()

示例代码:

base_entity.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#ifndef BASE_ENTITY_H
#define BASE_ENTITY_H
#include <QxOrm.h>

class BaseEntity
{
public:
BaseEntity();

long id;
QDateTime created_at;
QDateTime updated_at;

void onBeforeInsert(qx::dao::detail::IxDao_Helper * dao);
void onBeforeUpdate(qx::dao::detail::IxDao_Helper * dao);

};

QX_REGISTER_HPP_EXPORT_DLL(BaseEntity, qx::trait::no_base_class_defined, 1)

namespace qx {
namespace dao {
namespace detail {

template <>
struct QxDao_Trigger<BaseEntity>
{
static inline void onBeforeInsert(BaseEntity * t, qx::dao::detail::IxDao_Helper * dao)
{ if (t) { t->onBeforeInsert(dao); } }
static inline void onBeforeUpdate(BaseEntity * t, qx::dao::detail::IxDao_Helper * dao)
{ if (t) { t->onBeforeUpdate(dao); } }
static inline void onBeforeDelete(BaseEntity * t, qx::dao::detail::IxDao_Helper * dao)
{ Q_UNUSED(t); Q_UNUSED(dao); }
static inline void onBeforeFetch(BaseEntity * t, qx::dao::detail::IxDao_Helper * dao)
{ Q_UNUSED(t); Q_UNUSED(dao); }
static inline void onAfterInsert(BaseEntity * t, qx::dao::detail::IxDao_Helper * dao)
{ Q_UNUSED(t); Q_UNUSED(dao); }
static inline void onAfterUpdate(BaseEntity * t, qx::dao::detail::IxDao_Helper * dao)
{ Q_UNUSED(t); Q_UNUSED(dao); }
static inline void onAfterDelete(BaseEntity * t, qx::dao::detail::IxDao_Helper * dao)
{ Q_UNUSED(t); Q_UNUSED(dao); }
static inline void onAfterFetch(BaseEntity * t, qx::dao::detail::IxDao_Helper * dao)
{ Q_UNUSED(t); Q_UNUSED(dao); }

};

} // namespace detail
} // namespace dao
} // qx

#endif // BASE_ENTITY_H
base_entity.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include "base_entity.h"

BaseEntity::BaseEntity() {}

QX_REGISTER_CPP_EXPORT_DLL(BaseEntity)

namespace qx
{
template <> void register_class(QxClass<BaseEntity>& t)
{
t.id(&BaseEntity::id, "id");
t.data(&BaseEntity::created_at, "created_at");
t.data(&BaseEntity::updated_at, "updated_at");
}
}


void BaseEntity::onBeforeInsert(qx::dao::detail::IxDao_Helper * dao) {
created_at = QDateTime::currentDateTime();

}

void BaseEntity::onBeforeUpdate(qx::dao::detail::IxDao_Helper * dao) {
updated_at = QDateTime::currentDateTime();
}

我们真的需要 bi-directional 关系吗?

PaperAuthor 的多对多关系里,惯性的思维是应该同时包含 Paper-->AuthorsAuthor-->papers 的双向多对多关系,这样才可以同时互相获得。

然而,在 C++ 中,这将导致循环包含,编译无法通过。

PaperAuthor 中间,我们还需要一个 AuthorToPaper 中间实体,当我们获取需要知道某篇文章的作者时,我们已经知道了该文章的 id,那我们可以很容易的拿到 AuthorToPaper实体,反之亦然。

所以,从原先的双向多对多关系,变成了两个单向多对一关系,只是添加了一个需要接触的中间实体。

QxORM Relation

在定义 Relation 时,无需手动定义外联的列,如 category_id 等,只需要在声明实体结构的时候,手动指定这个列

1
t.relationManyToOne(&Category::parent, "parent_id");

这样之后,就会自动生成 parent_id。

在实际操作两个对象让他们关联的时候,直接使用 a->B = b 即可

QxORM 插入

在使用 qx::dao::insert(T &T) 的时候,注意其中是引用传递,所以插入数据库后,更新的 ID 也会同步到变量中。

Qt 插槽与信号

插槽Slots 函数 是用于处理 信号函数 的函数。类似于 监听器 <-> 事件 系统,插槽作为监听器,信号作为事件。

注意 Signal 函数和 Slots 函数相对应的签名必须一致,包括形参的 const 等修饰符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Handler : public QObject  
{
// 使用宏,使得我们可以定义插槽函数
Q_OBJECT

private:
... *emitter;

public:
Handler(QObject *parent = nullptr) : QObject(parent) {
// 某个会发出信号的类,不要忘记参数 this
manager = new ...(this);
// 将 manager 发出的 信号 连接到当前对象的 槽函数
// (发射器, 信号函数, 接收器this, 插槽函数)
connect(emitter, &...:signalFunc, this, &Handler::slotFunc);
}

private slots:
// slots 代表这里面的是插槽函数
// 注意 slotFunc 的签名应该和 &...:signal 保持一致
// 这样之后,当 ... 发出 signal 的时候,就会自动执行 slotFunc
void slotFunc(...) {
...
}

signals:
// signals 定义了信号
// 发射信号只需要一个语句:
// emit signalFunc(paper);
void signalFunc(...);

};

莫名其妙的 fetch_all 不存在问题

本来 QxORM 用的好好的,突然发现编译不通过了,一直报错 fetch all' is not a member 0f qx::dao'; did you mean qx::dao::throwable::fetch all?

差点就准备放弃继续开发了,还好最后看到问题时发生在 QxORM.h 头文件中的,猜想是源码可能被不小心修改了,所以重新下载了一遍源码并重新编译,成功解决了问题。

(其实可能并不需要重新编译,只是因为 QxORM/include 里的文件编译不通过,但编译后使用的应该是已经编译好的动态链接库)

Qt TreeView 鼠标悬停卡死 程序未响应

触发方法:

  1. 开启有道词典的划词、取词
  2. 鼠标悬停 TreeView 中的某一项等待两秒
  3. 程序未响应,可见后台内存逐渐攀升

Qt QTreeWidget控件造成程序不响应,内存泄露_qtreewidgetitem中的子节点点击后程序崩溃-CSDN博客

QAbstractNativeEventFilter 的签名和文中的时候有变动,需要修改

main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include "mainwindow.h"

#include <QApplication>
#include <qt_windows.h>
#include <QAbstractNativeEventFilter>

class MyFilter : public QAbstractNativeEventFilter
{
public:
bool nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result) override
{
MSG* msg = (MSG*)message;
if (msg->message == WM_GETOBJECT)
return true;
return false;
}
};

int main(int argc, char *argv[])
{
QApplication a(argc, argv);
a.installNativeEventFilter(new MyFilter());
MainWindow w;
w.show();
return a.exec();
}

解耦设计思路

在编辑某个实体的信息的时候,是否应该交由对话框类修改和保存实体呢?还是说对话框类仅仅用于返回所需要的数据?

假设:

  1. 对话框类负责修改和保存实体:那在呼出对话框的时候,就应该将所有所需的信息传入对话框中,在对话框编辑完成后,还需要根据编辑的结果更新主窗口的信息。比如在添加一个类别后,我们如何知道这个类别是归属于哪个父类的,这个信息也应该随着对话框的呼出传入。
  2. 如果只是返回所需的信息。这个思想很像 MVC 架构,View 层只负责收集数据,一切后台操作都交由 Controller 和 Model 层。但是在桌面应用程序中,这样是否过于麻烦?矫枉过正?

和文心一言讨论后,暂时决定使用 2 来设计,感觉是一个比较稳妥的方法,就是编码比较麻烦。

无法修改 paper->year

奇奇怪怪的坑,本来是在一个函数内调整 p->year 然后外部再用 QSring::number 方法来转为字符串,结果编译时竟然直接跳过了 p->year 这一行,但在 QString::number 中手动指定 base 参数后又可以了。貌似是由于 QtCreator 构建的时候没有清除旧的编译文件导致的。

member access into incomplete type QMimeData

要加入头文件 <QMimeData>