写出你自己的ORM框架(一)

前言:读完本片文章大约20分钟

  • 代码地址
  • 写出你自己的ORM框架(一)
  • 写出你自己的ORM框架(二)
  • 写出你自己的ORM框架(三)
  • 写出你自己的ORM框架(四)

一、架构说明

主要分为如下几个包,和以下几个类,接口(一级为包,二级(普通颜色为类,黄色为接口),加粗为说明)

  • cn.gxm.sorm.bean
    • ColumnInfo 对应数据库中的每一个字段结构
    • TableInfo 对应数据库中的每表结构
    • Configuration 映射资源文件(数据源 ,dataSource,将配置文件中的信息映射成对象)
  • cn.gxm.sorm.core
    • DBManager 根据配置信息,维持连接对象的管理(增加连接池功能)
    • Query 操作数据库的核心接口,CRUD都在这里定义
    • QueryFactory 工厂模式创建核心 query 对象
    • TableContext 理数据库中的所有表和java中的对象的关系可以根据表生成java对象
    • TypeConvertor 用于数据数据类型与java数据类型转换,因为数据库的不同,类型转换也不同,所以定义为接口
  • cn.gxm.sorm.utils
    • JavaFileUtils 操作文件工具类
    • JDBCUtils 封装数据库连接操作工具类
    • ReflectUtils 反射的工具类
    • StringUtils 操作字符串的工具类

二、第一部分实现

2.0 实现db.properties文件,以及对应的映射类configuration

  • db.properties文件即datasourse,连接数据库的必要参数,如下:
    前四个大家都知道,
    • srcPath 是以后使用我们这个框架项目的src路径
    • pojoPackage 是我们根据数据库的表信息生成的pojo的位置
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/sorm
username=root
password=123456
srcPath=D://IntelliJ_IDEA//MyDemo//SORM//src
pojoPackage=cn.gxm.sorm.pojo
  • configuration类就不用多说了(set,get等方法省略),如下
/**
     * 数据库驱动
     */
    private String driver;

    /**
     * 连接数据库url
     */
    private String url;

    /**
     * 数据库用户名
     */
    private String username;

    /**
     * 数据库密码
     */
    private String password;

    /**
     * 项目src绝对路径
     */
    private String srcPath;

    /**
     * 生成的表对象的包路径
     */
    private String pojoPackage;

2.1 实现数据库中表对应的Pojo

后面我们将会通过数据库的元数据获取表,以及表中的字段,所以,我们需要对应的pojo,如下
字段:(set,get等方法省略)

/**
 * @author GXM www.guokangjie.cn
 * @date 2019/5/15
 *
 * 对应数据库中的每一个字段结构
 */
public class ColumnInfo {

    /**
     * 字段名称 例如 id,name,age
     */
    private String name;

    /**
     * 字段类型 例如 varchar
     */
    private String dataType;

    /**
     * 字段键类型 例如 1:普通键 2:主键 3:外键
     */
    private Integer keyType;

(set,get等方法省略)

/**
 * @author GXM www.guokangjie.cn
 * @date 2019/5/15
 *
 * 对应数据库中的表结构
 */
public class TableInfo {
    /**
     * 表的名称 例如 User
     */
    private String name;

    /**
     * 表中的字段,使用map为方便后期获取
     * 例如 map
     */
    private Map<String,ColumnInfo> columns;

    /**
     * 表中的唯一主键
     */
    private ColumnInfo onlyPriKey;

    /**
     * 联合主键即(多个字段组成的主键)
     */
    private List<ColumnInfo> unionKey;

2.2 实现连接的提供,datasourse资源文件的映射

从前面的架构来说,连接的提供以及资源文件的映射都在DBManager中处理,因为资源文件映射只需要一次即可,所以放在static中(只会加载一次),并且向外提供了获取连接的静态方法,以及Configuration静态方法,方便外界的操作!

package cn.gxm.sorm.core;

import cn.gxm.sorm.bean.Configuration;

import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.Properties;

/**
 * @author GXM www.guokangjie.cn
 * @date 2019/5/15
 *
 * 根据配置信息,维持连接对象的管理(增加连接词功能)
 */
public class DBManager {

