一种基于 SQLite3 的半自动 C++ ORM 实现

1. 引言

在现代软件开发中,尤其是在后端系统与数据库交互的场景下,对象关系映射(Object-Relational Mapping, ORM)已成为一种主流的设计范式。ORM 的核心思想是将程序中的对象模型与关系型数据库中的表结构进行映射,使开发者能够以面向对象的方式操作数据,而无需直接编写繁琐且易错的 SQL 语句。这种抽象不仅显著提升了开发效率,也增强了代码的可维护性与可移植性。

尽管在对性能或控制力要求极高的场景中原生 SQL 仍不可替代,但在大多数常规业务系统中,一个设计良好的 ORM 层能有效简化数据持久化的实现。

然而,由于 C/C++ 是底层系统级语言,缺乏运行时反射、泛型擦除等高级特性,要实现一个全自动、功能完备的 ORM 框架(如 Java 的 Hibernate 或 Python 的 SQLAlchemy)极为复杂,甚至得不偿失。因此,本文提出一种半自动的 ORM 风格封装:它借鉴一些典型 ORM 的设计思想,但不追求完全自动化,而是通过合理的模板、枚举和工具函数,在保持轻量与可控的前提下,提供接近 ORM 的开发体验。

2. 实现

不必被 ORM 等看似复杂的概念所吓退。从底层视角出发,我们会发现:许多高级设计范式本质上是对底层重复、分散或冗余代码的封装与抽象------比如为了解决 SQL 模板重复、数据映射逻辑不内聚等问题。

以常见的业务开发为例,我们经常需要对数据库执行 CRUD 操作(即 Create 创建、Retrieve 查询、Update 更新、Delete 删除)。这些操作在不同数据表上反复出现,代码结构高度相似,却往往被一遍遍手写。那么,是否有可能将这些重复逻辑提炼出来,仅通过统一的四个接口,就能满足所有实体的 CRUD 需求?这正是轻量级 ORM 尝试回答的问题。

2.1 字段值

先来看看以下代码封装的四个 CRUD 接口:InsertRowDeleteRowUpdateRow 以及 QueryRows

Sqlite3Crud.h

cpp 复制代码
#pragma once
#include <string>
#include <variant>
#include <vector>

#include "TableName.h"
#include "Util/Statement.hpp"

namespace Persistence {

/// @brief Sqlite3数据库封装类
namespace Sqlite3Crud {

using TableValue = std::variant<std::monostate, std::string, int64_t>;  //表的值

//数据库表插入记录的抽象方法
bool InsertRow(const char *sql, const std::vector<TableValue> &params);

template <typename Field>
bool Insert(TableName tableName, const std::vector<Field> &fieldNames,
            const std::vector<Sqlite3Crud::TableValue> &fieldValues) {
  return InsertRow(Util::Statement::Insert(tableName, fieldNames).c_str(),
                   fieldValues);
}

//数据库表删除记录的抽象方法
bool DeleteRow(const char *sql, const TableValue &bindParam);

template <typename Field>
bool Delete(TableName tableName, const Field &fieldName,
            const TableValue &bindParam) {
  return DeleteRow(Util::Statement::Delete(tableName, fieldName).c_str(),
                   bindParam);
}

//数据库表更新记录的抽象方法
bool UpdateRow(const char *sql, const std::vector<TableValue> &bindParams);

template <typename Field>
bool Update(TableName tableName, const std::vector<Field> &fieldNames,
            const std::vector<Sqlite3Crud::TableValue> &fieldValues) {
  return Sqlite3Crud::UpdateRow(
      Util::Statement::Update(tableName, fieldNames).c_str(), fieldValues);
}

//数据库表查询记录的抽象方法
bool QueryRows(const char *sql, std::vector<TableValue> &result,
               const std::vector<TableValue> &bindParams = {});

template <typename Field>
bool Query(
    TableName tableName, const std::vector<Field> &fieldNames,
    std::vector<TableValue> &result, const std::vector<Field> &whereFields = {},
    const std::vector<TableValue> &whereBindParams = {},
    std::optional<Field> orderByField = std::nullopt,
    std::optional<Util::Statement::OrderByValue> orderByValue = std::nullopt) {
  if (whereFields.size() != whereBindParams.size()) {
    throw std::invalid_argument("whereFields 和 whereBindParams 数量不匹配!");
  }
  const std::string &statement = Util::Statement::Query(
      tableName, fieldNames, whereFields, orderByField, orderByValue);
  return QueryRows(statement.c_str(), result, whereBindParams);
}

};  // namespace Sqlite3Crud

}  // namespace Persistence

