orm基本概念

ORM,即Object-Relational Mapping(对象关系映射),它的作用是在关系型数据库和业务实体对象之间作一个映射,这样,我们在具体的操作业务对象的时候,就不需要再去和复杂的SQL语句打交道,只需简单的操作对象的属性和方法。

简单轻量的orm框架,对于提高整个团队的开发效率及代码可维护性具有很大的意义,下面介绍一下大搜车基于mybatis开发的orm框架开发出发点及基本的设计思路,希望能够给大家带来一些启发。

开发自己的orm框架的起因

大搜车的线上业务是从2013年6月份开始的,那时候我们做的业务是寄售业务,最多的业务场景是集中在后台业务逻辑处理及报表统计上面,开始的时候,我们的数据库操作是基于mybatis的底层来做的,mybatis本身是java领域最常用的一个轻量半自动数据库orm持久层框架,从oobject-relational-mapping的角度来说,它主要是解决数据返回层的java类映射的问题。从开发模式上,mybatis是把开发人员的sql放到xml配置文件中,任何对表的操作,事实还是需要自行写大量的sql来进行操作的。

在我们开始使用mybatis的过程中,虽然业务上看起来还不是很复杂,但是事实上开发人员在编写sql的过程中,已经开始突显很多问题:

由于业务一直在迭代,我们的表结构经常需要做一些调整,但是在调整表结构的时候,开发人员经常会漏调整部分sql中的字段,导致部分逻辑出现问题

由于业务逻辑的关系,有的表字段有好几十个,编写插入及查询sql时,很考验开发人员的细心程度,而且写sql的工作量也比较大,不同人写的sql方法,风格各异。

那时候我们后台管理系统中,大量使用了join的操作,从整体系统的可维护性来说,对一个表进行查询操作的sql落在不同xml文件中,系统极难维护,对系统后续的调整和升级埋下了很多隐患。 下面是我们当时一个典型的sql例子:

SELECT a.id,a.car_brand AS brand,a.car_series AS series,a.car_model AS model,a.first_register_date AS register,

a.license_plate_province AS province,a.license_plate_city AS city,a.car_mileage AS mileage,a.flag AS flag,

a.car_new_price AS carNewPrice,a.car_now_price AS carNowPrice,a.insure_expire_date AS insureExpireDate, a.year_check_date AS yearCheckDate,a.car_status AS carStatus,

a.car_input_user AS USER,a.car_deadline AS deadline, a.car_parking_space AS carParkingSpace,a.car_source AS carSource,a.car_trans_fee AS carTransFee,a.car_store AS carStore, a.lushi_describe AS lushiDescribe,a.engine_describe AS engineDescribe,a.body_trim_describe AS bodyTrimDescribe,

a.painting_describe AS paintingDescribe,a.date_create AS dateCreate,a.date_update AS dateUpdate,a.souche_number AS soucheNumber, a.date_up AS dateUp,a.date_down AS dateDown,

a.date_trade AS dateTrade,a.car_discount AS carDiscount, b.car_level AS carLevel,b.car_age AS carAge, c.picture_url_big AS carPicture, f.car_transmission_type as carTransmissionType, h.begin_date AS flashBeginDate,h.end_date AS flashEndDate

FROM car_basic_info a LEFT JOIN car_library_level b ON a.id=b.carId LEFT JOIN car_pictures c ON a.id=c.carId

left join carlibrary_label_info g on a.id=g.carId and other_label is null

left join carview_basic_parameter f on a.id=f.carId

LEFT JOIN index_car h ON a.id=h.car_id AND h.type ='3' AND h.date_delete is NULL

WHERE a.date_delete IS NULL AND a.car_status='zaishou'

AND a.car_brand = #{brand}

AND a.car_series = #{series}

AND f.car_model = #{model}

...

当时从整个业务逻辑sql的编写上,事实上我们虽然还是处于最早期的阶段,但是sql的维护上我认为我们已经开始有明显趋于混乱的迹象。为了尽量从根源上解决问题,我们开始尝试寻找一种更好的真正的orm策略,我们先思考了自己开发的基本方案,同时也对hibernate做了初步的调研,也想过是否直接使用hibernate来替换掉mybatis,但是最终还是决定自己开发。没有直接采用hibernate,主要是基于下面的几点考虑:

hibernate配置上,还是很难真正达到极简或完全不需要配置的程度

