半个小时手写一个极简版ORM框架,实现简单的CRUD操作

2022-07-26 17:11:21 浏览数 (1)

码农在囧途

已经忘记上一次写字是什么时候了,应该很久了吧,突然间想写写字,我翻箱倒柜的找了找,只找到了笔,却没有本, 这笔中的墨虽然不足以支撑我书写糟糕的过去和未知的未来,却能写下我当下的能把握住的人生,即使没有运笔舒畅的纸张, 床边的窗帘也足够我草草了事,虽写不出丹麦王子的悲剧,子美诗篇的荡气回肠,不过城市的灯光洒落在破旧的窗帘上, 夹缝中的几个字却是人生坚持的理想!

前言

ORM框架可以减轻在开发中的一些负担,简单的单表的增删改查如果全部都写sql的话那么也会是一个工作量,因为不仅要面临写大量的sql语句, 还要处理jdbc结果集映射到实体的操作,这其中会面临写大量重复无用的代码,而且在结果集映射的过程中出错的可能性也很大,所以就出现了 很多ORM框架,例如Mybatis,Hibernate等,对于简单的单表的操作,这些框架提供了大量的API给我们使用,大大的减轻开发的负担,本文 就实现一个简单版的ORM框架,让大家理解ORM的实现思路。

工程结构

代码语言:javascript复制
└─com
    └─steak
        └─orm
            │  SimpleOrmApplication.java
            │  
            ├─annotation
            │      PrimaryKey.java
            │      Table.java
            │      
            ├─builder
            │      BaseSQLBuilder.java
            │      DeleteSQLBuilder.java
            │      QuerySQLBuilder.java
            │      SaveSQLBuilder.java
            │      UpdateSQLBuilder.java
            │      
            ├─datasource
            │      JdbcTemplate.java
            │      MyDataSource.java
            │      RowMapper.java
            │      
            ├─domain
            │      User.java
            │      
            ├─factory
            │      SQLBuilderInstanceFactory.java
            │      
            ├─service
            │  │  IDelete.java
            │  │  IQuery.java
            │  │  ISave.java
            │  │  IUpdate.java
            │  │  
            │  └─impl
            │          Delete.java
            │          Query.java
            │          Save.java
            │          Update.java
            │          
            ├─test
            │      Client.java     
            └─tool
                    StringUtil.java

我们要做的就是实现实体到数据库字段的映射,那么势必会涉及到大量的反射操作,增删改查的操作都是一样的,只是查询多了一个结果集 的映射,增删改没有,所以他们在构建sql语句的时候其实都是一样的,那么本文就只来讲解查询功能,其他的就不讲了。

编码实现

Table注解

@Table注解标注在实体上面,表明是一个DO,在领域驱动设计中,对于实体的划分是严格的,但是在平常的开发中,我们发现开发人员对于实体的划分 是不严格的,比如DO应该是和数据库中的字段是一一对应的,这个实体的职责就是和数据库字段的映射,不应该有其他的职责,所以里面不应该添加其他的 字段,但是很多时候我们看到的是,这个实体中充满了很多额外字段,这个实体不仅作为数据传输对象DTO,还作为了视图对象VO,这都是不规范的做法。 @Table注解中value用来指定数据表名称,如果不指定,则默认为实体类名的小写。

代码语言:javascript复制
/**
 * @author 刘牌
 * @date 2022/3/315:34
 */
@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Table {
    String value() default "";
}

主键注解PrimaryKey

PrimaryKey的作用是标明那个字段是主键,如果value值为空,则m默认使用字段名。

代码语言:javascript复制
/**
 * @author 刘牌
 * @date 2022/3/414:26
 */
@Inherited
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PrimaryKey {
    String value() default "";
}

IQuery接口

定义一个查询接口,参数是一个泛型参数

代码语言:javascript复制
/**
 * @author 刘牌
 * @date 2022/3/322:31
 */
public interface IQuery<T> {
    List<T> query(T t) throws Exception;
}

查询实现类Query

Query类相当于一个执行器,它会调用sql构造器构造sql,然后调用Jdbc执行sql语句,并返回结果,它继承自JdbcTemplate,JdbcTemplate 封装了jdbc的连接和CRUD操作,Query类中核心的是调用sql构造器,和jdbc结果集和实体之间的映射,二者都是利用反射操作来完成的。

代码语言:javascript复制
/**
 * @author 刘牌
 * @date 2022/3/322:35
 */
public class Query<T> extends JdbcTemplate implements IQuery<T> {

    @Override
    public List<T> query(T t) throws Exception {
        String sql = SQLBuilderInstanceFactory.getQueryBuilder().querySql(t);
        Field[] fields = t.getClass().getDeclaredFields();
        return executeQuery(sql, new RowMapper<T>() {
            @Override
            public T mapRow(ResultSet resultSet) throws Exception {
                for (Field field : fields) {
                    String getField = StringUtil.getSetMethod(StringUtil.getLastStr(field.toString()));
                    Object object = resultSet.getObject(StringUtil.getLastStr(field.toString()), field.getType());
                    t.getClass().getMethod(getField,field.getType()).invoke(t,object);
                }
                return t;
            }
        });
    }
}