Sqlite3Crud.cpp

cpp 复制代码
#include "Sqlite3Crud.h"

#include <sqlite3.h>

#include <iostream>

#include "Bootstrap/App.h"

using namespace std;

namespace Persistence {

namespace Sqlite3Crud {

bool InsertRow(const char *sql, const std::vector<TableValue> &params) {
  sqlite3 *db = App::GetInstance().Db();

  // 准备 SQL 语句
  sqlite3_stmt *stmt;
  if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
    std::cerr << (const char *)(u8"SQL准备语句失败:") << sqlite3_errmsg(db)
              << std::endl;
    return false;
  }

  // 绑定参数
  for (size_t si = 0; si < params.size(); ++si) {
    int bindId = (int)(si + 1);

    // 使用 std::holds_alternative 检查当前是否存储了指定类型的值
    if (std::holds_alternative<std::string>(params[si])) {
      const auto &param = std::get<std::string>(params[si]);
      if (sqlite3_bind_text(stmt, bindId, param.c_str(), -1, SQLITE_STATIC) !=
          SQLITE_OK) {
        std::cerr << (const char *)(u8"绑定错误: ") << sqlite3_errmsg(db)
                  << std::endl;
        sqlite3_finalize(stmt);
        return false;
      }
    } else if (std::holds_alternative<int64_t>(params[si])) {
      const auto &param = std::get<int64_t>(params[si]);
      if (sqlite3_bind_int64(stmt, bindId, param) != SQLITE_OK) {
        std::cerr << (const char *)(u8"绑定错误: ") << sqlite3_errmsg(db)
                  << std::endl;
        sqlite3_finalize(stmt);
        return false;
      }
    } else {
      return false;
    }
  }

  // 执行插入语句
  if (sqlite3_step(stmt) != SQLITE_DONE) {
    std::cerr << (const char *)(u8"执行失败: ") << sqlite3_errmsg(db)
              << std::endl;
    sqlite3_finalize(stmt);
    return false;
  }

  sqlite3_finalize(stmt);  // 释放SQL语句对象
  return true;
}

bool DeleteRow(const char *sql, const TableValue &bindParam) {
  sqlite3 *db = App::GetInstance().Db();

  // 准备 SQL 语句
  sqlite3_stmt *stmt;
  int exit = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr);
  if (exit != SQLITE_OK) {
    std::cerr << (const char *)(u8"SQL准备语句失败:") << sqlite3_errmsg(db)
              << std::endl;
    return false;
  }

  // 绑定参数
  if (std::holds_alternative<std::string>(bindParam)) {
    const auto &param = std::get<std::string>(bindParam);
    if (sqlite3_bind_text(stmt, 1, param.c_str(), -1, SQLITE_STATIC) !=
        SQLITE_OK) {
      std::cerr << (const char *)(u8"绑定错误: ") << sqlite3_errmsg(db)
                << std::endl;
      sqlite3_finalize(stmt);
      return false;
    }
  } else if (std::holds_alternative<int64_t>(bindParam)) {
    const auto &param = std::get<int64_t>(bindParam);
    if (sqlite3_bind_int64(stmt, 1, param) != SQLITE_OK) {
      std::cerr << (const char *)(u8"绑定错误: ") << sqlite3_errmsg(db)
                << std::endl;
      sqlite3_finalize(stmt);
      return false;
    }
  }

  // 执行语句
  exit = sqlite3_step(stmt);
  if (exit != SQLITE_DONE) {
    std::cerr << (const char *)(u8"执行失败: ") << sqlite3_errmsg(db)
              << std::endl;
    sqlite3_finalize(stmt);
    return false;
  }

  sqlite3_finalize(stmt);  // 释放SQL语句对象
  return true;
}

