最近在学习使用C++下的ORM库——ODB,来抽象对数据库的CURD,由于C++的ORM实在是太冷门了,ODB除了官方英语文档,几乎找不到其他好用的资料,所以在使用过程中也是遇到很多疑惑,也解决很多问题。近期遇到的一个源码上的bug更是折腾了我很久。写个博客记录一下。
问题描述
举个官方的例子,现在有两张表employees和employers(雇员和雇主),每一个employee行对应一个employer行,模型可以这样设计(省略了一些冗余的代码)
#include <vector>
#include <odb/lazy-ptr.hxx>
#pragma db object
class Employee // 雇员
{
...
#pragma db id auto
unsigned long id;
#pragma db not_null
odb::lazy_shared_ptr<Employer> employer; // 一对一关系
};
#pragma db object
class Employer // 雇主
{
...
#pragma db id auto
unsigned long id;
#pragma db value_not_null
std::vector<lazy_weak_ptr<Employee>> employees_ // 一对多关系
};
这里使用lazy_shared_ptr
是为了延迟加载,考虑到如果直接使用shared_ptr
指针,Employer
的数据量比较大的话,每次读取一个employee
,就会自动加载对应的employer
,非常耗费时间和内存,但是我们又不一定会用到employer
。虽然在一对一关系的情况下勉强还能接受,但是如果是一对多关系,那预加载的数据量可能很庞大,吃力不讨好。所以官方提供了lazy-ptr
延迟指针帮我们解决这个问题,lazy_shared_ptr
指向的对应的employer
不会读取employee
时立即加载,而是在我们需要时,使用load()
方法显式地加载。这种方式更合理。
unsigned long search_id = 52;
std::shared_ptr<employee> epee(db->load<employee>(search_id));
cout << epee->id << endl;
... // 一系列操作之后
epee->employer.load(); // 延迟加载
cout << epee->employer.name << endl;
到这一步以为一切都很顺利,直到编译代码的时候,问题出现了。
/usr/include/odb/lazy-ptr.ixx:1153:10: error: no match for ‘operator=’ (operand types are ‘std::shared_ptr<Employer>’ and ‘odb::object_traits<Employer>::pointer_type’ {aka ‘Employer*’})
1153 | p_ = i_.template load<T> (true); // Reset id.
| ~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from /usr/include/c++/10/memory:84,
from driver.cxx:4:
/usr/include/c++/10/bits/shared_ptr.h:358:19: note: candidate: ‘std::shared_ptr<_Tp>& std::shared_ptr<_Tp>::operator=(const std::shared_ptr<_Tp>&) [with _Tp = Employer]’
358 | shared_ptr& operator=(const shared_ptr&) noexcept = default;
| ^~~~~~~~
/usr/include/c++/10/bits/shared_ptr.h:358:29: note: no known conversion for argument 1 from ‘odb::object_traits<Employer>::pointer_type’ {aka ‘Employer*’} to ‘const std::shared_ptr<Employer>&’
358 | shared_ptr& operator=(const shared_ptr&) noexcept = default;
| ^~~~~~~~~~~~~~~~~
/usr/include/c++/10/bits/shared_ptr.h:362:2: note: candidate: ‘template<class _Yp> std::shared_ptr<_Tp>::_Assignable<const std::shared_ptr<_Yp>&> std::shared_ptr<_Tp>::operator=(const std::shared_ptr<_Yp>&) [with _Yp = _Yp; _Tp = Article]’
362 | operator=(const shared_ptr<_Yp>& __r) noexcept
| ^~~~~~~~
解决问题
看错误提示,应该是shared_ptr的问题,我一开始一头雾水,去网上查了好多资料,也没人提出同样的问题(想不到ODB真就这么冷门)。查资料查了半天无果,决定从源码入手解决问题。
问题出在源码中/usr/include/odb/lazy-ptr.ixx
的第1153行代码
template <class T>
inline std::shared_ptr<T> lazy_shared_ptr<T>::
load () const
{
if (!p_ && i_)
p_ = i_.template load<T> (true); // Reset id. 就是这一行代码
return p_;
}
可以看出p_
的类型应该是std::shared_ptr<T>
,检测i_.template load<T> (true);
发现它的返回值是T*
指针类型,输出的错误告诉我们找不到将T*
指针类型赋值给std::shared_ptr<T>
的操作。很明显,不能将一个原始指针直接赋值给一个智能指针(这个可以在各种博客和教程上查到),所以程序编译不通过,正确的做法应该是使用reset()
方法,修改代码如下
template <class T>
inline std::shared_ptr<T> lazy_shared_ptr<T>::
load () const
{
if (!p_ && i_)
//p_ = i_.template load<T> (true); // Reset id.
p_.reset(i_.template load<T> (true)); // Reset id.
return p_;
}
重新编译代码,编译成功,问题解决!
/usr/include/odb/lazy-ptr.ixx
第1551行也有同样的问题,可以一起改了。
总结
由于我使用的版本是2.4.0,是2015年发布的版本,当时C++11可能还有许多问题没有完善,导致这个版本也有对应的bug(比如我现在遇到的这个bug,不知道是不是因为当时shared_ptr
还没有规定不能直接被原生指针赋值的问题),我看到现在2.5.0已经在测试了(2022年的),相信这里提到的问题应该能在2.5.0版本得到解决。考虑到2.5.0版本还没正式发布,我还是把2.4.0版本可能会遇到的这个bug贴出来,避免其他小伙伴掉坑里。