    private static Configuration configuration = null;

    private static Properties properties = null;
    public DBManager() {
    }

    // 将数据加载到Configuration类中
    static {
        try {
            properties = new Properties();
            configuration = new Configuration();
            properties.load(Thread.currentThread().getContextClassLoader().getResourceAsStream("db.properties"));

            configuration.setDriver(properties.getProperty("driver"));
            configuration.setUrl(properties.getProperty("url"));
            configuration.setUsername(properties.getProperty("username"));
            configuration.setPassword(properties.getProperty("password"));
            configuration.setSrcPath(properties.getProperty("srcPath"));
            configuration.setPojoPackage(properties.getProperty("pojoPackage"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    /**
     * 提供connection
     * @return
     */
    public static Connection getConnection(){
        try {
            Class.forName(configuration.getDriver());
            Connection connection = DriverManager.getConnection(configuration.getUrl(),
                    configuration.getUsername(), configuration.getPassword());
            return connection;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }


    /**
     * 返回db.properties对应的bean
     * @return
     */
    public static Configuration getConfiguration() {
        return configuration;
    }
}

2.3 根据数据库的表结构生成对应的pojo

这部分知识需要用到数据库的元数据,不理解的请参考:JDBC元数据操作(一)-- DatabaseMetaData接口详解

这部分我们需要在tableContext类中完成,如下

package cn.gxm.sorm.core;

import cn.gxm.sorm.bean.ColumnInfo;
import cn.gxm.sorm.bean.Configuration;
import cn.gxm.sorm.bean.TableInfo;

import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author GXM www.guokangjie.cn
 * @date 2019/5/15
 *
 * 管理数据库中的所有表和java中的对象的关系
 * 可以根据表生成java对象
 */
public class TableContext {

    /**
     * 对应数据库中所有的表
     * key 为表的名称, value为表对应的bean
     */
    private static Map<String, TableInfo> allTables = new HashMap<>();


    private TableContext(){
    }

    /**
     * 获取数据库中的所有表以及相关字段,并生成对应的pojo
     */
    static {
        Connection connection = DBManager.getConnection();
        ResultSet rs = null;
        try {
            // 根据metaData就可以获取到数据库的源信息(表,字段等等)
            // 具体请查看 https://blog.csdn.net/chen_zw/article/details/18816599
            DatabaseMetaData metaData = connection.getMetaData();

            //处理表
            rs = metaData.getTables(null, "%", "%", new String[]{ "TABLE" });
            while (rs.next()) {
                TableInfo tableInfo = new TableInfo();
                String tableName = rs.getString("TABLE_NAME");
                tableInfo.setName(tableName);

                //处理一个表中的所有列
                ResultSet set = metaData.getColumns(null, "%", tableName, "%");
                Map<String, ColumnInfo> columnInfoMap = new HashMap<>();
                while (set.next()){
                    ColumnInfo columnInfo = new ColumnInfo(set.getString("COLUMN_NAME"),
                            set.getString("TYPE_NAME"),1);
                    columnInfoMap.put(columnInfo.getName(),columnInfo);
                }

                //处理表中的主键,跟新字段的键类型
                List<ColumnInfo> columnInfoList = new ArrayList<>();
                ResultSet primaryKeys = metaData.getPrimaryKeys(null, "%", tableName);
                while (primaryKeys.next()){
                    ColumnInfo columnInfo = columnInfoMap.get(primaryKeys.getObject("COLUMN_NAME"));
                    columnInfo.setKeyType(2);
                    columnInfoMap.put((String)primaryKeys.getObject("COLUMN_NAME"),columnInfo);
                    columnInfoList.add(columnInfo);

                    if(columnInfoList.size() == 1){
                        tableInfo.setOnlyPriKey(columnInfo);
                    }else{
                        // 说明为联合主键
                        tableInfo.setUnionKey(columnInfoList);
                        tableInfo.setOnlyPriKey(null);
                    }
                }

                tableInfo.setColumns(columnInfoMap);
                allTables.put(tableName,tableInfo);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }

    }

    /**
     * 对外提供获取数据库表信息的方法
     * @return
     */
    public static Map<String, TableInfo> getAllTables() {
        return allTables;
    }
}

在这里对该类做一个测试,数据库的两张表结构如下
company

CREATE TABLE `company` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `address` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`,`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

emp

CREATE TABLE `emp` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

测试代码:

public static void main(String[] args) {
        System.out.println(TableContext.getAllTables());
    }

结果:(这样可能不舒服,推荐大家debug一下,在debug中看的更明显)

{emp=TableInfo{name='emp', columns={name=ColumnInfo{name='name', dataType='VARCHAR', keyType=1}, id=ColumnInfo{name='id', dataType='INT', keyType=2}, age=ColumnInfo{name='age', dataType='INT', keyType=1}}, onlyPriKey=ColumnInfo{name='id', dataType='INT', keyType=2}, unionKey=null}, company=TableInfo{name='company', columns={address=ColumnInfo{name='address', dataType='VARCHAR', keyType=1}, name=ColumnInfo{name='name', dataType='VARCHAR', keyType=2}, id=ColumnInfo{name='id', dataType='INT', keyType=2}}, onlyPriKey=null, unionKey=[ColumnInfo{name='id', dataType='INT', keyType=2}, ColumnInfo{name='name', dataType='VARCHAR', keyType=2}]}}

2.4 完成Mysql字段类型与java数据类型的转换

因为这里主要使用mysql所以就写这一部分即可,而且这里并不做所有数据的转化!

2.4.1 封装mysql字段类型

这样,方便以后的使用,与扩展
类MysqlType:

package cn.gxm.sorm.core;

/**
 * @author GXM www.guokangjie.cn
 * @date 2019/5/16
 */
public class MySqlType {


    /**
     * mysql的varchar类型
     */
    public static final String VARCHAR = "varchar";
    /**
     * mysql的bigint类型
     */
    public static final String BIGINT = "bigint";
    /**
     * mysql的int类型
     */
    public static final String INT = "int";
    /**
     * mysql的integer类型
     */
    public static final String INTEGER = "integer";
    /**
     * mysql的date类型
     */
    public static final String DATE = "date";
    /**
     * mysql的time类型
     */
    public static final String TIME = "time";
    /**
     * mysql的double类型
     */
    public static final String DOUBLE = "double";
    /**
     * mysql的float类型
     */
    public static final String FLOAT = "float";
}

2.4.2 实现具体的mysql字段类型与java类型转化

写一个类MySqlTypeConvertor继承TypeConvertor

package cn.gxm.sorm.core;

import java.sql.Date;

/**
 * @author GXM www.guokangjie.cn
 * @date 2019/5/16
 *
 * 实现mysql数据库与java数据类型的转换
 */
public class MySqlTypeConvertor implements TypeConvertor{

    /**
     * 暂时不实现
     * @param javaType java数据类型
     * @return
     */
    @Override
    public String javaType2DBType(String javaType) {

        return null;
    }

    /**
     * 现mysql数据库到java数据类型的转换
     * @param columnType 数据库类型
     * @return 与之对应的Java类型
     */
    @Override
    public String DBType2javaType(String columnType) {
        if(MySqlType.VARCHAR.equalsIgnoreCase(columnType)){
            return "String";
        }else if(MySqlType.INT.equalsIgnoreCase(columnType)
                || MySqlType.BIGINT.equalsIgnoreCase(columnType)
                || MySqlType.INTEGER.equalsIgnoreCase(columnType)){
            return "Integer";
        }else if(MySqlType.DATE.equalsIgnoreCase(columnType)
                || MySqlType.TIME.equalsIgnoreCase(columnType)){
            // 注意这里转为java.sql.Date而不是java.util.date
            // 如果直接转换为utils.date,则需要具体的实现,用到时,我们再转化即可
            return "java.sql.Date";
        }else if(MySqlType.DOUBLE.equalsIgnoreCase(columnType)){
            return "Double";
        }else if(MySqlType.FLOAT.equalsIgnoreCase(columnType)){
            return "Float";
        }

        // 这里不全部转化
        return "";
    }

}

三、完成javaFileUtils

上一部分我们已经做到了,可以根据数据库的表获取其中的元数据了,接下就是根据元数据完成pojo,说白了,就是拼接字符串,并生成文件

3.1 为完成javaFileUtils做出必要的准备

3.1.1 为更好的解耦,我们抽取公用的方法放到工具类中

StringUtils:

 /**
     * 将字符串的首字母大写
     * @param src
     * @return
     */
    public static String toUpperCaseHeadChar(String src){
        return src.toUpperCase().substring(0,1)+src.substring(1);
    }

3.1.2 设计一个属性的字段,与set,get方法

JavaFieldSetGet (set,get方法省略)

package cn.gxm.sorm.bean;

/**
 * @author GXM www.guokangjie.cn
 * @date 2019/5/17
 *
 * 每一个pojo都有的java属性,set,get等等
 */
public class JavaFieldSetGet {

    /**
     * 例如  (private String id)
     */
    private String fieldInfo;

    /**
     * 例如
     * public String getId(){
     *     return id;
     * }
     */
    private String fieldGetInfo;

    /**
     * 例如
     * public void setId(String id){
     *     this.id = id;
     * }
     */
    private String fieldSetInfo;
    @Override
    public String toString() {
        return fieldInfo+fieldGetInfo+fieldSetInfo;
    }
}

3.2 java的file生成我们在javaFileUtils工具类里面完成,其主要方法如下

  • generateJavaFieldSetGet 每一个pojo都有属性和set,get方法,我们生成说白了就是字符串
    该方法 测试结果如图:写出你自己的ORM框架(一)_第1张图片

  • generateJavaSrc 根据上面的方法我们可以完成一个属性的字符串所有功能,所以我们这里再将表的数据也拼接成字符串,合并最总变为一个java的pojo(只不过是字符串)
    该方法 测试结果如图:
    写出你自己的ORM框架(一)_第2张图片

  • generatePOJOFile 对外提供方法生成pojo文件

其代码如下:

/**
 * @author GXM www.guokangjie.cn
 * @date 2019/5/15
 *
 * 操作文件(暂时先将columnInfo转为对应的java代码)
 */
public class JavaFileUtils {


    /**
     * 根据传入的数据库字段信息生成java的bean代码
     * varchar id -> private String id 以及相应set与get方法
     * @param columnInfo 数据库字段信息
     * @param typeConvertor 对应的数据库字段数据类型转换
     * @return 一个属性的bean代码
     */
    private static JavaFieldSetGet generateJavaFieldSetGet(@NotNull ColumnInfo columnInfo,
                                                          @NotNull TypeConvertor typeConvertor){
        if(columnInfo==null || typeConvertor == null){
            throw new SormException("ColumnInfo or TypeConvertor is null");
        }

        JavaFieldSetGet fieldSetGet = new JavaFieldSetGet();

        // 字段名称 id
        String columnName = columnInfo.getName();
        String javaFieldType = typeConvertor.DBType2javaType(columnInfo.getDataType());

        //private String id
        String fieldInfo = "\tprivate "+ javaFieldType +" "+ columnName +";\n";

        /**
         * public void getId(){
         *      return id;
         * }
         */
        StringBuilder fieldGetInfo = new StringBuilder("\tpublic  "+javaFieldType+" get"+StringUtils.toUpperCaseHeadChar(columnName)+" (){\n");
        fieldGetInfo.append("\t\treturn "+ columnName+";\n");
        fieldGetInfo.append("\t}\n");

        /**
         * public void setId(String id){
         *      this.id = id;
         * }
         */
        StringBuilder fieldSetInfo = new StringBuilder("\tpublic void set"+StringUtils.toUpperCaseHeadChar(columnName)+" ("+javaFieldType+" "+columnName+"){\n");
        fieldSetInfo.append("\t\tthis."+columnName+"="+ columnName+";\n");
        fieldSetInfo.append("\t}\n");

        fieldSetGet.setFieldInfo(fieldInfo);
        fieldSetGet.setFieldGetInfo(fieldGetInfo.toString());
        fieldSetGet.setFieldSetInfo(fieldSetInfo.toString());

        return fieldSetGet;
    }


    /**
     * 根据表结构生成对应的pojo
     * @param tableInfo 表信息
     * @param typeConvertor java与数据库字段类型转化器
     * @param author 生成的Pojo的作者
     * @return pojo字符串
     */
    private static String generateJavaSrc(@NotNull TableInfo tableInfo,
                                         @NotNull TypeConvertor typeConvertor,String author){

        if(tableInfo==null || typeConvertor == null){
            throw new SormException("tableInfo or TypeConvertor is null");
        }

        StringBuilder javaResourse = new StringBuilder();
        // 生成package 例如 package cn.gxm.sorm.pojo
        javaResourse.append("package "+ DBManager.getConfiguration().getPojoPackage()+";\n\n");

        // 生成import 例如 import java.sql.Date
        javaResourse.append("import java.sql.*;\n");
        javaResourse.append("import java.lang.*;\n");

        // 生成autor 以及日期等java文件说明
        /**
         * @author GXM www.guokangjie.cn
         * @date 2019/5/17
         */
        javaResourse.append("/**\n")
                .append(" * @author "+author+"\n")
                .append(" * @date "+new SimpleDateFormat("yyyy/MM/dd").format(new Date())+"\n")
                .append(" */\n");
        // 生成类的开始 例如 public class Emp {
        javaResourse.append("public class "+StringUtils.toUpperCaseHeadChar(tableInfo.getName())+"{\n");

        // 生成类的属性
        Set<Map.Entry<String, ColumnInfo>> entries = tableInfo.getColumns().entrySet();
        for (Map.Entry<String, ColumnInfo> entry: entries){
            JavaFieldSetGet fieldSetGet = generateJavaFieldSetGet(entry.getValue(), typeConvertor);
            javaResourse.append(fieldSetGet+"\n");
        }

        // 生成类的结束  }
        javaResourse.append("}");
        return javaResourse.toString();

    }


    /**
     * 生成与数据库字段匹配的java的pojo
     * @param tableInfo 表信息
     * @param typeConvertor java与数据库字段类型转化器
     * @param author 生成的Pojo的作者
     */
    public static void generatePOJOFile(TableInfo tableInfo,TypeConvertor typeConvertor,String author){
        String javaSrc = generateJavaSrc(tableInfo, typeConvertor, author);
        String dirPath = DBManager.getConfiguration().getSrcPath()+"//"+DBManager.getConfiguration().getPojoPackage().replaceAll("\\.","//");
        File pojoDir = new File(dirPath);
        //包不存在则创建包
        if(!pojoDir.exists()){
            pojoDir.mkdirs();
        }

        Writer writer = null;
        try {
            writer = new FileWriter(pojoDir.getAbsoluteFile()+"//"+StringUtils.toUpperCaseHeadChar(tableInfo.getName())+".java");
            writer.write(javaSrc);
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                writer.close();
                System.out.println("pojo创建成功!!");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

四、完善TableContext

我们已经在TableContext类中获取了所有的数据库源信息(第一部分与第二部分),而在第三部分我们也完成了根据tableInfo来生成pojo的(.java文件),但是JavaFileUtils总归是一个工具类,我们将在TableContext对外提供生成的方法,即在TableContext类中添加方法

 /**
     * 生成或者跟新java的pojo与数据库的对应关系
     */
    public static void generateOrUpdateJavaFilePojo(){
        Set<Map.Entry<String, TableInfo>> entries = allTables.entrySet();
        for (Map.Entry<String, TableInfo> entry: entries){
            JavaFileUtils.generatePOJOFile(entry.getValue(),new MySqlTypeConvertor(),"GXM www.guokangjie.cn");
        }
    }

测试
配置正确的db.properties,就会在指定位置生成与数据库对应的pojo,如下图
写出你自己的ORM框架(一)_第3张图片

你可能感兴趣的:(自己的ORM框架)