// 从 bindParams 绑定所有参数
static bool BindParams2Stmt(const std::vector<TableValue> &bindParams,
                            sqlite3_stmt *stmt, sqlite3 *db) {
  for (size_t si = 0; si < bindParams.size(); ++si) {
    int bindIndex = static_cast<int>(si + 1);  // SQLite 参数从 1 开始
    const auto &bindParam = bindParams[si];

    // 使用 std::holds_alternative 检查当前是否存储了指定类型的值
    if (std::holds_alternative<std::string>(bindParam)) {
      const auto &param = std::get<std::string>(bindParam);
      if (sqlite3_bind_text(stmt, bindIndex, param.c_str(), -1,
                            SQLITE_STATIC) != SQLITE_OK) {
        std::cerr << "绑定错误: " << sqlite3_errmsg(db) << std::endl;
        sqlite3_finalize(stmt);
        return false;
      }
    } else if (std::holds_alternative<int64_t>(bindParam)) {
      const auto &param = std::get<int64_t>(bindParam);
      if (sqlite3_bind_int64(stmt, bindIndex, param) != SQLITE_OK) {
        std::cerr << "绑定错误: " << sqlite3_errmsg(db) << std::endl;
        sqlite3_finalize(stmt);
        return false;
      }
    } else {
      std::cerr << "不支持的绑定参数类型" << std::endl;
      sqlite3_finalize(stmt);
      return false;
    }
  }

  return true;
}

bool UpdateRow(const char *sql, const std::vector<TableValue> &bindParams) {
  sqlite3 *db = App::GetInstance().Db();

  // 准备SQL语句
  sqlite3_stmt *stmt;
  if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
    std::cerr << (const char *)(u8"SQL准备语句失败: ") << sqlite3_errmsg(db)
              << std::endl;
    return false;
  }

  if (!BindParams2Stmt(bindParams, stmt, db)) {
    return false;
  }

  // 执行语句
  if (sqlite3_step(stmt) != SQLITE_DONE) {
    std::cerr << (const char *)(u8"执行失败: ") << sqlite3_errmsg(db)
              << std::endl;
    sqlite3_finalize(stmt);
    return false;
  }

  // 清理
  sqlite3_finalize(stmt);
  return true;
}

bool QueryRows(const char *sql, std::vector<TableValue> &result,
               const std::vector<TableValue> &bindParams) {
  sqlite3 *db = App::GetInstance().Db();

  // 准备SQL语句
  sqlite3_stmt *stmt;
  if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
    std::cerr << (const char *)(u8"SQL准备语句失败: ") << sqlite3_errmsg(db)
              << std::endl;
    return false;
  }

  if (!BindParams2Stmt(bindParams, stmt, db)) {
    return false;
  }

  // 执行SQL语句并存储结果
  while (sqlite3_step(stmt) == SQLITE_ROW) {
    int colCount = sqlite3_column_count(stmt);
    for (int i = 0; i < colCount; ++i) {
      switch (sqlite3_column_type(stmt, i)) {
        case SQLITE_TEXT: {
          result.emplace_back(
              reinterpret_cast<const char *>(sqlite3_column_text(stmt, i)));
          break;
        }
        case SQLITE_INTEGER: {
          result.emplace_back(sqlite3_column_int64(stmt, i));
          break;
        }
        case SQLITE_NULL: {
          result.emplace_back(std::monostate{});
          break;
        }
        case SQLITE_FLOAT:
        case SQLITE_BLOB:
        default: {
          std::cerr << (const char *)(u8"执行失败: ") << sqlite3_errmsg(db)
                    << std::endl;
          sqlite3_finalize(stmt);
          return false;
        }
      }
    }
  }

  sqlite3_finalize(stmt);  // 释放SQL语句对象
  return true;
}

}  // namespace Sqlite3Crud

}  // namespace Persistence

Sqlite3 的 CURD 的操作都差不多,都是编译 statement ,然后绑定参数,最后执行。但是要实现统一的 CURD 接口的封装,一个关键的地方就在于传参表格字段值类型。如果是 JavaScript、Python 这样的编程语言好办,它们是弱类型语言,完全可以支持。作为强类型语言,C++也有办法,那就是使用 C++17 提供的 std::variant:

cpp 复制代码
using TableValue = std::variant<std::monostate, std::string, int64_t>;  //表的值

这里定义了一个类型别名 TableValue,它是一个 std::variant 类型,可以持有以下三种类型的值:std::monostatestd::stringint64_t 。其中 std::monostate 是一个空类型(empty struct),常用于 std::variant 中表示"无值"或"空状态"。当需要绑定参数到 SQL 语句时,再将 TableValue 拆箱成具体的值:

