MySQL SOCI 库类型问题

摘要

获取 SOCI 库的查询结果时,如果转换的字段没有精确匹配,会导致异常。

字段类型可以从 column_properties 中获取。

row r;
const column_properties & props = r.get_properties(i);
props.get_data_type(); // 字段类型

字段类型 soci::data_type 与 C++ 类型对应关系如下:

SOCI Data Typerow::get<T> specialization
dt_doubledouble
dt_integerint
dt_long_longlong long
dt_unsigned_long_longunsigned long long
dt_stringstd::string
dt_datestd::tm

参考文档:Data Types

与数据库对应关系:

MySQL Data TypeSOCI Data Typerow::get<T> specializations
FLOAT, DOUBLE, DECIMAL and synonymsdt_doubledouble
TINYINT, TINYINT UNSIGNED, SMALLINT, SMALLINT UNSIGNED, INTdt_integerint
INT UNSIGNEDdt_long_longlong long or unsigned
BIGINTdt_long_longlong long
BIGINT UNSIGNEDdt_unsigned_long_longunsigned long long
CHAR, VARCHAR, BINARY, VARBINARY, TINYBLOB, MEDIUMBLOB, BLOB,LONGBLOB, TINYTEXT, MEDIUMTEXT, TEXT, LONGTEXT, ENUMdt_stringstd::string
TIMESTAMP (works only with MySQL >= 5.0), DATE, TIME, DATETIMEdt_datestd::tm

参考文档:MySQL Backend Reference

问题描述

从 SOCI 的 MySQL 库的查询结果 soci::row 中获取字段的值时,需要使用模板参数指定字段的类型。

template <typename T>
T get(std::size_t pos) const

如果模板参数指定的字段类型与库内部解析的结果不一致,会抛出 bad_cast::bad_cast 异常。

出现问题的表结构如下:

CREATE TABLE `t_enum` (
  `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID',
  `tag` VARCHAR(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '筛选维度',
  `name` VARCHAR(120) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '筛选值名称',
  `value` INT(11) UNSIGNED NOT NULL DEFAULT '0' COMMENT '筛选值',
  PRIMARY KEY (`id`),
  KEY `idx_enum` (`tag`,`name`)
) ENGINE=InnoDB AUTO_INCREMENT=49 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='枚举字段表'

查询语句:

SELECT `tag`, `name`, `value` FROM stt_stream.t_enum;

解析的代码如下:

std::string tag = cit->get<std::string>(0);
std::string name = cit->get<std::string>(1);
int value = cit->get<int>(2);

在运行过程中会捕获到异常 bad_cast::bad_cast 并打印错误日志。

排查步骤

众所周知,C++ 模板的类型要求精确匹配,所以一开始就怀疑到了字段类型的问题上。

获取 value 字段时,分别尝试了 int64_tuint64_t 后,仍然有类型错误的异常。

查阅 SOCI 库的文档,也并未找到数据库字段类型与 C++ 类型的映射关系。

中途怀疑是其他地方的代码有问题,走了一些弯路。

最终查看了 SOCI 库的源代码,找到了类型转换的一个坑。

问题原因

SOCI 将不同类型的数据库后端封装成了统一的接口。 对于 MySQL, SOCI 依赖官方的 MySQL C 库。

src/backends/mysql/statement.cpp 中的 describe_column 函数,通过 MySQL 的 mysql_fetch_field_direct 获取表字段属性并判断类型。

MYSQL_FIELD *field = mysql_fetch_field_direct(result_, pos);
switch (field->type)
{
    case FIELD_TYPE_LONG:       //MYSQL_TYPE_LONG:
        type = field->flags & UNSIGNED_FLAG ? dt_long_long : dt_integer;
        break;
    case FIELD_TYPE_LONGLONG:   //MYSQL_TYPE_LONGLONG:
        type = field->flags & UNSIGNED_FLAG ? dt_unsigned_long_long : dt_long_long;
        break;
}

可以看到,对于数据库中原始类型为 MYSQL_TYPE_LONG 并且有 UNSIGNED_FLAG 的字段,SOCI 会当做 dt_long_long 处理,也就是需要转换成 long long 类型。

一般来说,在 64 位系统上,int32_tint 的别名,int64_tlong 的别名。long long 长度和 long 一样都是 8 字节,但它们是不同的两个类型。由于模板参数必须精确匹配类型,所以 int64_t 不能代替 long long

SOCI 使用基类 holder 保存字段,针对不同类型实现派生类 type_holder

使用 row::get<T>(pos) 获取字段值时,将基类 dynamic_cast 到对应类型 T,如果转换失败,会抛出异常。


类型转换的代码很精妙,摘录如下:

template <typename T>
class type_holder;

class holder
{
public:
    holder() {}
    virtual ~holder() {}

    template<typename T>
    T get()
    {
        type_holder<T>* p = dynamic_cast<type_holder<T> *>(this);
        if (p)
        {
            return p->template value<T>();
        }
        else
        {
            throw std::bad_cast();
        }
    }

private:

    template<typename T>
    T value();
};

template <typename T>
class type_holder : public holder
{
public:
    type_holder(T * t) : t_(t) {}
    ~type_holder() { delete t_; }

    template<typename TypeValue>
    TypeValue value() const { return *t_; }

private:
    T * t_;
};
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值