sql构建基类BaseSQLBuilder

BaseSQLBuilder主要提取出一些工作的常量和方法,

代码语言:javascript复制
/**
 * @author 刘牌
 * @date 2022/3/413:58
 */
public abstract class BaseSQLBuilder {

    protected String tableName; //表名
    protected String primaryKeyName; //主键名
    protected final String SELECT = "select ";
    protected final String FROM = " from ";
    protected final String WHERE = " where ";
    protected final String AND = " and ";
    protected final String IN = " IN ";
    protected final String UPDATE = " UPDATE ";
    protected final String SET = " SET ";
    protected final String VALUES = " VALUES ";
    protected final String OR = " OR ";
    protected final String DELETE = " DELETE ";
    protected final String INSERT = " INSERT ";
    protected final String INTO = " INTO ";

    protected StringBuilder sqlBuilder = new StringBuilder();

    //获取表名
    protected void getTableName(Object obj){
        Table table = obj.getClass().getAnnotation(Table.class);
        tableName = table.value();
        if (Objects.equals(tableName, ""))
            tableName = StringUtil.getLastStr(obj.getClass().getName());
    }
    //获取主键名
    protected void getPrimaryKey(Field field){
        PrimaryKey primaryKey = field.getAnnotation(PrimaryKey.class);
        primaryKeyName = primaryKey.value();
        if (Objects.equals(primaryKeyName, ""))
            primaryKeyName = StringUtil.getLastStr(field.getClass().getName());
    }
    //判断主键
    protected boolean hasPrimaryKey(Field field){
        PrimaryKey primaryKey = field.getAnnotation(PrimaryKey.class);
        return primaryKey != null;
    }
    
    protected String getField(String fieldStr){
        return "get"   fieldStr.substring(0, 1).toUpperCase()   fieldStr.substring(1);
    }

    protected Field[] getFields(Object obj){
        return obj.getClass().getDeclaredFields();
    }
}

查询sql构建类QuerySQLBuilder

它的作用就是构建查询sql,通过反射操作,实现sql的动态拼接。

代码语言:javascript复制
/**
 * @author 刘牌
 * @date 2022/3/315:38
 */
public class QuerySQLBuilder extends BaseSQLBuilder {
    public String querySql(Object t) throws Exception {
        getTableName(t);
        sqlBuilder.append(SELECT   "*"   FROM).append(tableName).append(WHERE   " 1=1 ");
        for (Field field : getFields(t)) {
            String fieldStr = StringUtil.getLastStr(field.toString());
            Object value = t.getClass().getMethod(getField(fieldStr)).invoke(t);
            if (!"".equals(value) && null != value) {
                sqlBuilder.append(AND).append(fieldStr).append("=").append("'").append(value).append("'");
            }
        }
        return sqlBuilder.toString();
    }
}

执行sql语句

JdbcTemplate中查询操作。

代码语言:javascript复制
public abstract class JdbcTemplate extends MyDataSource {

    protected static Connection connection;
    protected static PreparedStatement preparedStatement;
    protected ResultSet resultSet;

    //查询
    protected <T> List<T> executeQuery(String sql, RowMapper<T> rowMapper) throws Exception {
        preparedStatement = preparedStatement(sql);
        resultSet = preparedStatement.executeQuery();
        List<T> list = resultSet(resultSet, rowMapper);
        close();
        return list;
    }

    //结果集
    private <T> List<T> resultSet(ResultSet resultSet, RowMapper<T> rowMapper) throws Exception {
        List<T> list = new ArrayList<>();
        while (resultSet.next()) {
            list.add(rowMapper.mapRow(resultSet));
        }
        return list;
    }
}

测试

我们只构造一个实体作为查询条件,就能够查询出对应的数据。

代码语言:javascript复制
/**
 * @author 刘牌
 * @date 2022/3/315:46
 */
public class Client {
    public static void main(String[] args) throws Exception {
        User user = new User();
        user.setAddress("china");
        user.setUsername("steak");
        user.setGender("man");
        user.setId(3);
        List<User> userList = new Query<User>().query(user);
        System.out.println(userList);
    }
}

查询功能就完成了,其他的添加,删除,修改等功能其实也是一个的思想,都是构建sql,执行查询操作,在本示例中,例子过于简单,显然不能够满足 开发需要,只提供了条件查询操作,并没有提供像去重,分组,排序等等操作,不过这些要加入这些操作,其实也是一样的,只要我们明白其核心思想就行了, 在实际开发中我们也不会自己去封装一套,因为像Mybatis这种ORM框架提供更加方便快捷友好的操作,完全能够满足我们的需求,我们造轮子的初心并不是 取代别的轮子,而是在造轮子的过程中提升自己的认知和水平。

项目demo地址

对于其他的增加,删除修改操作,可去看源代码。

https://gitee.com/steakliu/design-pattern/tree/de623f1dc97e8793fa732e726f88c480132288c8/orm/simpleOrm

今天的分享就到这里,感谢你的观看,下期见。

0 人点赞