cpp 复制代码
// 从 bindParams 绑定所有参数
static bool BindParams2Stmt(const std::vector<TableValue> &bindParams,
                            sqlite3_stmt *stmt, sqlite3 *db) {
  for (size_t si = 0; si < bindParams.size(); ++si) {
    int bindIndex = static_cast<int>(si + 1);  // SQLite 参数从 1 开始
    const auto &bindParam = bindParams[si];

    // 使用 std::holds_alternative 检查当前是否存储了指定类型的值
    if (std::holds_alternative<std::string>(bindParam)) {
      const auto &param = std::get<std::string>(bindParam);
      if (sqlite3_bind_text(stmt, bindIndex, param.c_str(), -1,
                            SQLITE_STATIC) != SQLITE_OK) {
        std::cerr << "绑定错误: " << sqlite3_errmsg(db) << std::endl;
        sqlite3_finalize(stmt);
        return false;
      }
    } else if (std::holds_alternative<int64_t>(bindParam)) {
      const auto &param = std::get<int64_t>(bindParam);
      if (sqlite3_bind_int64(stmt, bindIndex, param) != SQLITE_OK) {
        std::cerr << "绑定错误: " << sqlite3_errmsg(db) << std::endl;
        sqlite3_finalize(stmt);
        return false;
      }
    } else {
      std::cerr << "不支持的绑定参数类型" << std::endl;
      sqlite3_finalize(stmt);
      return false;
    }
  }

  return true;
}

2.2 字段名

仔细观察 InsertRowDeleteRowUpdateRow 以及 QueryRows 的实现,虽然它们通过 TableValue 解决了字段值类型多样的传参问题,但仍然要求调用者传入完整的 SQL 字符串。这意味着业务代码仍需手动拼接表名、字段名和占位符,不仅重复繁琐,还容易引入语法错误或 SQL 注入风险(尽管使用参数绑定可缓解后者)。

更重要的是,表结构本身是静态的、已知的:比如一篇博客文章对应 posts 表,包含 id、title、content 等字段。如果每次插入都要写 "INSERT INTO posts (title, content) VALUES (?, ?)",那么当表结构变更时,所有相关 SQL 字符串都需同步修改,违反了"一处修改,处处生效"的工程原则。

因此,理想的做法是:让 CRUD 接口接受高层语义信息(如表名、字段名列表),由底层自动构造对应的 SQL 语句。这样,业务层只需关注"我要插入哪些字段",而无需关心具体的 SQL 语法细节。这不仅提升了开发效率,也增强了系统的内聚性与可维护性。

基于 ORM 的思想,一个简单的想法是可以将数据库表映射成类/结构体对象,通过反射特性获取类/结构体对象的成员信息,进而知道数据库表名和字段名,实现自动构造语句。如果是 Java 或者 C# 这样的高级托管语言就没有问题,因为它们支持反射(Reflection)特性。对于 C++ ,可以引入 magic_enum 库,将枚举值(enum)转换为对应的字符串名称。参看工具接口Util::Statement 的实现:

Statement.hpp:

cpp 复制代码
#pragma once

#include <format>
#include <magic_enum/magic_enum.hpp>
#include <optional>
#include <string>

#include "Persistence/TableName.h"

