一、ORM介绍
ORM(Object-Relationl Mapping)对象关系映射,一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术。它的作用是在关系型数据库和对象之间作一个映射,这样,我们在具体的操作数据库的时候,就不需要再去和复杂的SQL语句打交道,只要像平时操作对象一样操作它就可以了 。
基本思想
- 表结构和类结构对应
- 表中字段属性和类的属性对应
- 表中的记录与对象对应
SORM框架
简单的orm框架,实现数据库表和po实体类的映射,通过数据库连接池获取连接,提供简单的CRUD方法。
二、框架简介
目录结构
架构设计
框架分析
目前实现对MySql数据库的操作。使用的资源文件提供数据库连接池信息和项目相关信息,创建数据库连接池。在连接上数据库后直接读取表信息生成对应的po类文件。使用工厂设计模式创建query对象实现对mysql数据库的CRUD操作。
由于目前的增删改操作都是以主键作为查询条件,目前只支持数据库表中的主键只有一个字段的情况进行查询。若该表的主键由多个字段构成,在获取到主键后也只会使用主键的第一个字段,可能导致在执行数据库操作时出现错误的情况。
1.core
- BaseQuery抽象类——负责查询(对外提供服务的核心类)
- QueryFactory类——负责更具配置信息创建BaseQuery对象
- TableContext类——负责获取和管理数据库所有表结构和类结构的关系,并可以根据表结构生成对应的po实体类文件
- TypeConvertor接口——负责数据库字段类型到java属性类型的转换
- ConnectionPool——负责构建数据库连接池,同时根据配置信息加载configuration类
- DBManager——负责维持从连接池获取的链接对象的管理
- CallBack接口——负责提供使用模板方法时的回调方法
- MySqlQuery类——负责提供执行MySql数据库查询时独有的方法,继承自BaseQuery抽象类,目前对MySql的操作只使用BaseQuery中的方法。
- MySqlTypeConvertor——负责数据库字段属性与java实体类属性之间的转换
2.bean
- ColumnInfo——封装数据库表一个字段的信息(字段名、字段类型、键类型)
- Configuration——封装了配置文件的信息
- JavaField——封装了po类的属性,用于生成po类的java文件
- TableInfo——封装了数据库表的信息(表名,字段、主键)
3.util
- JDBCUtils——封装常用的JDBC操作
- JavaFileUtils——封装生成po类java文件的操作
- ReflectUtils——封装常用的反射操作
- StringUtils——封装常用的字符串操作
三、详细分析
bean对象分析
框架内共有4个bean对象,以下只展示bean对象中的属性代码。
1.ColumnInfo
/**
* 封装表中一个字段的信息
* @author xxbb
*/
public class ColumnInfo {
/**
* 字段名
*/
private String name;
/**
* 字段的数据类型
*/
private String dataType;
/**
* 字段的键类型(0:普通键,1:主键,2:外键)
*/
private int keyType;
}
2.Configuration
/**
* 管理配置文件信息
* @author xxbb
*/
public class Configuration {
/**
* 使用数据库类型名称:mysql
*/
private String usingDB;
/**
* 数据库连接驱动
*/
private String driverClass;
/**
* 连接url
*/
private String url;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 项目绝对路径
*/
private String srcPath;
/**
* 与数据库表对应的po类所在的包名(po:persistence object 持久化对象)
*/
private String poPackage;
/**
* 当前使用的数据库名称
*/
private String catalog;
/**
* 当前使用的查询类的路径
*/
private String queryClass;
}
3.JavaField
/**
* 封装了java属性的get,set方法
* @author xxbb
*/
public class JavaField {
/**
* 属性的源码信息:如 private int id
*/
private String fieldInfo;
/**
* get方法的源码信息,如 public int getId()
*/
private String getInfo;
/**
* set方法的源码信息,如 public void setId()
*/
private String setInfo;
}
4.TableInfo
**
* 存储表结构信息
* @author xxbb
*/
public class TableInfo {
/**
* 表名
*/
private String tableName;
/**
* 所有字段信息
*/
private Map<String,ColumnInfo> colums;
/**
* 唯一主键(目前只考虑表中只有一个主键的情况)
*/
private ColumnInfo uniquePrimaryKey;
/**
* 联合主键(拓展)
*/
private List<ColumnInfo> unionPrimaryKey;
}
util工具分析
1.JavaFileUtils
类中封装了三个方法,分别是:
- createJavaField()——用于数据库字段对应java类的属性、get和set方法生成,封装成一个JavaField对象
- createJavaSrc()——用于调用createJavaField()方法生成JavaField对象并将所有对象整合起来封装成一个包含po类所有内容的字符串。
- createJavaPoFile()——用于调用createJavaSrc()方法获取整个po类字符串,读取配置文件获取po包位置,使用处理字节流BufferWrite向po包路径下写入粕类文件
/**
* 封装了生成java文件的操作
* @author xxbb
*/
public class JavaFileUtils {
public static void createJavaPoFile(TableInfo tableInfo,TypeConvertor typeConvertor){
String src=createJavaSrc(tableInfo,typeConvertor);
String srcPath=DBManager.getConfiguration().getSrcPath();
String packagePath=DBManager.getConfiguration().getPoPackage().replaceAll("\\.","\\\\");
File poFile=new File(srcPath+"/"+packagePath);
if(!poFile.exists()){
Boolean flag=poFile.mkdirs();
}
BufferedWriter bufferedWriter=null;
try {
File po=new File(srcPath+"/"+packagePath+"/"+
StringUtils.tableNameToClassName(tableInfo.getTableName())+".java");
bufferedWriter=new BufferedWriter(new FileWriter(po));
bufferedWriter.write(src);
bufferedWriter.close();
System.out.println("更新了数据库表 "+tableInfo.getTableName()+"的对应类:"+StringUtils.tableNameToClassName(tableInfo.getTableName()));
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 根据表信息生成java类的源代码
* @param tableInfo 表信息
* @param typeConvertor 类型转化器
* @return java类源代码
*/
public static String createJavaSrc(TableInfo tableInfo,TypeConvertor typeConvertor){
//获取所有字段
Map<String,ColumnInfo> columnInfoMap=tableInfo.getColums();
//将所有字段转化为javaField的数组
List<JavaField> javaFieldList=new ArrayList<>();
for(ColumnInfo c:columnInfoMap.values()){
javaFieldList.add(createJavaField(c,typeConvertor));
}
//构建源文件
StringBuilder srcStr=new StringBuilder();
//生成package语句
srcStr.append("package ").append(DBManager.getConfiguration().getPoPackage()).append(";\n\n");
//生成import语句
srcStr.append("import java.sql.*;\n");
srcStr.append("import java.util.*;\n\n");
//生成类声明
srcStr.append("public class ").append(StringUtils.tableNameToClassName(tableInfo.getTableName())).append("{\n\n");
//生成属性列表
for(JavaField j:javaFieldList){
srcStr.append(j.getFieldInfo());
}
srcStr.append("\n");
//生成构造方法
srcStr.append("\tpublic ").append(StringUtils.tableNameToClassName(tableInfo.getTableName())).append("(){}\n");
srcStr.append("\n");
//生成get方法
for(JavaField j:javaFieldList){
srcStr.append(j.getGetInfo());
}
srcStr.append("\n");
//生成set方法
for(JavaField j:javaFieldList){
srcStr.append(j.getSetInfo());
}
srcStr.append("\n");
srcStr.append("}");
return srcStr.toString();
}
/**
* 根据字段信息生成java属性信息,如varchar username——>private String username以及它的get,set方法
* @param columnInfo 字段信息
* @param typeConvertor 类型转化器
* @return java属性和get,set方法
*/
public static JavaField createJavaField(ColumnInfo columnInfo, TypeConvertor typeConvertor){
JavaField javaField=new JavaField();
//获取字段类型有的时候是值是大写的,故在末尾添加toLowerCase()方法
String javaFieldType=typeConvertor.databaseTypeToJavaType(columnInfo.getDataType().toLowerCase());
//构建属性
javaField.setFieldInfo("\tprivate "+javaFieldType+" "+StringUtils.lineToHump(columnInfo.getName())+";\n");
//构建get方法
StringBuilder getStr=new StringBuilder();
getStr.append("\tpublic ").append(javaFieldType).append(" get").append(StringUtils.columnNameToMethodName(columnInfo.getName())).append("(){\n");
getStr.append("\t\treturn ").append(StringUtils.lineToHump(columnInfo.getName())).append(";\n");
getStr.append("\t}\n");
javaField.setGetInfo(getStr.toString());
//构建set方法
StringBuilder setStr=new StringBuilder();
setStr.append("\tpublic void").append(" set").append(StringUtils.columnNameToMethodName(columnInfo.getName()));
setStr.append("(").append(javaFieldType).append(" ").append(StringUtils.lineToHump(columnInfo.getName())).append("){\n");
setStr.append("\t\tthis.").append(StringUtils.lineToHump(columnInfo.getName())).append("=").append(StringUtils.lineToHump(columnInfo.getName())).append(";\n");
setStr.append("\t}\n");
javaField.setSetInfo(setStr.toString());
return javaField;
}
public static void main(String[] args) {
Map<String,TableInfo> map= TableContext.getDatabaseTableMap();
TableInfo t=map.get("t_user");
createJavaPoFile(t,new MySqlTypeConvertor());
}
}
2.JDBCUtils
- handleParams()——将预处理sql语句的PreparedStatement对象中的占位符?替换成具体的参入值
/**
* 封装的JDBC查询常用的操作
* @author xxbb
*/
public class JDBCUtils {
/**
* 给PreparedStatement语句设置参数
* @param ps
* @param params
*/
public static void handleParams(PreparedStatement ps,Object[] params){
if(params!=null){
for(int i=0;i<params.length;i++){
try{
ps.setObject(1+i,params[i]);
}catch (SQLException e){
e.printStackTrace();
}
}
System.out.println("准备执行的sql语句:"+ps);
}
}
}
3.ReflectUtils
- invokeGet()——通过反射调用传入对象的get方法
- invokeSet()——通过反射调用传入对象的set方法
/**
* 封装反射常用操作
* @author xxbb
*/
public class ReflectUtils {
/**
* 调用Object对应属性的get方法
* @param obj 类对象
* @param columnName 与类对象属性对应的数据库字段名
* @return 类对象的属性
*/
public static Object invokeGet(Object obj,String columnName){
Class clazz=obj.getClass();
Method method= null;
Object res=null;
try {
method = clazz.getDeclaredMethod("get"+ StringUtils.columnNameToMethodName(columnName), (Class<?>) null);
res=method.invoke(obj, (Object) null);
} catch (Exception e) {
e.printStackTrace();
}
return res;
}
/**
* 调用Object对应属性的get方法
* @param obj 类对象
* @param columnName 与类对象属性对应的数据库字段名
* @param value 属性值
*/
public static void invokeSet(Object obj,String columnName,Object value){
Class clazz=obj.getClass();
Method method= null;
try {
method = clazz.getDeclaredMethod("set"+ StringUtils.columnNameToMethodName(columnName),value.getClass());
method.invoke(obj,value);
} catch (Exception e) {
e.printStackTrace();
}
}
}
4.StringUtils
方法功能参考注释
/**
* 封装了字符串常用的操作
* @author xxbb
*/
public class StringUtils {
/**
* 正则表达式 用于匹配下划线
*/
private static Pattern linePattern = Pattern.compile("_(\\w)");
/**
* 正则表达式 用于匹配大写字母
*/
private static Pattern humpPattern = Pattern.compile("[A-Z]");
/**
* 将传入的表名去除t_字符,转化为类名
* @param str 传入字段
* @return 取出t_的下划线转驼峰+首字母大写字段
*/
public static String tableNameToClassName(String str){
return firstCharToUpperCase(lineToHump(str.substring(2)));
}
/**
* 将类名转化为数据库表名
* @param str 传入类名
* @return 数据库表名
*/
public static String classNameToTableName(String str){
return "t_"+humpToLine(str);
}
/**
* 将数据库字段名转化为类的命名规则,即下划线改驼峰+首字母大写,例如要获取if_freeze字段的方法,方法为getIfFreeze(),
* @param str 传入字段
* @return 下划线改驼峰+首字母大写
*/
public static String columnNameToMethodName(String str){
return firstCharToUpperCase(lineToHump(str));
}
/**
* 将传入字符串的首字母大写
* @param str 传入字符串
* @return 首字母大写的字符串
*/
public static String firstCharToUpperCase(String str){
return str.toUpperCase().substring(0,1)+str.substring(1);
}
/**
* 下划线转驼峰
* @param str 待转换字符串
* @return 驼峰风格字符串
*/
public static String lineToHump(String str) {
//将小写转换
String newStr=str = str.toLowerCase();
Matcher matcher = linePattern.matcher(newStr);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(sb, matcher.group(1).toUpperCase());
}
matcher.appendTail(sb);
return sb.toString();
}
/**
* 驼峰转下划线
* @param str 待转换字符串
* @return 下划线风格字符串
*/
public static String humpToLine(String str) {
//将首字母先进行小写转换
String newStr=str.substring(0,1).toLowerCase()+str.substring(1);
//比对字符串中的大写字符
Matcher matcher = humpPattern.matcher(newStr);
StringBuffer sb = new StringBuffer();
//匹配替换
while (matcher.find()) {
matcher.appendReplacement(sb, "_" + matcher.group(0).toLowerCase());
}
matcher.appendTail(sb);
return sb.toString();
}
public static void main(String[] args) {
String str=columnNameToMethodName(humpToLine("ifFreeze"));
System.out.println(str);
}
}
core核心业务分析
1.ConnectionPool
负责构建数据库连接池,同时根据配置信息加载configuration类。连接池采用双重检测锁的单例设计模式。读取配置信息的操作在静态代码块中进行。
在获取连接和归还连接的方法中设置了持有同一个monitor对象的同步锁,实现当连接数达到上限时,获取连接方法进行等待,只有当连接归还时才通过归还连接的方法唤醒(这里未考虑超时问题,故获取连接的方法可能会一直等待)
package com.xxbb.sorm.core;
import com.xxbb.sorm.bean.Configuration;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.LinkedList;
import java.util.Properties;
public class ConnectionPool {
/**
* 数据库连接属性
*/
private static String driverClass;
private static String url;
private static String username;
private static String password;
/**
* 初始连接数量
*/
private static int initCount = 5;
/**
* 最小连接数量
*/
private static int minCount = 5;
/**
* 最大连接数量
*/
private static int maxCount = 20;
/**
* 已创建的连接数量
*/
private static int createdCount;
/**
* 连接数增长步长
*/
private static int increasingCount = 2;
/**
* 存储配置文件信息
*/
private static Configuration configuration;
/**
* 存储连接的集合
*/
private LinkedList<Connection> conns = new LinkedList<>();
/**
* 用于获取连接和归还连接的同步锁对象
*/
private static final Object monitor = new Object();
/**
* 属性初始化
*/
static {
Properties properties = new Properties();
InputStream in = ConnectionPool.class.getClassLoader().getResourceAsStream("db.properties");
try {
properties.load(in);
//设置配置文件信息
configuration = new Configuration();
configuration.setUsingDB(properties.getProperty("usingDB"));
configuration.setDriverClass(properties.getProperty("jdbc.driverClass"));
configuration.setUrl(properties.getProperty("jdbc.url"));
configuration.setUsername(properties.getProperty("jdbc.username"));
configuration.setPassword(properties.getProperty("jdbc.password"));
configuration.setSrcPath(properties.getProperty("srcPath"));
configuration.setPoPackage(properties.getProperty("poPackage"));
configuration.setCatalog(properties.getProperty("catalog"));
configuration.setQueryClass(properties.getProperty("queryClass"));
//设置连接池信息
driverClass = configuration.getDriverClass();
url = configuration.getUrl();
username = configuration.getUsername();
password = configuration.getPassword();
//以下属性如果在properties文件中没有设置则使用默认值
try {
initCount = Integer.parseInt(properties.getProperty("jdbc.initCount"));
} catch (Exception e) {
System.out.println("initCount使用默认值:" + initCount);
}
try {
minCount = Integer.parseInt(properties.getProperty("jdbc.minCount"));
} catch (Exception e) {
System.out.println("minCount使用默认值:" + minCount);
}
try {
maxCount = Integer.parseInt(properties.getProperty("jdbc.maxCount"));
} catch (Exception e) {
System.out.println("maxCount使用默认值:" + maxCount);
}
try {
increasingCount = Integer.parseInt(properties.getProperty("jdbc.increasingCount"));
} catch (Exception e) {
System.out.println("increasingCount使用默认值:" + increasingCount);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 连接池对象
*/
private static volatile ConnectionPool instance;
private ConnectionPool() {
//防止反射破坏单例
if (instance != null) {
throw new RuntimeException("Object has been instanced!!!");
}
init();
}
public static ConnectionPool getInstance() {
//双重检测锁
if (null == instance) {
synchronized (ConnectionPool.class) {
if (null == instance) {
instance = new ConnectionPool();
}
}
}
return instance;
}
/**
* 初始化连接池
*/
private void init() {
//循环给集合中添加初始化连接
for (int i = 0; i < initCount; i++) {
boolean flag = conns.add(createConnection());
if (flag) {
createdCount++;
}
}
System.out.println("连接池连接初始化----->连接池对象:" + this);
System.out.println("连接池连接初始化----->连接池可用连接数量:" + createdCount);
}
/**
* 构建数据库连接对象
*
* @return
*/
private Connection createConnection() {
try {
Class.forName(driverClass);
return DriverManager.getConnection(url, username, password);
} catch (Exception e) {
throw new RuntimeException("数据库连接创建失败:" + e.getMessage());
}
}
/**
* 连接自动增长
*/
private synchronized void autoAdd() {
//增长步长默认为2
if (createdCount == maxCount) {
throw new RuntimeException("连接池中连接已达最大数量,无法再次创建连接");
}
//临界时判断增长个数
for (int i = 0; i < increasingCount; i++) {
if (createdCount == maxCount) {
break;
}
conns.add(createConnection());
createdCount++;
}
}
/**
* 自动减少连接
*/
private synchronized void autoReduce() {
if (createdCount > minCount && conns.size() > 0) {
//关闭池中空闲连接
try {
conns.removeFirst().close();
createdCount--;
System.out.print(Thread.currentThread().getName() + "--->已关闭多余空闲连接");
System.out.println(" 当前已创建连接数:" + createdCount+" 当前空闲连接数:" + conns.size() );
} catch (SQLException e) {
e.printStackTrace();
}
} else {
System.out.println(Thread.currentThread().getName() + "--->空闲连接保留在到连接池中或已被使用");
}
}
/**
* 获取池中连接
*
* @return 连接
*/
public Connection getConnection() {
//判断池中是否还有连接
synchronized (monitor){
if (conns.size() > 0) {
System.out.println(Thread.currentThread().getName()+"--->获取到连接:"+conns.getFirst()+" 已创建连接数量:"+createdCount+" 空闲连接数"+(conns.size()-1));
return conns.removeFirst();
}
//如果没有空连接,则调用自动增长方法
if (createdCount < maxCount) {
autoAdd();
return getConnection();
}
//如果连接池连接数量达到上限,则等待连接归还
System.out.println(Thread.currentThread().getName() + "--->连接池中连接已用尽,请等待连接归还");
try {
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
return getConnection();
}
}
/**
* 归还连接
*
* @param conn
*/
public void returnConnection(Connection conn) {
synchronized (monitor) {
System.out.println(Thread.currentThread().getName() + "--->准备归还数据库连接" + conn);
conns.add(conn);
monitor.notify();
autoReduce();
}
}
/**
* 获取当前空闲连接数
* @return 空闲连接数
*/
public int getIdleCount(){
return conns.size();
}
/**
* 返回已创建连接数量
*
* @return
*/
public int getCreatedCount() {
return createdCount;
}
/**
* 返回配置文件信息
*
* @return
*/
public Configuration getConfiguration() {
return configuration;
}
public static void main(String[] args) {
ConnectionPool.getInstance().init();
}
}
2.TableConvert
TableConvert提供了两个接口方法,负责java数据类型和数据库数据类型的相互转换。目前由MySqlTypeConvert类继承TableConvert接口实现了数据库数据类型到java数据类型的转换。
package com.xxbb.sorm.core;
/**
* 负责java数据类型和数据库数据类型的相互转换
* @author xxbb
*/
public interface TypeConvertor {
/**
* 将数据库类型转化为java的数据类型
* @param columnType 数据库字段的数据类型
* @return java属性的数据类型
*/
String databaseTypeToJavaType(String columnType);
/**
* 将java的数据类型转化为数据库的数据类型
* @param javaType java属性的数据类型
* @return 数据库字段的数据类型
*/
String javaTypeToDatabaseType(String javaType);
}
package com.xxbb.sorm.core;
/**
* mysql数据类型和java数据类型的转换
* @author xxbb
*/
public class MySqlTypeConvertor implements TypeConvertor {
/**
* 数据库中各个字段类型名称
*/
private static final String VARCHAR="varchar";
private static final String INT="int";
private static final String TINY_INT="tinyint";
private static final String SMALL_INT="smallint";
private static final String MEDIUM_INT="mediumint";
private static final String INTEGER="integer";
private static final String BIG_INT="bigint";
private static final String DOUBLE="double";
private static final String FLOAT="float";
private static final String BLOB="blob";
private static final String CLOB="clob";
private static final String DATE="date";
private static final String TIME="time";
private static final String TIME_STAMP="timestamp";
@Override
public String databaseTypeToJavaType(String columnType) {
//varchar-->String
if(VARCHAR.equals(columnType)){
return "String";
}
if(INT.equals(columnType)||
TINY_INT.equals(columnType)||
SMALL_INT.equals(columnType)||
MEDIUM_INT.equals(columnType)||
INTEGER.equals(columnType)){
return "Integer";
}
if(BIG_INT.equals(columnType)){
return "Long";
}
if(DOUBLE.equals(columnType)){
return "Double";
}
if(FLOAT.equals(columnType)){
return "Float";
}
if(BLOB.equals(columnType)){
return "java.sql.Blob";
}
if(CLOB.equals(columnType)){
return "java.sql.Clob";
}
if(DATE.equals(columnType)){
return "java.sql.Date";
}
if(TIME.equals(columnType)){
return "java.sql.Time";
}
if(TIME_STAMP.equals(columnType)){
return "java.sql.Timestamp";
}
return "";
}
@Override
public String javaTypeToDatabaseType(String javaType) {
return null;
}
}
3.TableContext
负责管理数据库的所有表结构和类结构的关系,并可以根据表结构生成类结构,这里采用了两个HashMap分别用来存储所有表的信息和实体类与表的映射关系。
在静态代码块中获取数据库元数据,从元数据中得到所有表数据,对表数据进行遍历,先创建TableInfo类,设置表名,将该类存放进map中,再创建map<String,ColumnInfo>类,存储表的字段信息,再创建ColumnInfo类存储主键信息。
即根据TableInfo的属性,将其所需要的信息都存放进去。这里我获取了主键信息的集合,并且将它放进了ColumnInfo类中,并且取主键信息集合的第一个值作为唯一主键。进行增删改操作时只会使用这个唯一主键,主键集合为扩展做准备。
设置了一个根据数据库的表结构生成对应的类结构的方法和设置po类与数据库表的对应关系的方法。在静态代码中,HashMap存储完所有的表信息后,调用前者放的生成对应的po类文件,再调用后者往开头提到的第二个HashMap中存储实体类与表的对应关系。
该类在DBManager的静态代码块中进行类加载。
package com.xxbb.sorm.core;
import com.xxbb.sorm.bean.ColumnInfo;
import com.xxbb.sorm.bean.Configuration;
import com.xxbb.sorm.bean.TableInfo;
import com.xxbb.sorm.util.JavaFileUtils;
import com.xxbb.sorm.util.StringUtils;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/**
* 负责管理数据库的所有表结构和类结构的关系,并可以根据表结构生成类结构
*
* @author xxbb
*/
public class TableContext {
/**
* 表名为key,表信息对象为value
*/
private static Map<String, TableInfo> databaseTableMap = new HashMap<>();
/**
* 将po的class对象和表信息对象关联起来,便于重用!
*/
private static Map<Class, TableInfo> poClassTableMap = new HashMap<>();
private TableContext() {
}
static {
Connection conn=null;
ResultSet tableResultSet=null;
ResultSet primaryKeyResultSet=null;
try {
//初始化获得表的信息
conn = DBManager.getConnection();
//获取数据库元数据
DatabaseMetaData databaseMetaData = conn.getMetaData();
//获取使用的数据库名称
String catalog = DBManager.getConfiguration().getCatalog();
/*
* catalog -目录名称即数据库名;必须匹配的目录名称,因为它是存储在数据库中;
* “检索那些没有目录; null意味着目录名称不应该被用来缩小搜索范围
*
* schemaPattern -模式名称模式;必须匹配的模式名称是存储在数据库中;
* “检索那些没有模式; null意味着架构名称不应该被用来缩小搜索范围
* tableNamePattern -表名模式;
* 必须匹配的表名是存储在数据库中
* columnNamePattern -列名称模式;
* 必须匹配的列名称,因为它是存储在数据库中
*/
tableResultSet = databaseMetaData.getTables(catalog, "%", "%", new String[]{"TABLE"});
while (tableResultSet.next()) {
String tableName = (String) tableResultSet.getObject("TABLE_NAME");
TableInfo tableInfo = new TableInfo(tableName, new HashMap<>(), new ArrayList<>());
databaseTableMap.put(tableName, tableInfo);
ResultSet columnResultSet = databaseMetaData.getColumns(catalog, "%", tableName, null);
while (columnResultSet.next()) {
ColumnInfo columnInfo = new ColumnInfo(columnResultSet.getString("COLUMN_NAME"),
columnResultSet.getString("TYPE_NAME"),
0);
tableInfo.getColums().put(columnResultSet.getString("COLUMN_NAME"), columnInfo);
}
//获取主键
primaryKeyResultSet = databaseMetaData.getPrimaryKeys(catalog, "%", tableName);
while (primaryKeyResultSet.next()) {
//设置主键
ColumnInfo columnInfo2 = (ColumnInfo) tableInfo.getColums().get(primaryKeyResultSet.getString("COLUMN_NAME"));
columnInfo2.setKeyType(1);
tableInfo.getUnionPrimaryKey().add(columnInfo2);
}
//如果主键里只有一个候选码,说明该表只有唯一主键
if (tableInfo.getUnionPrimaryKey().size() == 1) {
tableInfo.setUniquePrimaryKey(tableInfo.getUnionPrimaryKey().get(0));
}
}
} catch (SQLException e) {
e.printStackTrace();
}finally {
DBManager.closeResultSet(primaryKeyResultSet);
DBManager.closeResultSet(tableResultSet);
DBManager.closeConnection(conn);
}
//更新类结构
updateJavaPoFile();
//第一次启动程序,第一次生成java文件后,准备将po类与数据库表建立映射关系,提示类找不到
//经测试java文件已经生成了采用sleep方法无效
//TODO 尚未解决项目第一次加载时第一次执行updateJavaPoFile()生成类文件后,无法建立映射关系的问题=
setPoClassTableMap();
}
/**
* 根据数据库的表结构生成对应的类结构
*/
public synchronized static void updateJavaPoFile() {
for (TableInfo t : databaseTableMap.values()) {
JavaFileUtils.createJavaPoFile(t, new MySqlTypeConvertor());
}
}
/**
* 设置po类与数据库表的对应关系
*/
public static void setPoClassTableMap() {
try {
for (TableInfo t : databaseTableMap.values()) {
Class clazz = Class.forName("com.xxbb.po." + StringUtils.tableNameToClassName(t.getTableName()));
poClassTableMap.put(clazz,t);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* 获取设置po类与数据库表的对应关系的表
* @return 对应关系的哈希表
*/
public static Map<Class, TableInfo> getPoClassTableMap() {
return poClassTableMap;
}
/**
* 获取数据库表
* @return 数据库表的哈希表
*/
public static Map<String, TableInfo> getDatabaseTableMap() {
return databaseTableMap;
}
public static void main(String[] args) {
Map<String, TableInfo> tables = TableContext.databaseTableMap;
}
}
4.DBManager
负责维持从连接池获取的链接对象的管理,获取数据库连接池和配置文件类,调用连接池类中对应的获取连接和关闭连接方法。这里我对每一个关系方法都进行了封装,以适应各种关闭情况。
package com.xxbb.sorm.core;
import com.xxbb.sorm.bean.Configuration;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;
/**
* 根据配置信息,维持连接对象的管理
* @author xxbb
*/
public class DBManager {
private static ConnectionPool connectionPool=ConnectionPool.getInstance();
private static Configuration configuration=ConnectionPool.getInstance().getConfiguration();
static{
//初始化表类映射关系
try {
Class.forName("com.xxbb.sorm.core.TableContext");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static int getIdleCount(){
return connectionPool.getIdleCount();
}
/**
* 获取已创建连接数
* @return 已创建连接数
*/
public static int getCreatedCount(){
return connectionPool.getCreatedCount();
}
/**
* 获取配置信息
* @return 配置信息对象
*/
public static Configuration getConfiguration(){
return configuration;
}
//
/**
* 获取连接
* @return 数据库连接
*/
public static Connection getConnection(){
return connectionPool.getConnection();
}
/**
* 以下为关闭数据库相关连接
* @param conn 数据库连接对象
*/
public static void closeConnection(Connection conn){
if(conn!=null){
connectionPool.returnConnection(conn);
}
}
public static void closeAll(Connection conn, Statement stat, ResultSet rs){
closeResultSet(rs);
closeStatement(stat);
closeConnection(conn);
}
public static void closeAll(Connection conn, PreparedStatement ps, ResultSet rs){
closeResultSet(rs);
closePreparedStatement(ps);
closeConnection(conn);
}
public static void closeStatement(Statement stat){
if(stat!=null){
try {
stat.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
public static void closePreparedStatement(PreparedStatement ps){
if(ps!=null){
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
public static void closeResultSet(ResultSet rs){
if(rs!=null){
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
5.CallBack
用于BaseQuery类中模板方法的回调方法,目前只定义了一个doExecute()方法
package com.xxbb.sorm.core;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
/**
* 回调接口
* @author xxbb
*/
public interface CallBack {
/**
* 具体的数据库操作
* @param conn 连接对象
* @param ps 预处理对象
* @param rs 结果集对象
* @return Object对象,可以接收多种数据类型,如List,以适应不同的情况
*/
Object doExecute(Connection conn, PreparedStatement ps, ResultSet rs);
}
6.BaseQuery
负责查询,封装了所有对数据库的基本操作,即简单的增删改查。由于BaseQuery的方法能满足对mysql查询的需要,继承该抽象类的MySqlQuery并未添加新的功能,只是作为该抽象类的实现类使用。
查询有三个方法:
- queryRows——查询多条记录
- queryUniqueRows——查询一条记录
- queryValue——查询一行一列记录
其中queryRows和queryValue在查询过程中都是相同的,只是对结果的处理有所不同,故使用一个模板方法将从获取连接到查询出结果的过程封装起来。
在查询多行或一行数据时传入的参数为:
- 带占位符的sql语句(需要我们能在service层创建好,可以根据数据库表查询,也可以根据前端界面所需内容插查询,灵活性强)
- 查询的一张表或多张表对应的po或vo类
- 查询传入的参数,传入顺序和占位符顺序需对应
根据传入的po或vo类反射调用它们的set方法,将查询结果传入对象或集合中。
增删改方法则时通过传入需要执行操作的类或类对象和参数,通过TableContext中的类与数据库表的对应关系获取到数据库表对象TableInfo的信息,通过反射获取类的属性和方法,将他们拼装成需要的带占位符的语句,调用executeDML方法给语句进行处理和执行操作。
最后还实现了Cloneable接口,重写clone方法,提高在Query工厂中生成Query的对象的效率
package com.xxbb.sorm.core;
import com.xxbb.sorm.bean.ColumnInfo;
import com.xxbb.sorm.bean.TableInfo;
import com.xxbb.sorm.util.JDBCUtils;
import com.xxbb.sorm.util.ReflectUtils;
import com.xxbb.sorm.util.StringUtils;
import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
/**
* 负责查询,(对外提供服务的核心类)
*
* @author xxbb
*/
public abstract class BaseQuery implements Cloneable{
public Object executeQueryTemplate(String sql, Class clazz, Object[] params, CallBack back) {
Connection conn = DBManager.getConnection();
System.out.println("连接:"+conn);
PreparedStatement ps = null;
ResultSet rs = null;
try {
//执行查询操作
ps = conn.prepareStatement(sql);
JDBCUtils.handleParams(ps, params);
rs = ps.executeQuery();
//相当于一个占位符,说明在调用模板时这里会执行重写的doExecute方法
// 即具体内容到具体方法再去写
return back.doExecute(conn, ps, rs);
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
DBManager.closeAll(conn, ps, rs);
}
}
/**
* 直接执行一个DML语句 DML数据操纵语言
*
* @param sql sql语句
* @param params 参数
* @return 执行sql语句影响的行数
*/
public int executeDML(String sql, Object[] params) {
//记录受影响的行数
int count = 0;
//获取连接
Connection conn = DBManager.getConnection();
PreparedStatement ps = null;
try {
ps = conn.prepareStatement(sql);
//给sql传递参数
JDBCUtils.handleParams(ps, params);
count = ps.executeUpdate();
System.out.println("受影响的行数:" + count);
} catch (Exception e) {
e.printStackTrace();
} finally {
DBManager.closeAll(conn, ps, null);
}
return count;
}
/**
* 添加一个对象到数据库中
* 把对象中不为空的属性存储到数据库中
*
* @param obj 要存储的对象
*/
public void insert(Object obj) {
//获取信息
Class clazz = obj.getClass();
//存储object对象的非空属性
List<Object> params = new ArrayList<>();
//获取表信息
TableInfo tableInfo = TableContext.getPoClassTableMap().get(clazz);
//构建sql语句
StringBuilder sql = new StringBuilder();
sql.append("insert into ").append(tableInfo.getTableName()).append("(");
//获得属性
Field[] fields = clazz.getDeclaredFields();
//遍历属性
for (Field f : fields) {
String fieldName = f.getName();
Object fieldValue = ReflectUtils.invokeGet(obj, StringUtils.humpToLine(fieldName));
if (fieldValue != null) {
sql.append(StringUtils.humpToLine(fieldName)).append(",");
params.add(fieldValue);
}
}
//替换最后一个逗号
sql.setCharAt(sql.length() - 1, ')');
sql.append(" values(");
for (int i = 0; i < params.size(); i++) {
sql.append("?,");
}
//替换最后一个逗号
sql.setCharAt(sql.length() - 1, ')');
//执行sql
executeDML(sql.toString(), params.toArray());
}
/**
* 删除class表示类对应的数据库表中的记录(指定主键值id的记录)
*
* @param clazz 和数据库表对应的class类
* @param id 主键的值
*/
public void delete(Class clazz, Object id) {
if (id == null) {
throw new RuntimeException("传入的删除条件为空,执行删除功能失败!");
}
//获取表信息
TableInfo tableInfo = TableContext.getPoClassTableMap().get(clazz);
//获取主键
ColumnInfo uniquePrimaryKey = tableInfo.getUniquePrimaryKey();
//生成语句
StringBuilder sql = new StringBuilder();
sql.append("delete from ").append(tableInfo.getTableName()).append(" where ").append(uniquePrimaryKey.getName()).append("=? ");
//执行语句
int count = executeDML(sql.toString(), new Object[]{id});
}
/**
* 删除对象在数据库中对应的记录(对象所在的类对应数据库表,对象的主键的值对应到记录)
*
* @param obj
*/
public void delete(Object obj) {
//获取Class对象
Class clazz = obj.getClass();
//获取表信息
TableInfo tableInfo = TableContext.getPoClassTableMap().get(clazz);
//获取主键
ColumnInfo uniquePrimaryKey = tableInfo.getUniquePrimaryKey();
//通过反射获取传入对象的主键值
Object uniquePrimaryKeyValue = ReflectUtils.invokeGet(obj, uniquePrimaryKey.getName());
//执行语句
delete(clazz, uniquePrimaryKeyValue);
}
/**
* 更新对象对应的记录,并且只更新指定的字段的值
*
* @param obj 所需要更新的对象
* @param fieldName 需要更新的属性名,对应数据库字段名
* @return 受影响的行数
*/
public int update(Object obj, String[] fieldName) {
//获取信息
Class clazz = obj.getClass();
//存储object对象的非空属性
List<Object> params = new ArrayList<>();
//获取表信息
TableInfo tableInfo = TableContext.getPoClassTableMap().get(clazz);
//获取主键
ColumnInfo uniquePrimaryKey = tableInfo.getUniquePrimaryKey();
//构建sql语句
StringBuilder sql = new StringBuilder();
sql.append("update ").append(tableInfo.getTableName()).append(" set ");
for (String name : fieldName) {
Object value = ReflectUtils.invokeGet(obj, StringUtils.humpToLine(name));
sql.append(name).append("=?,");
params.add(value);
}
sql.setCharAt(sql.length() - 1, ' ');
sql.append("where ").append(uniquePrimaryKey.getName()).append("=?");
//获取修改的查询条件值
Object queryValue = ReflectUtils.invokeGet(obj, uniquePrimaryKey.getName());
params.add(queryValue);
return executeDML(sql.toString(), params.toArray());
}
/**
* 更新对象对应的记录,不指定更新字段,条件
*
* @param obj
* @return
*/
public int update(Object obj) {
//获取信息
Class clazz = obj.getClass();
//存储object对象的需要修改的属性
List<Object> params = new ArrayList<>();
//获取表信息
TableInfo tableInfo = TableContext.getPoClassTableMap().get(clazz);
//获取主键
ColumnInfo uniquePrimaryKey = tableInfo.getUniquePrimaryKey();
//获取查询条件名称,即主键的属性名
String key = StringUtils.lineToHump(uniquePrimaryKey.getName());
//构建sql语句
StringBuilder sql = new StringBuilder();
sql.append("update ").append(tableInfo.getTableName()).append(" set ");
//获取需要更新的属性
Field[] fields = clazz.getDeclaredFields();
for (Field f : fields) {
String fieldName = f.getName();
Object fieldValue = ReflectUtils.invokeGet(obj, StringUtils.humpToLine(fieldName));
//属性值非空且不是主键
if ((!key.equals(fieldName)) && fieldValue != null) {
sql.append(fieldName).append("=?,");
params.add(fieldValue);
}
}
sql.setCharAt(sql.length() - 1, ' ');
sql.append("where ").append(uniquePrimaryKey.getName()).append("=?");
params.add(ReflectUtils.invokeGet(obj, uniquePrimaryKey.getName()));
return executeDML(sql.toString(), params.toArray());
}
/**
* 查询返回多行记录,并且将每行记录封装到clazz指定类的对象中
*
* @param sql 查询语句
* @param clazz 封装数据的javabean类的clazz对象
* @param params sql参数
* @return 查询到的结果
*/
public List queryRows(String sql, Class clazz, Object[] params) {
return (List) executeQueryTemplate(sql, clazz, params, (connection, ps, rs) -> {
//存放查询结果
List<Object> res = new ArrayList<>();
//获取结果集的元数据
ResultSetMetaData resultSetMetaData = null;
try {
resultSetMetaData = rs.getMetaData();
//遍历查询结果
while (rs.next()) {
//实例化查询结果对应的类
Object rowObject = clazz.newInstance();
//获取查询结果的列数据
for (int i = 0; i < resultSetMetaData.getColumnCount(); i++) {
//获取列名,从1开始
String columnName = resultSetMetaData.getColumnLabel(i + 1);
Object columnValue = rs.getObject(i + 1);
//将数据赋值给类对象
ReflectUtils.invokeSet(rowObject, columnName, columnValue);
}
res.add(rowObject);
}
} catch (Exception e) {
e.printStackTrace();
}
return res;
});
}
/**
* 查询返回一行记录,并且将每行记录封装到clazz指定类的对象中
*
* @param sql 查询语句
* @param clazz 封装数据的javabean类的clazz对象
* @param params sql参数
* @return 查询到的结果
*/
public Object queryUniqueRows(String sql, Class clazz, Object[] params) {
List res = queryRows(sql, clazz, params);
return (res != null && res.size() > 0) ? res.get(0) : null;
}
/**
* 查询返回一行一列记录,并且将每行记录封装到clazz指定类的对象中
*
* @param sql 查询语句
* @param params sql参数
* @return 查询到的结果
*/
public Object queryValue(String sql, Object[] params) {
return executeQueryTemplate(sql, null, params, (conn, ps, rs) -> {
//获取查询结果,虽然这里用了while,但正确使用这个方法只会返回一行一列的值
Object res = new Object();
try {
while (rs.next()) {
res = rs.getObject(1);
}
} catch(SQLException ex){
ex.printStackTrace();
}
return res;
});
}
/**
* 查询返回一行一列的数字记录,并且将每行记录封装到clazz指定类的对象中
*
* @param sql 查询语句
* @param params sql参数
* @return 查询到的结果
*/
public Number queryNumber(String sql, Object[] params) {
return (Number) queryValue(sql, params);
}
/**
* 实现克隆方法
* @return 克隆的实例对象
* @throws CloneNotSupportedException
*/
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
6.QueryFactory
采用静态内部类的单例设计模式的工厂,读取配置文件中的查询类即BaseQuery的子类,通过反射将查询类实例化,在createQuery方法中调用clone方法获取查询类实例。
package com.xxbb.sorm.core;
/**
* 查询工厂,调用需要的查询类
* @author xxbb
*/
public class QueryFactory {
/**
* 查询类的原型对象
*/
private static BaseQuery prototypeObj;
/**
* 静态内部类实现单例,只有在使用静态内部类时才会加载
* 天然线程安全,延时加载
*/
private static class Instance{
private static QueryFactory instance=new QueryFactory();
}
//读取配置文件中的查询类
static {
try {
Class clazz = Class.forName(DBManager.getConfiguration().getQueryClass());
prototypeObj = (BaseQuery) clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
private QueryFactory(){
}
/**
* 获取单例工厂
* @return 工厂对象
*/
public static QueryFactory getInstance(){
return Instance.instance;
}
/**
* 获取查询类
* @return 查询类
*/
public BaseQuery createQuery(){
try {
return (BaseQuery) prototypeObj.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}
四、项目总结
这个项目的逻辑比较容易理解:
- 先构建好连接池
- 创建连接池管理类
- 获取数据库元数据来存储数据库信息和创建po类
- 创建Query类实现对数据库的操作
- 使用Query工厂获取Query类对象
其中代码看起来最复杂的就是TableContext中对于数据库元数据的操作,这是我首次接触这样的内容,对于获取元数据对象内数据的方法大的传入参数也是花了点时间取理解,特别是getTables的传入参数的catalog参数,在JDK的API文档中为说明为目录,实际上就是所使用数据库的名称。如果该项为空的话则会查询目前该连接下所有数据库的所有表。
在写项目的过程中也了解了一点模板方法设计模式和克隆设计模式的知识,加深了我对数据库连接池的理解,使用同步锁的方式让关闭连接方法取唤醒获取连接方法。
最后也留下了一个暂未完全解决的问题:
在TableContext的静态代码块最后我使用了updateJavaPoFile()用来创建po类,紧接着我调用 setPoClassTableMap()方法准备将po类与表信息对应起来,存储到HashMap中。可如果是我第一次创建po类时,该方法会报错提示po类未找到,可明明创建po类的方法已经执行完了,po包下已经有文件存在。目前如果需要解决该问题只能是在别处调用setPoClassTableMap()方法。该问题出现的原因还有待我的进一步学习。