hibernate的使用上过于灵活,实现同一种目的可能会有很多种不同的写法,使用上不规范的话,可能会带来极大的性能问题,而且事实上我们整体要往hibernate上迁的话,业务sql上要做不少改动。

我们团队没有完全吃透hibernate的人

而从另一方面,mybatis本身透露给外面的接口会简单得多,所以我们决定基于mybatis进行二次开发。我们希望自研的orm策略需要含有下面的基本原则:

能够规范开发人员编写普通业务的sql,不需要直接写sql(或写很少的sql),能够极大简化开发人员编写业务sql的时间

开发人员在使用时,调试上比较便利,而且不容易出错。

原有的业务sql,能够比较便利地迁移到新的orm框架上面

对一条数据进行insert/update时,把创建时间/更新时间自动填入默认的数据库字段中

尽量基于mybatis进行二次开发

基本设计思路

下面说一下大搜车orm的基本设计思路:

基于annotation自动进行or-mapping映射的操作,在对java bean有操作需求时,加载对应的table,自动去映射表结构与java bean的关系。

定义几个通用的mybatis数据库操作,把所有对数据库表的增删改查操作,都映射到这几个mybatis配置文件里定义的sql上去。

对mybatis的返回结果,默认使用map进行传递,我们再自行做map2Object的java bean转化,从而保证与mybatis内部的传递及对外部接口返回的透明。

下面是我们定义的通用mybatis sql:

show tables

desc ${table}

${sql}

${sql}

${sql}

${sql}

数据表关系映射

在要操作的object中,加上@SqlTable映射,表示某一个java bean与表的映射关系。另外在做具体映射时,基于下面的规则进行表与数据字段的映射:

如果bean中的字段上有定义@SqlColumn的声明,则使用表中的字段名与该字段做为映射关系。

如果bean中对应的的字段没有定义@SqlColumn,则把javaObject中的字段名及表中的字段名都同步做同样的normalize,再进行字段比对。normalize的规则是去除掉下划线及中划线,然后同步转化为大写字母。如果两个的字段名一样,则认为它们是匹配上的。

如java bean中有dateCreate字段,数据库中有date_create字段,则它们都normalize成DATECREATE,这样就表示javabean中的dateCreate与数据库中的date_create是映射上的

下面是一个简单的映射例子:

CREATE TABLE `test` (

`id` int(11) NOT NULL AUTO_INCREMENT,

`user_id` varchar(45) DEFAULT NULL,

`numbers` varchar(45) DEFAULT NULL,

`date_create` timestamp NULL DEFAULT NULL,

`date_update` timestamp NULL DEFAULT NULL,

`date_delete` timestamp NULL DEFAULT NULL,

PRIMARY KEY (`id`),

KEY `date_create` (`date_create`)

);

@SqlTable("test")

public class TestDO implements Serializable {

private Long id; // 对应表中的主键id

private String userId; // 对应表中的字段user_id

@SqlField("numbers")

private String nnn; // 对应numbers字段

private Timestamp dateCreate; // 对应date_create字段

private Timestamp dateUpdate;

private Timestamp dateDelete;

...

get/set函数

}

基本实现策略

@Override

public List findListByQuery(QueryObj query, Class extends E> cls) {

Map queryMap = Maps.newHashMap();

// 构建查询的sql生成器

SqlBuilder sqlBuilder = new QuerySqlBuilder(query, cls);

// queryMap中,产生sql语句及要送入mybatis中的查询参数

sqlBuilder.buildSql(queryMap);

// 执行mybatis中的orm.select语句,得到返回结果

List> retMaps = sqlSession.selectList("orm.select", queryMap);

List rets = new ArrayList();

for(Map map : retMaps) {

// 把返回结果映射为cls类

E ret = SqlAnnotationResolver.convertToObject(map, cls);

if(ret != null) {

rets.add(ret);

}

}

return rets;

}

上面这段代码片断,是产生查询语句的代码片断,从代码片断中可以基本看出我们的实现思路:

构建一个map,用于传递mybatis中的上下文

对于不同的数据库查询操作,构建不同的SqlBuilder,所有sqlBuilder执行完以后,在map对象中,会产生一个key为"sql"的值,用于表示要执行的sql语句。另外map对象中,还会包含要传递进入mybatis中的参数列表

执行完mybatis以后,把返回的结构转化为需要的对象。

基本使用例子