namespace Util {

namespace Statement {

using namespace Persistence;

enum class OrderByValue { ASC, DESC };

template <typename Field>
std::string Insert(TableName tableName, const std::vector<Field>& fieldNames) {
  size_t num = fieldNames.size();
  if (num == 0) {
    throw std::invalid_argument("插入数据缺少插入项!");
  }

  std::string columnList = "(";           //列列表
  std::string valuesClause = "VALUES (";  // values子句
  for (size_t i = 0; i < num; ++i) {
    columnList += magic_enum::enum_name(fieldNames.at(i));
    valuesClause += "?";
    if (i != num - 1) {
      columnList += ", ";
      valuesClause += ", ";
    }
  }
  columnList += ")";
  valuesClause += ")";

  //插入子句
  std::string insertClause = std::format(
      "INSERT INTO {} {}", magic_enum::enum_name(tableName), columnList);

  return std::format("{} {};", insertClause, valuesClause);
}

template <typename Field>
std::string Delete(TableName tableName, const Field& fieldName) {
  std::string deleteClause = "DELETE";

  std::string fromClause =
      std::format("FROM {}", magic_enum::enum_name(tableName));

  std::string whereClause =
      std::format("WHERE {} = ?", magic_enum::enum_name(fieldName));

  return std::format("{} {} {};", deleteClause, fromClause, whereClause);
}

template <typename Field>
std::string Update(TableName tableName, const std::vector<Field>& fieldNames) {
  size_t num = fieldNames.size();
  if (num <= 1) {
    throw std::invalid_argument("更新数据至少要两列值!");
  }

  std::string updateClause =
      std::format("UPDATE {}", magic_enum::enum_name(tableName));

  std::string setClause = "SET ";
  for (size_t i = 0; i < num - 1; ++i) {
    setClause += magic_enum::enum_name(fieldNames[i]);
    setClause += " = ?";
    if (i != num - 2) {
      setClause += ", ";
    }
  }

  std::string whereClause =
      std::format("WHERE {} = ?", magic_enum::enum_name(fieldNames[num - 1]));

  return std::format("{} {} {};", updateClause, setClause, whereClause);
}

template <typename Field>
std::string Query(TableName tableName, const std::vector<Field>& fieldNames,
                  const std::vector<Field>& whereFields = {},
                  std::optional<Field> orderByField = std::nullopt,
                  std::optional<OrderByValue> orderByValue = std::nullopt) {
  if (fieldNames.empty()) {
    throw std::invalid_argument("query sql fieldNames must not be empty");
  }

  size_t num = fieldNames.size();

  //
  std::string columnList;  //列列表
  for (size_t i = 0; i < num; ++i) {
    columnList += magic_enum::enum_name(fieldNames.at(i));
    if (i != num - 1) {
      columnList += ", ";
    }
  }
  std::string selectClause = std::format("SELECT {}", columnList);

  std::string fromClause =
      std::format("FROM {}", magic_enum::enum_name(tableName));
  std::string statement = std::format("{} {}", selectClause, fromClause);

  // 添加 WHERE 子句(支持多个条件)
  if (!whereFields.empty()) {
    std::string whereClause = "WHERE ";

    for (size_t i = 0; i < whereFields.size(); ++i) {
      whereClause += magic_enum::enum_name(whereFields[i]);
      whereClause += " = ?";
      if (i != whereFields.size() - 1) {
        whereClause += " AND ";
      }
    }
    statement = std::format("{} {}", statement, whereClause);
  }

  if (orderByField.has_value() && orderByValue.has_value()) {
    std::string orderByClause = std::format(
        "ORDER BY {} {}", magic_enum::enum_name(orderByField.value()),
        magic_enum::enum_name(orderByValue.value()));
    statement = std::format("{} {}", statement, orderByClause);
  }

  return std::format("{};", statement);
}

}  // namespace Statement

}  // namespace Util

利用模板特性,接口InsertDeleteUpdateQuery可以接受不同的数据库表的枚举类映射对象。例如博文表TableBlogsField.h

cpp 复制代码
#pragma once

namespace Persistence {

enum class TableBlogsField {
  id,
  title,
  content,
  summary,
  cover_image_url,
  created_at,
  updated_at,
  is_draft
};

}

分类专栏表TableCategoriesField.h

cpp 复制代码
#pragma once

namespace Persistence {

enum class TableCategoriesField {
  id,
  title,
  description,
  image_url,
  parent_id,
  created_at
};

}

TableBlogsFieldTableCategoriesField 会在 InsertDeleteUpdateQuery 的模板 template <typename Field> 中展开成字符串,实现 SQL 字符串的拼接;并被 Sqlite3CrudInsertDeleteUpdateQuery 所调用,形成更加通用的接口。这样,业务代码只需操作语义清晰的枚举字段,而无需硬编码任何 SQL 字符串。这种基于枚举与模板的"编译期反射"机制,在 C++ 缺乏原生反射能力的前提下,提供了一种轻量、类型安全且易于维护的 ORM 风格接口。

2.3 调用

当然,在 Sqlite3CrudInsertDeleteUpdateQuery 接口中,还需要知道数据库名,这也是枚举类TableName.h

cpp 复制代码
#pragma once

namespace Persistence {

enum class TableName {
  users,
  tags,
  blog_tags,
  categories,
  category_blog,
  blog_views,
  blog_likes,
  friend_links,
  blogs
};

}

如果是从博文表中查询数据:

cpp 复制代码
vector<Sqlite3Crud::TableValue> result;
if (!Sqlite3Crud::Query<TableBlogsField>(
        TableName::blogs,
        {TableBlogsField::title, TableBlogsField::content,
        TableBlogsField::summary, TableBlogsField::created_at,
        TableBlogsField::updated_at},
        result, {TableBlogsField::is_draft, TableBlogsField::id},
        {isDraft, blogId})) {
return false;
}

