写在前面
ORM框架,关系映射框架,只需要提供持久化类和数据库表之间的关系,就可以在运行时候进行数据持久化,通常javaweb使用hibernate或者mybatis,本文就是写了一个简易版的orm框架。。只是实现了部分功能
功能介绍
1.可以根据表结构自动生成实体类
2.可直接插入,修改删除对象,不需要写sql语句
3.在查询时候需要自己写SQL语句,支持多行,单行,某一个字段的查询
4.支持自定义实体类查询,当在进行连表查询的时候,得到的数据是自定义的,可自定义一个实体类进行接收
5.连接使用了连接池,效率相对高一点
1.接口设计
Query接口:符合查询,对外提供服务的核心类
QueryFactory类:管理query对象
TypeConvertor接口:负责数据类型转换
TableContext类:负责获取数据库的表结构和类结构的关系,并可以根据表结构生成类结构,
DBManager类:根据配置信息,维持连接对象的管理
工具类
JdbcUtils 用来封装通用的jdbc操作
StringUtils 用来封装常用的字符串操作
ReflectUtils 用来封装反射相关的操作
PathUtils 用来操作本地文件路径相关的操作
JavaFileUtils
核心beanCOlumnInfo 封装表中一个字段的信息
Configuration 封装整个项目的配置信息
TableInfo: 封装一张表的信息
JavaFeildInfo:用于封装一个属性和get set方法的数据(在自动生成java文件的时候用)
2.具体实现
1.query接口设计
框架封装了常用的几个方法
//将某个实体类持久化到数据库中public void insert(Object object);//从数据库中删除某个数据 public void delete(Object object)//从数据库中删除某条数据,指定id public void delete(Class clazz,Object id) ;//更新某条数据public void update(Object object,String[] fieldNames);//根据传入的sql语句进行 查询数据,某个类型的数据 public List queryRows(String sql,Class clazz,Object[] params);//查找某一列的数据 public Object queryValue(String sql,Object[] params);//执行增删改sql语句的方法public int executeDML(String sql,Object[] params);
2.数据类型转换器
因为不知道项目用的什么数据库,所以数据类型也不一样,类型转换器是一个接口,提供了实现,现在只提供了mysql数据类型转换器
2.2.1
转换器有两个方法,一个是java数据类型转换成数据库数据类型,另一个是数据库类型转换成java数据类型,因为此项目需要根据数据库表生成java文件,所以只实现了数据库类型转换成java数据类型,如果需要另外转换,只需要反过来即可
3.Configuration封装配置信息
配置文件是db.properties,在src下
driver = com.mysql.jdbc.Driver //数据库驱动username = root //用户名password = 123456 //密码useDB = mysql //声明使用的数据库url = jdbc:mysql://localhost:3306/test // 连接urlpackageName = jk.zmn.sorm.pojo //声明自动生成的实体类放在当前项目哪个包下,可不存在
这个类主要是用来封装用户的配置信息,将配置信息保存起来,使用更方便,配置类即是将这个属性都封装起来
4.DBManager类
这个类主要是用来,读取用户的配置信息然后将信息封装到configuration类中,管理数据库连接,等功能
2.3.1 封装配置信息
图2.3.1封装配置信息
2.3.2
图2.3.2是管理数据库连接,后期将会增加连接池功能
4.TableContext类
此类用来连接数据库,得到库中的表,并封装起来,并且根据表信息自动生成java文件,当生成java实体类的时候,需要使用map将实体类的Class文件和表信息对应存储起来,用于数据持久化,后面会讲到
5.自动生成java实体类实现
约定:如何根据表信息生成java类呢,我们先看看平时手动生成的java实体类是什么样子的,如表user,或_user 或t_user 再看看字段,id,name,或者t_id,t_name,我们自己写实体类会写成什么样子呢,Class User 属性写成 id,name或tname,tid, 那我们就先来个约定,如表名user,生成的类就叫User,首字母大写,去掉下划线,字段也是一样,只需要去掉下划线,get和set方法是getId 或者setName,首字母大写,
思路: 上面我们已经得到了数据库中所有的表信息,也有了生成实体类的约定,那到底怎么生成呢,1.我们确定类名,根据表名得到得到类名, 2.属性如何生成呢,还有get set方法,我们将一个属性和一个getset 方法封装在一个类中,根据表中的字段信息,来生成一个字段对应的属性的相关信息,然后将所有字段的信息整合在这个类中,具体实现一下,我们还缺一些东西
5.1 ColumnInfo 类
封装表中一个字段的信息,什么意思,
封装了三个属性,
/** * 字段名称 */ private String name; /** * 字段数据类型 */ private String dataType; /** * 字段键类型 0普通键,1主键 2外键 */ private int keyType;
5.2TableInfo
封装一张表的信息
/** * 表名 */ private String name; /** * 存放字段信息, 字段名和字段信息 */ private Map<String,ColumnInfo> columns; /** * 主键信息 */ private ColumnInfo onlyPriKey; /** * 联合主键 */ private List<ColumnInfo> priKeys;
5.3JavaFeildInfo
用于封装一个属性和get set方法的数据,最后拼接类文件的时候用
/** * 属性信息 private String feild */ private String feildInfo; /** * 封装get方法 */ private String getFeildInfo; /** * 封装set方法 */ private String setFeildInfo;
有了上面的基本数据,就可以进行操作了
5.4 开始生成文件
上面介绍了tablecontext类,用来得到数据库中的表,使用map封装起来,附上源码,接着下面看
/** * 表名为key,表信息对象为value */ public static Map<String,TableInfo> tables = new HashMap<String,TableInfo>(); /** * 将po的class对象和表信息对象关联起来,便于重用! */ public static Map<Class,TableInfo> poClassTableMap = new HashMap<Class,TableInfo>(); private TableContext(){} static { try { //初始化获得表的信息 Connection con = DBManager.getConnection(); DatabaseMetaData dbmd = con.getMetaData(); ResultSet tableRet = dbmd.getTables(null, "%","%",new String[]{"TABLE"}); while(tableRet.next()){ String tableName = (String) tableRet.getObject("TABLE_NAME"); TableInfo ti = new TableInfo(tableName, new HashMap<String, ColumnInfo>(),new ArrayList<ColumnInfo>()); tables.put(tableName, ti); ResultSet set = dbmd.getColumns(null, "%", tableName, "%"); //查询表中的所有字段 while(set.next()){ ColumnInfo ci = new ColumnInfo(set.getString("COLUMN_NAME"), set.getString("TYPE_NAME"), 0); ti.getColumns().put(set.getString("COLUMN_NAME"), ci); } ResultSet set2 = dbmd.getPrimaryKeys(null, "%", tableName); //查询t_user表中的主键 while(set2.next()){ ColumnInfo ci2 = (ColumnInfo) ti.getColumns().get(set2.getObject("COLUMN_NAME")); ci2.setKeyType(1); //设置为主键类型 ti.getPriKeys().add(ci2); } if(ti.getPriKeys().size()>0){ //取唯一主键。。方便使用。如果是联合主键。则为空! ti.setOnlyPriKey(ti.getPriKeys().get(0)); } } } catch (SQLException e) { e.printStackTrace(); } //生成java文件 updatePoFile(); //将对应的类和表封装起来 loadTablePoToMap(); } /** * 根据数据库表结构生成pojo类 */ public static void updatePoFile(){ JavaFileUtils.createJavaFileToPackage(); } /** * 加载完数据库中的表,生成pojo之后,把对应的类和对应的表关联起来 * @date 2018/6/15 19:44 * @param * @return void */ public static void loadTablePoToMap(){ for (TableInfo tableInfo : tables.values()){ //实体类 Class<?> aClass = null; try { aClass = Class.forName(DBManager.getConfiguration().getPackageName() +"."+StringUtils.UpFirstString(tableInfo.getName())); } catch (ClassNotFoundException e) { e.printStackTrace(); } poClassTableMap.put(aClass,tableInfo); } }
5.5 使用javafileutils生成java文件
讲一下思路:得到了所有的表信息,如何拼接成一个类
1.首先要先得到类名
根据表信息,得到java文件的类名,
2.得到所有的属性和get set方法
得到表的一个字段信息,将字段的数据类型转换成java数据类型,将字段名转换成类的属性名,拼接字符串,就得到了一个定义属性的字符串,然后封装get和set方法,将这一个属性的信息,封装在FieldInfo中,循环将所有的属性都封装完毕,
3.拼装整个类文件
将包名,和类名和属性一起封装起来,然后使用io流将文件输出,问题来了,输出到哪里,指定了包,包不存在,使用PathUtils,将包名穿进去,就会自动创建包,
附源码,
PathUtils
String str=packageName; //"jk.zmn.auto.dfd"; str = str.replace(".","\\"); File file = new File(""); String canonicalPath = null; try { canonicalPath = file.getCanonicalPath(); } catch (IOException e) { e.printStackTrace(); } canonicalPath += "\\src\\"+str;// String resource = Thread.currentThread().getContextClassLoader().getResource("/").getPath();// resource = resource.substring(1);// System.out.println(resource); File packageFile = new File(canonicalPath); if (!packageFile.exists()){ packageFile.mkdirs(); } return canonicalPath; }
JavaFileutils
private static Configuration configuration = DBManager.getConfiguration(); /** * private String name; * <p> * private String dataType; * <p> * private int keyType; */ public static javaFeildInfo createJavaFeild(ColumnInfo columnInfo, TypeConvertorHandler convertorHandler) { //将字段数据类型转换成java数据类型 String javaType = convertorHandler.JdbcType2JavaType(columnInfo.getDataType()); String columnName = columnInfo.getName().toLowerCase(); javaFeildInfo feildInfo = new javaFeildInfo(); //生成属性语句 feildInfo.setFeildInfo("\tprivate " + javaType + " " + StringUtils.trimUnderLine(columnName) + ";\n"); StringBuilder sb = new StringBuilder(); sb.append("\tpublic " + javaType + " " + "get" + StringUtils.UpFirstString(columnName) + "() {\n"); sb.append("\t\treturn " + columnName + ";\n"); sb.append("\t}\n"); feildInfo.setGetFeildInfo(sb.toString()); StringBuilder sb1 = new StringBuilder(); sb1.append("\tpublic void " + "set" + StringUtils.UpFirstString(columnName) + "(" + javaType + " " + columnName + ") {\n"); sb1.append("\t\t this." + columnName + " = " + columnName + ";\n"); sb1.append("\t}\n"); feildInfo.setSetFeildInfo(sb1.toString()); return feildInfo; } public static void createJavaFile(TableInfo tableInfo, TypeConvertorHandler typeConvertorHandler){ //得到所有的列信息 Map<String, ColumnInfo> columns = tableInfo.getColumns(); ArrayList<javaFeildInfo> javaFeildInfos = new ArrayList<>(); Collection<ColumnInfo> values = columns.values(); //生成所有的java属性信息和get set方法 for (ColumnInfo columnInfo : values){ javaFeildInfo javaFeild = createJavaFeild(columnInfo, typeConvertorHandler); javaFeildInfos.add(javaFeild); } StringBuilder sb = new StringBuilder(); sb.append("package "+configuration.getPackageName()+";\n\n"); sb.append("import java.sql.*;\n"); sb.append("import java.util.*;\n\n"); sb.append("public class "+StringUtils.UpFirstString(tableInfo.getName())+" {\n\n"); for (javaFeildInfo javaFeildInfo: javaFeildInfos){ sb.append(javaFeildInfo.getFeildInfo()); } sb.append("\n"); for (javaFeildInfo javaFeildInfo: javaFeildInfos){ sb.append(javaFeildInfo.getGetFeildInfo()); } for (javaFeildInfo javaFeildInfo: javaFeildInfos){ sb.append(javaFeildInfo.getSetFeildInfo()); } sb.append("}\n"); //System.out.println(sb.toString()); String classInfo = sb.toString(); String filePathFromPackage = PathUtils.getFilePathFromPackage(configuration.getPackageName()); File file = new File(filePathFromPackage, StringUtils.UpFirstString(tableInfo.getName()) + ".java"); BufferedOutputStream bufferedOutputStream=null; try { bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(file)); bufferedOutputStream.write(classInfo.getBytes(),0,classInfo.getBytes().length); bufferedOutputStream.flush(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally { try { bufferedOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } System.out.println("表"+tableInfo.getName()+"对应的类"+StringUtils.UpFirstString(tableInfo.getName())+"已自动生成.."); } public static void createJavaFileToPackage(){ Map<String, TableInfo> tables = TableContext.tables; TypeConvertorHandler convertorHandler = null; if (configuration.getUseDB().equalsIgnoreCase("mysql")){ convertorHandler = new MysqlConvertorHandler(); } for (TableInfo tableInfo:tables.values()){ createJavaFile((tableInfo),convertorHandler); } }
6.查询实现
如今实体类已经有了,那么如何对数据进行操作呢
,上面我们定义了一个Query接口,不同数据库查询可能不一样,所以做一个mysqlQuery的实现类,做mysql的查询
简单介绍添加方法,
传入实体类,如何持久化到数据库呢,还记得我们前面在得到数据库表,生成java文件的时候将,实体类的Class对象和表名存储起来吗,现在用到了哦
得到该类的Class对象,从map中取出表名,拼接sql语句,通过反射,得到该对象的所有的属性,判断是否为空,将不为空的属性插入,那么该类的属性的值怎么获取呢,还是反射,根据属性的get和set方法,反射调用该方法,得到属性的值,将值和sql一起传入执行sql语句的方法,就完成持久化了
// insert into logs(a,b) values (?,?) //得到类对应的表信息 Class<?> aClass = object.getClass(); TableInfo tableInfo = TableContext.poClassTableMap.get(aClass); StringBuilder sb = new StringBuilder("insert into "+tableInfo.getName()+"("); //得到属性 Field[] fields = aClass.getDeclaredFields(); ArrayList<Object> fieldValueList = new ArrayList<>(); for (Field field : fields){ String name = field.getName(); Object value = ReflectUtils.invokeGet(object, name); if (value!=null){ sb.append(name+","); fieldValueList.add(value); } } //将最后一个,换成) sb.setCharAt(sb.length()-1,')'); sb.append(" values("); for (int i=0;i<fieldValueList.size();i++){ sb.append("?,"); } sb.setCharAt(sb.length()-1,')'); executeDML(sb.toString(),fieldValueList.toArray());
其他方法
/** * 删除一个对象 * @date 2018/6/15 16:54 * @param object 要移除的对象 * @return void */ public void delete(Object object){ Class<?> aClass = object.getClass(); TableInfo tableInfo = TableContext.poClassTableMap.get(aClass); //得到表的主键 ColumnInfo onlyPriKey = tableInfo.getOnlyPriKey(); String sql = "delete from "+tableInfo.getName()+" where "+onlyPriKey.getName()+"=?"; //反射调用get方法,得到属性的值 Object o = ReflectUtils.invokeGet(object, onlyPriKey.getName()); executeDML(sql,new Object[]{o}); } /** * 删除类 对应的表中的数据,删除该id的对象 * @date 2018/6/15 16:55 * @param clazz 类对象 * @param id 主键 * @return void */ public void delete(Class clazz,Object id) { // delete from logs where id=? TableInfo tableInfo = TableContext.poClassTableMap.get(clazz); ColumnInfo onlyPriKey = tableInfo.getOnlyPriKey(); String sql = "delete from "+tableInfo.getName()+" where "+onlyPriKey.getName()+"=?"; executeDML(sql.toString(),new Object[]{id}); } /** * 更新对象字段的信息 * @date 2018/6/15 16:57 * @param object 对象 * @param fieldNames 多个字段 * @return void */ public void update(Object object,String[] fieldNames){ //obj{"uanme","pwd"}-->update 表名 set uname=?,pwd=? where id=? Class c = object.getClass(); List<Object> params = new ArrayList<Object>(); //存储sql的参数对象 TableInfo tableInfo = TableContext.poClassTableMap.get(c); ColumnInfo priKey = tableInfo.getOnlyPriKey(); //获得唯一的主键 StringBuilder sql = new StringBuilder("update "+tableInfo.getName()+" set "); for(String fname:fieldNames){ Object fvalue = ReflectUtils.invokeGet(object,fname); params.add(fvalue); sql.append(fname+"=?,"); } sql.setCharAt(sql.length()-1, ' '); sql.append(" where "); sql.append(priKey.getName()+"=? "); params.add(ReflectUtils.invokeGet(object,priKey.getName())); //主键的值 executeDML(sql.toString(), params.toArray()); } /** * 根据参数,查询指定的数据,多行记录,单行记录可直接get(0) * @date 2018/6/15 16:59 * @param sql sql语句 * @param clazz 类对象 * @param params sql语句参数 * @return java.util.List */ public List queryRows(String sql,Class clazz,Object[] params){ Connection connection = DBManager.getConnection(); PreparedStatement ps = null; List<Object> rows = new ArrayList<Object>(); ResultSet resultSet = null; try { ps = connection.prepareStatement(sql); JdbcUtils.handlerParams(ps,params); resultSet = ps.executeQuery(); //得到返回结果又多少列 ResultSetMetaData metaData = resultSet.getMetaData(); while (resultSet.next()){ Object o = clazz.newInstance(); for (int i=0;i<metaData.getColumnCount();i++){ //得到每一列的名称 String columnLabel = metaData.getColumnLabel(i + 1); Object columnValue = resultSet.getObject(i + 1); ReflectUtils.invokeSet(o, columnLabel, columnValue); } rows.add(o); } return rows; } catch (Exception e) { e.printStackTrace(); }finally { DBManager.close(connection,ps); } return null; } /** * 查询某个字段的数据 * @date 2018/6/15 17:05 * @param sql sql语句 * @param params 参数 * @return java.lang.Object 封装查询到的数据 */ public Object queryValue(String sql,Object[] params){ Connection connection = DBManager.getConnection(); PreparedStatement ps = null; ResultSet resultSet = null; Object o =null; try { ps = connection.prepareStatement(sql); JdbcUtils.handlerParams(ps,params); resultSet = ps.executeQuery(); while (resultSet.next()){ o = resultSet.getObject(1); } } catch (Exception e) { e.printStackTrace(); return null; }finally { DBManager.close(connection,ps); } return o; } /** * 执行sql语句 * @date 2018/6/15 16:50 * @param sql sql语句 * @param params 参数 * @return int SQL影响的行数 * */ public int executeDML(String sql,Object[] params){ Connection connection = DBManager.getConnection(); int count = 0; PreparedStatement ps = null; try { ps = connection.prepareStatement(sql); JdbcUtils.handlerParams(ps,params); System.out.println(ps); count = ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); }finally { DBManager.close(connection,ps); } System.out.println("count:"+count); return count; }
以上,基础的增删改查就可以实现了
7.项目修改
1.工厂模式得到Query对象,
private static QueryFactory queryFactory = new QueryFactory(); private static Class c; static { if (DBManager.getConfiguration().getUseDB().equalsIgnoreCase("mysql")){ try { c = Class.forName("jk.zmn.sorm.core.MySqlQuery"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } /* 构造器私有化 */ private QueryFactory(){ } public static Query createQuery(){ try { return (Query) c.newInstance(); } catch (Exception e) { e.printStackTrace(); return null; } }
2.添加连接池功能
当对数据库操作很频繁的时候,如果每次都新建链接,关闭链接,很耗资源,使用连接池保存链接,提高项目的效率
/** * 连接池对象 */ private List<Connection> pool; /** * 最大连接数 */ private static final int POOL_MAX_SIZE = DBManager.getConfiguration().getPoolMaxSize(); /** * 最小连接池 */ private static final int POOL_MIN_SIZE = DBManager.getConfiguration().getPoolMinSize(); /** * 初始化连接池,使池中的连接数达到最小值 */ public void initPool() { if(pool==null){ pool = new ArrayList<Connection>(); } while(pool.size()<DBPool.POOL_MIN_SIZE){ pool.add(DBManager.getConnection()); System.out.println("初始化池,池中连接数:"+pool.size()); } } /** * 从连接池中取出一个连接 * @return */ public synchronized Connection getConnection() { int last_index = pool.size()-1; Connection conn = pool.get(last_index); pool.remove(last_index); return conn; } /** * 将连接放回池中 * @param conn */ public synchronized void close(Connection conn){ if(pool.size()>=POOL_MAX_SIZE){ try { if(conn!=null){ conn.close(); } } catch (SQLException e) { e.printStackTrace(); } }else { pool.add(conn); } } public DBPool() { initPool(); }
3.测试
如此一来,项目大致就结束了,直接打成jar包,由别的项目引入
新建项目,导入此jar包
添加
image.png
image.png
image.png
这里的count是影响数据库的行数
查询
image.png
image.png
演示到这里
源码看这里:https://gitee.com/zhangqiye/SORM
作者:z七夜
链接:https://www.jianshu.com/p/c13870967d9b