大搜車orm框架設計思路

kncb5891 7年前發布 | 50K 次閱讀 SQL MyBatis

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 id="queryCarBasicInfo" resultType="com.souche.vo.carbasic.CarBasicVO">  
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
<if test="label != null">  
left join carlibrary_label_info g on a.id=g.carId and other_label is null  
</if>  
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'  
<if test="brand != null">  
    AND a.car_brand = #{brand}
</if>  
<if test="series != null">  
 AND a.car_series = #{series}
</if>  
<if test="model != null">  
    AND f.car_model = #{model}
</if>  
<if ……>  
...
</if>  
 </select>

當時從整個業務邏輯sql的編寫上,事實上我們雖然還是處于最早期的階段,但是sql的維護上我認為我們已經開始有明顯趨于混亂的跡象。為了盡量從根源上解決問題,我們開始嘗試尋找一種更好的真正的orm策略,我們先思考了自己開發的基本方案,同時也對hibernate做了初步的調研,也想過是否直接使用hibernate來替換掉mybatis,但是最終還是決定自己開發。沒有直接采用hibernate,主要是基于下面的幾點考慮:

  1. hibernate配置上,還是很難真正達到極簡或完全不需要配置的程度
  2. hibernate的使用上過于靈活,實現同一種目的可能會有很多種不同的寫法,使用上不規范的話,可能會帶來極大的性能問題,而且事實上我們整體要往hibernate上遷的話,業務sql上要做不少改動。
  3. 我們團隊沒有完全吃透hibernate的人

而從另一方面,mybatis本身透露給外面的接口會簡單得多,所以我們決定基于mybatis進行二次開發。我們希望自研的orm策略需要含有下面的基本原則:

  1. 能夠規范開發人員編寫普通業務的sql,不需要直接寫sql(或寫很少的sql),能夠極大簡化開發人員編寫業務sql的時間
  2. 開發人員在使用時,調試上比較便利,而且不容易出錯。
  3. 原有的業務sql,能夠比較便利地遷移到新的orm框架上面
  4. 對一條數據進行insert/update時,把創建時間/更新時間自動填入默認的數據庫字段中
  5. 盡量基于mybatis進行二次開發

基本設計思路

下面說一下大搜車orm的基本設計思路:

  1. 基于annotation自動進行or-mapping映射的操作,在對java bean有操作需求時,加載對應的table,自動去映射表結構與java bean的關系。
  2. 定義幾個通用的mybatis數據庫操作,把所有對數據庫表的增刪改查操作,都映射到這幾個mybatis配置文件里定義的sql上去。
  3. 對mybatis的返回結果,默認使用map進行傳遞,我們再自行做map2Object的java bean轉化,從而保證與mybatis內部的傳遞及對外部接口返回的透明。

下面是我們定義的通用mybatis sql:

<mapper namespace="orm">
<select id="ALL_TABLE" resultType="string"> show tables </select> <select id="TABLE_SCHEMA" resultType="com.souche.optimus.dao.mapping.TableColumn"> desc ${table} </select>

<insert id="insert" useGeneratedKeys="true" keyProperty="__pid">
    ${sql}
</insert>

<select id="update" resultType="HashMap">
    ${sql}
</select>

<select id="delete" resultType="HashMap">
    ${sql}
</select>

<select id="select" resultType="HashMap">
    ${sql}
</select>

</mapper></code></pre>

數據表關系映射

在要操作的object中,加上@SqlTable映射,表示某一個java bean與表的映射關系。另外在做具體映射時,基于下面的規則進行表與數據字段的映射:

  1. 如果bean中的字段上有定義@SqlColumn的聲明,則使用表中的字段名與該字段做為映射關系。
  2. 如果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函數 }</code></pre>

基本實現策略

@Override
    public <E> List<E> findListByQuery(QueryObj query, Class<? extends E> cls) {
        Map<String, Object> queryMap = Maps.newHashMap();

    // 構建查詢的sql生成器
    SqlBuilder sqlBuilder = new QuerySqlBuilder(query, cls);
    // queryMap中,產生sql語句及要送入mybatis中的查詢參數
    sqlBuilder.buildSql(queryMap);

    // 執行mybatis中的orm.select語句,得到返回結果
    List<Map<String, Object>> retMaps = sqlSession.selectList("orm.select", queryMap);
    List<E> rets = new ArrayList<E>();
    for(Map<String, Object> map : retMaps) {
        // 把返回結果映射為cls類
        E ret = SqlAnnotationResolver.convertToObject(map, cls);
        if(ret != null) {
            rets.add(ret);
        }
    }
    return rets;
}</code></pre> 

上面這段代碼片斷,是產生查詢語句的代碼片斷,從代碼片斷中可以基本看出我們的實現思路:

  1. 構建一個map,用于傳遞mybatis中的上下文
  2. 對于不同的數據庫查詢操作,構建不同的SqlBuilder,所有sqlBuilder執行完以后,在map對象中,會產生一個key為"sql"的值,用于表示要執行的sql語句。另外map對象中,還會包含要傳遞進入mybatis中的參數列表
  3. 執行完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<UserDO> 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<String> typeList = new ArrayList<String>();
        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<UserOpteratorDTO> 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<UserDO> 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<UserOpteratorDO> 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

 

來自:https://blog.souche.com/da-sou-che-ormkuang-jia-she-ji-si-lu/

 

 本文由用戶 kncb5891 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!