auto &blogMeta = blogData.blogMeta;
blogMeta.id = blogId;
blogMeta.title = std::move(std::get<std::string>(result.at(0)));
blogData.content = std::move(std::get<std::string>(result.at(1)));
blogMeta.summary = std::move(std::get<std::string>(result.at(2)));
blogMeta.createdTime = std::move(std::get<std::string>(result.at(3)));
blogMeta.updatedTime = std::move(std::get<std::string>(result.at(4)));

创建博文插入到博文表中:

cpp 复制代码
auto &blogMeta = blogData.blogMeta;
blogMeta.updatedTime = blogMeta.createdTime;

// 插入博文表
Sqlite3Crud::Insert<TableBlogsField>(
    TableName::blogs,
    {TableBlogsField::id, TableBlogsField::title, TableBlogsField::content,
    TableBlogsField::summary, TableBlogsField::created_at,
    TableBlogsField::updated_at, TableBlogsField::cover_image_url,
    TableBlogsField::is_draft},
    {blogId, std::move(blogMeta.title), std::move(blogData.content),
    std::move(blogMeta.summary), std::move(blogMeta.createdTime),
    std::move(blogMeta.updatedTime), std::move(blogMeta.coverAddress),
    std::move(blogMeta.isDraft)});

更新某篇博文数据:

cpp 复制代码
const auto &blogId = blogData.blogMeta.id;
auto &blogMeta = blogData.blogMeta;

if (!Sqlite3Crud::Update<TableBlogsField>(
        TableName::blogs,
        {TableBlogsField::title, TableBlogsField::content,
        TableBlogsField::summary, TableBlogsField::created_at,
        TableBlogsField::updated_at, TableBlogsField::cover_image_url,
        TableBlogsField::is_draft, TableBlogsField::id},
        {std::move(blogMeta.title), std::move(blogData.content),
        std::move(blogMeta.summary), std::move(blogMeta.createdTime),
        std::move(blogMeta.updatedTime), std::move(blogMeta.coverAddress),
        std::move(blogMeta.isDraft), blogId})) {
return false;
}

删除某篇博文:

cpp 复制代码
Sqlite3Crud::Delete<TableBlogsField>(
      TableName::blogs, TableBlogsField::id, {std::move(blogId)});

这种调用方式将数据库操作完全解耦于原始 SQL 字符串,不仅避免了手写 SQL 带来的拼写错误与注入风险,还使得表结构变更只需修改对应的枚举定义即可自动同步到所有相关操作中,显著提升了代码的安全性、可读性与可维护性。

3. 优化

3.1 编译期生成

虽然这样做已经非常自动化了,但是数据库表映射的枚举类对象可能并不能跟 SQLite3 数据库同步------因为这些枚举是在编译期静态定义的,而数据库表结构可能在运行时被外部工具(如迁移脚本、手动 ALTER TABLE 操作或不同版本的部署)动态修改。一旦实际数据库中的字段增删改与 C++ 枚举中定义的字段不一致,就会导致生成的 SQL 语句引用不存在的列、遗漏新增列,甚至因类型不匹配而引发运行时错误或数据丢失。

此外,这种"代码即 Schema"的方式缺乏对数据库真实状态的验证机制,无法在程序启动时自动检测表结构是否与枚举定义一致,从而埋下潜在的兼容性隐患。因此,在追求开发效率的同时,仍需辅以数据库版本管理(如迁移脚本)、启动时结构校验或构建阶段的代码生成工具(如从数据库 schema 自动生成枚举),才能真正实现类型安全与结构一致性的双重保障。

对 C++ 来说,比较好的方法就是在构建程序的时候通过代码生成工具从数据库 schema 自动生成枚举类。笔者的实现代码是:

cpp 复制代码
// Script/DbSchemaGenerator.cpp
#include <sqlite3.h>

#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>

#ifdef _WIN32
#include <Windows.h>
#endif

using namespace std;

//转换成帕斯卡命名
std::string ToPascalCase(const std::string& input) {
  if (input.empty()) {
    return "";
  }

  std::string result;
  bool nextUpper = true;  // 下一个有效字符应大写

  for (char c : input) {
    if (c == '_') {
      // 遇到下划线,下一个非下划线字母要大写
      nextUpper = true;
    } else {
      if (nextUpper) {
        result +=
            static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
        nextUpper = false;
      } else {
        result +=
            static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
      }
    }
  }

  // 如果结果为空(比如输入全是下划线),返回空串
  return result;
}