假设要操作一个用户表,表的基本定义及javabean定义是

CREATE TABLE `user` (

`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户id',

`phone` varchar(20) NOT NULL DEFAULT '' COMMENT '电话',

`name` varchar(64) DEFAULT NULL COMMENT '姓名',

`age` int(5) DEFAULT '0' COMMENT '年龄',

`sallary` float(12) DEFAULT '0' COMMENT '薪水',

`height` float(5) DEFAULT NULL comment '身高',

`weight` float(5) DEFAULT NULL comment '体重',

`date_create` datetime DEFAULT NULL,

`date_update` datetime DEFAULT NULL,

PRIMARY KEY (`id`),

unique KEY (`phone`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';

要对该表进行操作,只需要定义一个java bean及声明:

@SqlTable("user")

public class UserDO extends BaseVo {

private Integer id; //ID

private String phone;

private String name; //姓名

private Integer age; //年龄

private Date birthday; //生日

private BigDecimal sallary; //薪水

private Double height; //身高

private Float weight; //体重

private Timestamp dateCreate;

private Timestamp dateUpdate;

}

基本的增删改查

下面以用户表的简单操作为例子,说明一下orm的基本用法。

插入一条新的用户

UserDO user = new UserDO();

user.setPhone("13735914821");

user.setName("hello world");

user.setAge(20);

user.setHeight(1.75);

user.setWeight(65.1f);

int id = basicDao.insert(user); // 返回的用户id

产生的sql:

insert into user(name, age, height, weight, phone, date_create, date_update) values (#{name}, #{age}, #{height}, #{weight}, #{phone}, #{dateCreate}, #{dateUpdate})

查询主键id为4的用户

UserDO u = basicDao.findObjectByPrimaryKey(4, UserDO.class);

上面的语句产生的sql为:

select a.name as name, a.id as id, a.age as age, a.birthday as birthday, a.sallary as sallary, a.height as height, a.weight as weight, a.phone as phone, a.date_create as dateCreate, a.date_update as dateUpdate from user a where a.id = #{id}

根据电话查询用户

UserDO sample = new UserDO();

sample.setPhone("13735914822");

UserDO ret = basicDao.findObjectBySample(sample, UserDO.class);

产生的sql为:

select a.name as name, a.id as id, a.age as age, a.birthday as birthday, a.sallary as sallary, a.height as height, a.weight as weight, a.phone as phone, a.date_create as dateCreate, a.date_update as dateUpdate from user a where 1=1 and a.phone = #{phone} order by a.date_create desc limit 0, 1

根据特定条件使用like查询

查询年龄为20岁,且手机号以137开头,且体重大于60公斤的用户:

String key = "137";

UserDO sample = new UserDO();

sample.setAge(20);

QueryObj queryObj = new QueryObj(sample);

queryObj.addQuery("weight >= #{weight}", 60);

queryObj.addQuery("phone like #{query}", key + "%");

List user = basicDao.findListByQuery(queryObj, UserDO.class);

产生的sql:

select a.name as name, a.id as id, a.age as age, a.birthday as birthday, a.sallary as sallary, a.height as height, a.weight as weight, a.phone as phone, a.date_create as dateCreate, a.date_update as dateUpdate from user a where 1=1 and a.age = #{age} and ((a.weight >= #{_weight}) and (a.phone like #{_query})) order by a.date_create desc limit 0, 1000

其中,查询map中,age为20, _weight为60, _query为137%

join查询的使用说明

大搜车orm框架中,对表的join操作策略,设计得是相对比较简洁的一个点,下面以一个简单的例子,来说明一下orm join的用法

假设在上面用户表的基础上增加一个用户操作记录表,表的结构及java bean的定义如下:

CREATE TABLE `user_operator` (

`id` int(11) unsigned NOT NULL AUTO_INCREMENT,

`user_id` int(11) COMMENT '用户id',

`operate_type` varchar(20) DEFAULT NULL COMMENT '用户操作类型',

`app_name` varchar(20) DEFAULT '' COMMENT 'app name',

`op_time` datetime default null comment '用户操作时间',

PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户操作表';

用户操作记录bean:

@SqlTable("user_operator")

public class UserOpteratorDO {

private Integer id; //ID

private Integer userId; // user id

private String operateType; // 操作类型

private String appName; // 应用名

private Timestamp opTime; // 操作时间

... // getter setter

}

用户操作记录传输dto:

public class UserOpteratorDTO {

private UserDO user;

private UserOpteratorDO operator;

public void setUser(UserDO user) {

this.user = user;

}

public UserDO getUser() {

return user;

}

public UserOpteratorDO getOperator() {

return operator;

}

public void setOperator(UserOpteratorDO operator) {

this.operator = operator;

}

}

下面要查询用户操作类型为add或者click,操作时间为2017-03-20 00:00 ~ 2017-03-21 10:00,且用户的手机号以"137"开头的所有用户操作记录

// 以UserDO为主表进行查询

QueryObj queryObj = new QueryObj(new UserDO());

// 对主表加上phone的like语句

queryObj.addQuery("UserDO.phone like #{phone}", "137%");

// 加上对表UserOpteratorDO的join操作,join操作为inner join,表的级联以主表的id和operator表的user_id作为关联主键

JoinParam joinParam = new JoinParam(UserOpteratorDO.class, JoinType.INNER_JOIN, new JoinPair("id", "userId"));

queryObj.addJoinParam(joinParam);

// 加上操作类型列表

List typeList = new ArrayList();

typeList.add("add");

typeList.add("click");

// 对UserOpteratorDO表的查询语句

queryObj.addQuery("UserOpteratorDO.operateType in #{type}", typeList);

queryObj.addQuery("UserOpteratorDO.opTime >= #{startTime}", "2017-03-20 00:00");

queryObj.addQuery("UserOpteratorDO.opTime <= #{endTime}", "2017-03-20 10:00");

// 操作1,返回结果为UserOpteratorDTO类型

List operators = basicDao.findListByQuery(queryObj, UserOpteratorDTO.class);

返回的sql语句为:

select a.name as __user_name, a.id as __user_id, a.age as __user_age, a.birthday as __user_birthday, a.sallary as __user_sallary, a.height as __user_height, a.weight as __user_weight, a.phone as __user_phone, a.date_create as __user_dateCreate, a.date_update as __user_dateUpdate, b.id as __operator_id, b.user_id as __operator_userId, b.operate_type as __operator_operateType, b.app_name as __operator_appName, b.op_time as __operator_opTime from user a INNER JOIN user_operator b on a.id = b.user_id where 1=1 and ((a.phone like #{_phone}) and (b.operate_type in ('add','click')) and (b.op_time >= #{_startTime}) and (b.op_time <= #{_endTime})) order by a.date_create desc limit 0, 1000

其中,_phone的值为137%, _startTime的值为 2017-03-20 00:00

操作2,最后一步查询操作,如果返回结果为UserDO类型

// generate query

List operators = basicDao.findListByQuery(queryObj, UserDO.class);

对应的sql为:

select a.name as name, a.id as id, a.age as age, a.birthday as birthday, a.sallary as sallary, a.height as height, a.weight as weight, a.phone as phone, a.date_create as dateCreate, a.date_update as dateUpdate from user a INNER JOIN user_operator b on a.id = b.user_id where 1=1 and ((a.phone like #{_phone}) and (b.operate_type in ('add','click')) and (b.op_time >= #{_startTime}) and (b.op_time <= #{_endTime})) order by a.date_create desc limit 0, 1000

操作3,最后一步的查询操作,如果返回的结果为UserOperateDO类型

// generate query

List operators = basicDao.findListByQuery(queryObj, UserOpteratorDO.class);

对应的sql为:

select a.id as id, b.user_id as userId, b.operate_type as operateType, b.app_name as appName, b.op_time as opTime from user a INNER JOIN user_operator b on a.id = b.user_id where 1=1 and ((a.phone like #{_phone}) and (b.operate_type in ('add','click')) and (b.op_time >= #{_startTime}) and (b.op_time <= #{_endTime})) order by a.date_create desc limit 0, 1000

java orm设计_大搜车orm框架设计思路相关推荐

  1. python数据接口设计_基于python的接口测试框架设计(一)连接数据库

    基于python的接口测试框架设计(一)连接数据库 首先是连接数据库的操作,最好是单独写在一个模块里, 然后便于方便的调用,基于把connection连接放在__init__()方法里 然后分别定义D ...

  2. 大搜车Java面试 2017.10.30

    大搜车Java面试 2017.10.30 杭州余杭区办公环境一般,两层一栋的办公楼,但是开发工位是连成一片的没有卡位一说.比较拥挤. 面试两轮技术面,总共耗时3小时,各种等待就有1小时,最后人事说三天 ...

  3. 关于大搜车「无线开发中心」团队

    更多文章,参见大搜车技术博客:blog.souche.com/ 大搜车无线开发中心持续招聘中,前端,Nodejs,android 均有 HC,简历直接发到:sunxinyu@souche.com 我们 ...

  4. 记录去大搜车的一道笔试题

    java编程题 实现一个字符串数字转人民币的小功能 入参:一个字符串,范围是0.00~999999.99,最多两位小数 出参:对应的人民币大写或者验证不合法 例子1: input:12345.67 o ...

  5. HBase在大搜车金融业务中的应用实践

    摘要: 2017云栖大会HBase专场,大搜车高级数据架构师申玉宝带来HBase在大搜车金融业务中的应用实践.本文主要从数据大屏开始谈起,进而分享了GPS风控实践,包括架构.聚集分析等,最后还分享了流 ...

  6. 回客科技 面试的 实现ioc 容器用到的技术,简述BeanFactory的实现原理,大搜车面试的 spring 怎么实现的依赖注入(DI)...

    前言:这几天的面试,感觉自己对spring 的整个掌握还是很薄弱.所以需要继续加强. 这里说明一下spring的这几个面试题,但是实际的感觉还是不对的,这种问题我认为需要真正读了spring的源码后说 ...

  7. 大搜车面向复杂业务场景的研发运维体系治理实践

    图:大搜车基础设施部负责人李同刚 2021年12月10日,在云上架构与运维峰会上,大搜车集团基础设施部负责人李同刚分享了大搜车在研发运维体系治理的一些实践.以下是他的演讲实录: 一.业务介绍 1.汽车 ...

  8. 大搜车java_记录去大搜车的一道笔试题

    java编程题 实现一个字符串数字转人民币的小功能 入参:一个字符串,范围是0.00~999999.99,最多两位小数 出参:对应的人民币大写或者验证不合法 例子1: input:12345.67 o ...

  9. JuiceFS 在大搜车数据平台的实践

    大搜车已经搭建起比较完整的汽车产业互联网协同生态.在这一生态中,不仅涵盖了大搜车已经数字化的全国 90% 中大型二手车商.9000+ 家 4S 店和 70000+ 家新车二网,还包括大搜车旗下车易拍. ...

最新文章

  1. C语言将正整数转换为字符串(附完整源码)
  2. mysql left join 结果怎么这么慢
  3. 12 c for. oracle rac,【案例】Oracle RAC FOR AIX搭建执行root.sh时两次报错的解决办法
  4. Bdsyn百度手机助手是何物,它是怎样神不知鬼不觉地安装到你的电脑里的?
  5. C++编译运行过程分析
  6. hbuilderx内置服务器启动失败_我们来看看Swoole是如何实现WebSocket服务器及客户端的...
  7. java 包装类缺点_Java 自动拆箱和自动装箱学习笔记
  8. 科研_今天,我们怎么做科研?
  9. bzoj_3529 数表
  10. SQL注入漏洞-POST注入
  11. 【LG3244】[HNOI2015]落忆枫音
  12. 在OpenCV里实现直方图反向投影算法
  13. Nginx配置虚拟主机
  14. 群晖Docker百度网盘套件一直灰界面
  15. 亚马逊云计算平台---------AWS(一)
  16. Android M 差分包的制作流程
  17. android手游自动按键,天涯明月刀手游自动弹奏按键精灵使用详细教学 安卓ios使用教程...
  18. 统一过程(UP)模型
  19. 进入2.0阶段!从阿里大鱼买断军事大V看内容平台的生态之争
  20. 农历公历万年历互查系统

热门文章

  1. Java、JSP飞机航班信息查询系统
  2. 移动增值业务新人入职培训
  3. 快速入门:LINUX基础 ——教你使用 linux 操作
  4. 使用opencv-python和dlib实现的简单换脸程序
  5. Ajax数据的爬取(淘女郎为例)
  6. Ubuntu18编译Kalibr报错总结
  7. 中国高科技企业在忙什么(聚焦)
  8. Shopee关联广告和关键词广告有什么不同?如何优化广告关键词?
  9. Fastreport.Net用户手册(八):设计器中的Bands
  10. 2021年安全员-A证考试报名及安全员-A证最新解析