vector<string> QueryTableName(sqlite3* db) {
  vector<string> tableNames;

  // 获取所有用户表
  const char* sqlTables =
      "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE "
      "'sqlite_%';";

  sqlite3_stmt* stmtTables;
  int rc = sqlite3_prepare_v2(db, sqlTables, -1, &stmtTables, nullptr);

  if (rc != SQLITE_OK) {
    std::cerr << "Failed to fetch tables: " << sqlite3_errmsg(db) << "\n";
    return tableNames;
  }

  while (sqlite3_step(stmtTables) == SQLITE_ROW) {
    const char* tableNameCstr =
        reinterpret_cast<const char*>(sqlite3_column_text(stmtTables, 0));

    if (!tableNameCstr) continue;

    tableNames.emplace_back(tableNameCstr);
  }
  sqlite3_finalize(stmtTables);

  return tableNames;
}

string Read2String(filesystem::path& filePath) {
  std::ifstream infile(filePath);
  if (!infile) {
    return {};
  }
  return {(std::istreambuf_iterator<char>(infile)),
          std::istreambuf_iterator<char>()};
}

void WriteTableName(filesystem::path& tableNameFile,
                    const vector<string>& tableNames) {
  std::ostringstream memStream;

  memStream << "#pragma once\n";
  memStream << "\n";
  memStream << "namespace Persistence {\n";
  memStream << "\n";
  memStream << "enum class TableName {\n";

  for (size_t i = 0; i < tableNames.size(); ++i) {
    string line;
    if (i == tableNames.size() - 1) {
      line = std::format("  {}\n", tableNames[i]);
    } else {
      line = std::format("  {},\n", tableNames[i]);
    }
    memStream << line;
  }
  memStream << "};\n";
  memStream << "\n";
  memStream << "}";

  if (memStream.str() == Read2String(tableNameFile)) {
    return;
  }

  ofstream file(tableNameFile);
  if (!file) {
    std::cerr << "Failed to open file '" << tableNameFile.generic_string()
              << "' for writing.\n";
    return;
  }

  file << memStream.str();
}

vector<string> QueryFiledName(sqlite3* db, const string& tableName) {
  vector<string> filedNames;

  const string& sql = "PRAGMA table_info(" + tableName + ");";
  sqlite3_stmt* stmt;
  int rc = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr);
  if (rc != SQLITE_OK) {
    std::cerr << "Failed to get schema for table '" << tableName.c_str()
              << "': " << sqlite3_errmsg(db) << "\n";
    return filedNames;
  }

  while (sqlite3_step(stmt) == SQLITE_ROW) {
    const char* col_name = reinterpret_cast<const char*>(
        sqlite3_column_text(stmt, 1));  // 第1列是name

    if (col_name) {
      filedNames.emplace_back(col_name);
    }
  }

  sqlite3_finalize(stmt);

  return filedNames;
}

void WriteFiledName(filesystem::path& outSourceDir, const string& fileName,
                    const vector<string>& filedNames) {
  std::ostringstream memStream;

  memStream << "#pragma once\n";
  memStream << "\n";
  memStream << "namespace Persistence {\n";
  memStream << "\n";
  memStream << std::format("enum class {} {{\n", fileName);

  for (size_t i = 0; i < filedNames.size(); ++i) {
    string line;
    if (i == filedNames.size() - 1) {
      line = std::format("  {}\n", filedNames[i]);
    } else {
      line = std::format("  {},\n", filedNames[i]);
    }
    memStream << line;
  }
  memStream << "};\n";
  memStream << "\n";
  memStream << "}";

  filesystem::path filedNameFile = outSourceDir / (fileName + ".h");
  if (memStream.str() == Read2String(filedNameFile)) {
    return;
  }

  ofstream file(filedNameFile);
  if (!file) {
    std::cerr << "Failed to open file '" << filedNameFile.generic_string()
              << "' for writing.\n";
    return;
  }

  file << memStream.str();
}

int main(int argc, char* argv[]) {
#ifdef _WIN32
  SetConsoleOutputCP(65001);
#endif

  //
  if (argc != 3) {
    std::cerr << "Usage: " << argv[0]
              << " <database_path> <output_directory>\n";
    return 1;
  }

  //
  const char* dbPath = argv[1];
  const char* outputDir = argv[2];
  std::cout << "Generating DB schema enums...\n";
  std::cout << "  DB Path: " << dbPath << "\n";
  std::cout << "  Output : " << outputDir << "\n";
  filesystem::path outSourceDir{outputDir};

  sqlite3* db;
  int rc = sqlite3_open(dbPath, &db);

  if (rc != SQLITE_OK) {
    std::cerr << "Cannot open database: " << sqlite3_errmsg(db) << "\n";
    sqlite3_close(db);
    return 1;
  }

  vector<string> tableNames = QueryTableName(db);
  filesystem::path tableNameFile = outSourceDir / "TableName.h";
  WriteTableName(tableNameFile, tableNames);

  for (auto tableName : tableNames) {
    string fileName = "Table" + ToPascalCase(tableName) + "Field";
    WriteFiledName(outSourceDir, fileName, QueryFiledName(db, tableName));
  }

  sqlite3_close(db);

  return 0;
}

代码虽然很长,但是实现思路很简单:既然要保证 SQLite3 数据库中的表格数据与映射的枚举类一致,那就让这些枚举类代码在构建之前通过这个 Script/DbSchemaGenerator.cpp 工具生成就可以了,数据库表的名称和字段名都可以通过调用相应的 SQL 语句来实现。当然,这需要借助于构建系统,可参照《CMake 构建学习笔记 31-构建前执行可执行程序》

3.2 JOIN 查询

当前 Util::Statement::Query 只支持 单表查询(FROM table),那么复杂的 JOIN 查询如何实现呢?一般来说, JOIN 涉及多表、别名、复杂 SELECT 列等,无法直接用"字段枚举列表"简单表达,也很难用一个函数生成所有 JOIN SQL 。最好的办法是两种方案并行:

  1. 简单单表 CRUD : 继续用现有的实现。
  2. 复杂查询(含 JOIN / 子查询 / 聚合):允许手写 SQL 字符串,但复用你已有的 QueryRows 执行引擎。

在主流 Java 后端框架(Spring Data JPA、MyBatis)中,这种"简单操作用 ORM 自动生成,复杂查询允许手写 SQL"的混合策略是一种广泛采纳的最佳实践。其核心思想是:在开发效率与控制力之间取得平衡------既享受 ORM 带来的类型安全、代码简洁和快速开发优势,又不牺牲对复杂 SQL 场景的完全掌控能力。

4. 补充

笔者的这段实现只能算是半自动的 ORM 风格封装,没有实现全自动的对象与数据库表的映射(比如不能直接传一个 User 对象就自动插入),但它确实体现了一些 ORM 的典型设计思路:

ORM 特性 是否体现 说明
隐藏原始 SQL 字符串拼接 ✔️ 使用 Util::Statement 自动生成带占位符的 SQL
参数绑定防注入 ✔️ 全部使用 sqlite3_bind_*,安全
字段名类型安全 ✔️ 用枚举 + magic_enum 代替字符串 "name",避免拼错
CRUD 抽象为函数 ✔️ InsertRow, QueryRows 等提供统一接口
对象自动映射 ✖️ 仍需手动构造 vector,不能直接传对象
自动生成 SQL(基于对象) ✖️ SQL 由工具函数生成,但需显式指定字段列表

不同的编程语言有着各自适用的编程范式,盲目照搬其他生态的全自动 ORM 模式并不可取------它不仅可能引入不必要的性能开销,还可能与语言特性格格不入。归根结底,只要能以合理、安全且可维护的方式解决问题,就是好的设计。

相关推荐
闻缺陷则喜何志丹2 小时前
【C++组合数学】P8106 [Cnoi2021] 数学练习|普及+
c++·数学·洛谷·组合数学
ALex_zry2 小时前
C++ const成员函数详解:原理、应用与最佳实践
c++
SelectDB技术团队2 小时前
慢 SQL 诊断准确率 99.99%,天翼云基于 Apache Doris MCP 的 AI 智能运维实践
大数据·数据库·人工智能·sql·apache
hetao17338373 小时前
2025-12-22 hetao1733837的笔记
c++·笔记·算法
DeltaTime3 小时前
三 视图变换, 投影变换, 正交投影, 透视投影
c++·图形渲染
superman超哥3 小时前
仓颉Result类型的错误处理模式深度解析
c语言·开发语言·c++·python·仓颉
八月的雨季 最後的冰吻3 小时前
FFmepg-- 38-ffplay源码-缓冲区 audio_buf调试
c++·ffmpeg·音视频
会思考的猴子3 小时前
UE5 C++ 笔记 GameplayAbilitySystem人物角色
c++·笔记·ue5
ht巷子3 小时前
Qt:信号与槽
开发语言·c++·qt