- 反射依赖注入实践-实现ORM
到目前为止,我们应该都了解了JavaSE、JavaEE这两门技术,能够使用JavaEE进行一些企业级应用开发,这是作为Java开发人员的必备技能,在应用开发中JPA、Hibernate和MyBatis肯定都用过,估计很多人都会这些东西怎么实现的有很大的好奇心,本章就是带着大家用反射机制和JDBC来实现一个简化版的类似组件。在开发之前我们先了解一下与之相关的持久层的相关概念和术语,本章从持久化的概念入手,先引出持久化对象,接着详述了解软件分层思想的发展过程,并由此可知这类中间件在软件分层体系结构中所处的位置及它的作用。
3.1 持久层概述
分层结构是目前软件开发设计里的一种非常重要的思想。在开发中,把一个项目分成三层或者多层来实现,注意,这层的意思是把各个功能模块部分进行了归纳而形成的一个种概念。持久层就是在软件的多层体系结构基础上发展起来的,它以解决实体对象和对象关系这两大领域之间存在的不匹配问题为目标,为应用程序与对象-关系型数据库之间提供了一个成功的映射解决方案。本小节将围绕持久层来介绍一些相关概念,现在,我们先从持久化对象来开始进行讲解。
3.1.1 持久化对象
到目前为止,我们知道了程序运行时的数据都是保存在计算机的内存中,也就是RAM里面,但是内存的特点是:易失性、可读写,在计算机关机或断电后,内存中的数据也随之丢失,那么,问题来了,我们的一些重要数据是需要长期保存的,以供以后使用,那么如何解决?为解决这个问题,所以在计算机中引入了持久化的概念。
持久化(Persistent)指的是将内存中的数据进行永久性保存,基本上是保存到磁盘这一些存储设备中。目前,几乎所有的应用系统都需要进行持久化数据。我们可想而之,如果一个系统不需要或不能保存业务数据,那么这个系统基本上可以说没有什么实用价值。因此,如何对项目中的业务数据进行持久化就非常重要了。从目前情况来看,项目的数据持久化过程基本上是通过各种关系型数据库来完成的。大家都知道,目前比较流行的数据库有:Oracle、MySQL、SQL Server、DB2…. 。
持久化对象就是指已经存储到数据库或磁盘中的数据。为了保证一个对象持久存储,必须将其状态保存到非易失性的存储设备中,持久化对象可以在创建它的程序的作用域之外保持其自身的状态。不同的对象有不同的状态,状态数据都存放在对象的实例变量中。位于内存的堆栈空间中的对象在计算机关机或断电后会丢失,所以,我们为了永久的保存这些对象的状态,并需要的时候能够再获得这个对象数据,就需要对它们进行持久化操作。
在Java中,我们通常采用以下三种方式对对象进行持久化:
- 序列化对象,将对象进行序列化,以二进制格式存入文本文件。
- 将对象持久化到XML文件中。
- 将对象持久化到数据库中,目前,采用最多的就是关系型数据库。
大家都知道,关系型数据库中遵循了一条重要原则就是”数据独立性”,即数据可以独立于应用程序而存在。因此,数据可以比任何应用程序都存在得更久。同时,它的性能及安全性也有保障。并且,不同的应用程序之间还可以共享这些数据。
前面两种方式,我们在JavaSE基础课程上都已经进行了讨论,本章的感兴趣的第三种方式,把应用程序中的数据持久到到数据库中去。
3.1.2分层体系结构和持久层
随着应用软件的不断发展,应用程序从简单变得越来越庞大,功能越来越多,业务也更加的复杂。通过程序员的不停的总结,分层也成为了计算机软件设计中的一种重要思想。从单层结构发展到双层结构,双层结构分为应用层与数据库访问层,见图3.1。在双层结构中,用户界面和业务逻辑控制都由应用层负责实现,数据库访问层(持久层)负责与数据库进行交互。这样导致用户界面代码和业务逻辑代码混合在一起,产生了程序结构不清晰、维护困难等问题。同时,不懂编程的美工开发人员也无法参与到软件开发过程中。再后来,我们把应用层再次进行细分,将用户界面的设计从业务逻辑中进行分离,形成单独的一层——表示层,演变成三层结构。
经典的软件应用体系三层结构有三层:表示层、业务逻辑层、数据访问层(持久层),见图1.2所示。
图3.1 两层结构应用 图3.2 三层结构应用
各层主要功能如下:
- 表示层:提供了与用户进行交互的界面,作用是展示数据和收集数据。
- 业务逻辑层:完成业务逻辑处理,处理表示层提交的数据请求,并将要保存的数据提交给下层数据访问层。或根据表示的请求向底层数据访问层请求数据。
- 数据访问层(持久层):存储需要持久化的数据。数据库独立于应用程序,它只是提供了一种持久化的表现形式。
在上面的三层结构中,持久层对数据访问逻辑进行抽象,业务逻辑层通过持久层提供的数据访问接口来访问底层数据库中的数据。这不仅将应用开发人员从底层操作中解放出来,更多的去关注业务处理,同时,由于业务逻辑与数据访问分离开来,使得开发人员分工更加细化。某些数据库比较精通的开发人员可以专门负责持久层的数据库访问操作,而对业务流程比较熟悉的开发人员可以避开繁琐的数据库访问细节,只实现业务逻辑。这样,才更加有利于团队合作开发,有利于软件的健壮性,可维护性。
3.1.3持久层实现
持久层的实现是和数据库紧密相连的,在Java领域中,访问数据库的技术通常都采用JDBC,至于JDBC我们在前面的课程中已经进行了学习,这里不做详述。JDBC使用灵活而且访问速度快,性能好,但是JDBC不仅要操作对象,还需要操作关系,并不是完全面向对象编程,同时,开发人员还要编写大量的重复的数据库操作代码。
近年来涌现出许多新的持久层框架,这些框架为持久层的实现提供了更多的选择,同时,也简化了繁琐、重复的代码。目前,比较流行的持久层框架包括:Hibernate、iBatis、MyBites、JDO、Nhibernate、Linq To SQL …. 。
3.2 对象关系映射
面向对象的开发方式是现今企业级应用开发中的主流开发方法,关系型数据库是企业级应用环境中永久存放数据的主要方式。在软件开发过程中,对象 和关系数据是业务实体的两种不现的表现形式,业务实体在内存中的存在形式为对象,要想将业务实体永久存储则只能将其放入关系型数据库中,在数据库中它以关系型数据的形式存在。由面向对象基本理论知道,内存中的对象之间是存在着关联和继承关系的,而在关系型数据库中,数据之间无法直接表达多对多的关联和继承关系。此时,对象—关系映射组件就应运而生了。
对象关系映射(英语:Object Relation Mapping,简称ORM,或O/RM,或O/R mapping),是一种程序技术,用于实现面向对象编程语言里不同类型系统的数据之间的转换。从效果上说,它其实是创建了一个可在编程语言里使用的—“虚拟对象数据库”。面向对象是从软件工程基本原则(如耦合、聚合、封装)的基础上发展起来的,而关系数据库则是从数学理论发展而来的,两套理论存在显著的区别。为了解决这个不匹配的现象,对象关系映射技术应运而生。
对象关系映射提供了概念性的、易于理解的模型化数据的方法。ORM方法论基于三个核心原则:
- 简单:以最基本的形式建模数据。
- 传达性:数据库结构被任何人都能理解的语言文档化。
- 精确性:基于数据模型创建正确标准化的结构。
典型地建模者通过收集来自那些熟悉应用程序但不熟练的数据建模者的人的信息开发信息模型。建模者必须能够用非技术企业专家可以理解的术语在概念层次上与数据结构进行通讯。建模者也必须能以简单的单元分析信息,对样本数据进行处理。ORM专门被设计为改进这种联系。
回想以前开发一个应用程序的时候(不使用 ORM),我们写了不少数据访问层的代码,用来从数据库保存,删除,读取对象信息,等等。并且在DDL中也写了很多的方法来读取对象数据,改变状态对象等等任务。而这些代码写起来总是重复的。
如果打开你最近的程序,看看DAL代码,你肯定会看到很多近似的通用的模式。我们以保存对象的方法为例,你传入一个对象,为Statement对象添加Parameter,把所有属性和对象对应,获取 Conncation对象,然后Statement执行操作方法。对于每个对象都要重复的写这些代码,想想我上学期讲软件设计时候最后一个MVC的应用。
除此之外,还有更好的办法吗?有,引入一个ORM。实质上,一个 ORM 会为你生成DAL。与其自己写DAL代码,不如用ORM 。你用 ORM 实现数据的增、删、改、查,ORM负责生成SQL,你只需要关心对象就好。
简单的说:ORM相当于中继数据,具体到产品上,例如Hibernate、 Entity Framework、MyBites、Nhibernate,DLINQ中实体类的属性[Table]就算是一种中继数据。一般的ORM包括以下四部分:
- 一个对持久类对象进行CRUD操作的API;
- 一个语言或API用来规定与类和类属性相关的查询;
- 一个规定Mapping MetaData的工具;
- 一种技术可以让ORM的实现同事务对象一起进行DIRTYCHECKING, LAZY ASSOCIATION FETCHING以及其他的优化操作。
ORM把应用程序世界表示为具有角色(关系中的部分)的一组对象(实体或值)。ORM有时也称为基于事实的建模,因为它把相关数据描述为基本事实。这些事实如果分割为再小的事实就会丢失信息。例如:
人有电话
人住在某个地方
人生于某个日期
人在某个日期被雇佣
ORM提供的不只是描述不同对象间关系的一个简单而直接的方式。ORM还提供了灵活性。使用ORM创建的模型比使用其它方法创建的模型更有能力适应系统的变化。另外,ORM允许非技术企业专家按样本数据谈论模型,因此他们可以使用真实世界的数据验证模型。因为ORM允许重用对象,数据模型能自动映射到正确标准化的数据库结构。
ORM模型的简单性简化了数据库查询过程。使用ORM查询工具,用户可以访问期望数据,而不必理解数据库的底层结构。
本质上,ORM完成的是将数据从一种表现形式转换为另一种表现形式。因此,对象—关系映射系统一般以中间件的形式存在,主要实现程序对象到关系数据库数据的关系映射。Hibernate就是一种实现了ORM的框架。如下图1.3所示。
图3.3 ORM组件在应用中的位置
3.3 使用JDBC做数据持久的流程
写到这里我们回顾一下我们写数据库应用的时候使用JDBC的提交数据的过程:
- 我们前端应用要提交的数据首先以合理的形式保存在一个对象或或者一个对象数组中
- 最终提交的数据是是存储在数据库,以二维表的形式存储。
- 为了存储数据我么首先要使用JDBC Connections连接到对应的数据库。
- 我们应用数据形式是以对象形式出现的,数据库的数据以二维表的形式存在,针对数据库的数据的操纵使用的是SQL语言。
- 我们要通过一定的方法和套路,把对象中的信息转换成对应操作的SQL语言,在转换的过程中要注意对象的属性和数据库字段的对应。
- 对象数据转换成特定SQL语句后,我们根据操作类型是否需要动态参数通过数据库Connections对象选择创建PrepareStatment或Statment。
- 通过调用Statement.executeQuery(SqlStr)完成对数据库的操作。
- 如果有返回数据集,我们要解析Statement.executeQuery(SqlStr)返回的ResultSet,一条数据装配成一个对象,然后以对象数组列表的形式返回去。
这是软件设计讲过增加数据代码:
saveBtn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
int row = table.getSelectedRow();
int column = table.getSelectedColumn();
if (row == -1 || column == 0) return;
String val = dataModel.get(row).get(column);
String sid = dataModel.get(row).get(0); //需要保存的数据
String sql = "update student set " + TITLES.get(column) + " = ? where Sid = ?;";
//在文本框显示 SQL 命令
String cmd = "update student set " + TITLES.get(column) + " = ";
cmd += (TITLES.get(column) == "Sage") ? val : "'" + val + "'";
cmd += " where Sid = '" + sid + "';"; //拼装SQL
textarea.setText(cmd);
PreparedStatement ps; //创建执行SQL需要类型的Statement
try {
ps = conn.prepareStatement(sql); //做更新准备
if (TITLES.get(column) == "Sage")
ps.setInt(1, Integer.valueOf(val));
else
ps.setString(1, val);
ps.setString(2, sid); //传入修改数据
ps.executeUpdate(); //执行SQL
} catch (SQLException e1) {
e1.printStackTrace();
}
}
});
显然这个传统的操纵过程我们需要会使用SQL,会使用原生的JDBC API,还要在数据类型转换过程中要知道数据库中数据类型和Java数据类型之间的差异,这还仅仅是一张表的处理,而且我们实现的时候还没考虑多表关联,显然要是稍微大点系统,光这个转换过程就是巨大的工作量。
而我们现在开发,使用市面上成熟的ORM中间件的时候,我们只需填好数据库的对应基本配置,然后就是可以在应用中定义好实体类就可以自由的做数据的CRID了,显然使用的中间件帮我我们完成了Java 实体类和数据库直接的转换。写到这里估计很多人就好奇了ORM中间件好实现吗?我们自己使用原生的Java JDBC可以实现一套我们自己的ORM中间件吗?下面笔者就带着大家用原生的用刚学过的反射机制和原生JDBC来实现一个简易的ORM中间。
3.3.1 应用程序和数据库交互分类
上面讲过基本的数据库应用的更新流程,那么在我们实际开发过程中我们几种情形需要和数据库匹配呢?
- 从我们使用ORM中间件的流程看,我们写完实体类之后,配置好数据库相关信息,然后运行程序,用数据库管理软件打开数据库,你会惊奇地发现,我们需要的数据表自动建好了,显然从实体类建表的操作中间件帮我们完成了;
- 接着我们在应用中我们正常的操作对象,最后打开数据库,需要添加的信息添加了,需要修改的信息已经改了,需要删除的信息按照我们定义好的方式消失了,这个过程中,过去我们需要使用代码操纵SQL来完成,现在中间件也帮我们完成了;
- 另外还有一种情形,我们数据库结构定义好了,当我们运行程序点击某个按钮,发觉系统的实体类自动的出现在我们代码包中,现在这个代码也是ORM中间件根据数据库的信息帮我们自动生成的。
显然如果我们要自己实现中间件,这三个方面的工作都必须完成,下面我们就逐步分析这个过程,在代码实现的过程中,笔者仅仅讲原理,很多代码没有优化,而且很多机制没有实现完整,但原理会讲解,请同学们课下自己补充完整实现代码,先从基本的数据更新开始,接着讨论从数据库生成实体类,然后讲解从实体类生成数据库基本结构
3.3.1.1公共配置
完成数据库操作,必须在应用中定义如何连接数据库,所有的中间件都是通过外部一个文本文件来实现的,这里我们定义一个有关整个应用的数据库的配置文件,这里同时会演示一下缓冲池的实现方法,在工程的根目录建立一个application.properties,
1、application.properties内容如下:
driver = org.mariadb.jdbc.Driver 数据库驱动
username = oliver 登录数据库的用户名
password = 123456 登录数据库的密码
useDB = mysql 使用的数据库类型,演示写代码的时候为了方便
url = jdbc:mariadb://localhost:3306/test 数据库所在地址
sqlfile=pojo.sql 从实体类自动生成数据库的SQL文件
packageName = cn.edu.bbc.computer.pojo 实体类所在报名
poolMinSize = 10 数据库连接缓冲池个数最小值
poolMaxSize = 100 数据库连接缓冲池个数最大值
- Configuration属性内容
接下来就是定义一个类Configuration,可以在代码中保存这个配置文件,基本结构如下,对应于application.properties,详细结构参见代码:
/***
* @author oliver
** 核心配置类,用于存储配置文件的信息
*/
public class Configuration {
/**
* 声明驱动
*/
private String driver;// org.mariadb.jdbc.Driver
/**
* 声明数据库用户名
*/
private String username;// oliver
/**
* 密码
*/
private String password;// 123456
/**
* 使用的数据库
*/
private String useDB; // mysql
/**
* 访问数据库
*/
private String url;// = jdbc:mariadb://localhost:3306/test
/**
* 生成的pojo所在的包
*/
private String packageName; // = cn.edu.bbc.computer
/**
* 连接池最小链接数
*/
private int poolMinSize;
/**
* 连接池最大链接数
*/
private int poolMaxSize;
/**
* 保存实体类到数据库的SQL语句的文件
*/
private String sqlfile;
- 数据库连接管理类DBManager
显然应用程序中,我们都会实现一直接种管理应用连接的类,实现ORM中间件更必须实现,这个管理类负责根据配置文件中的信息,加载数据库驱动,建立连接缓冲池,创建数据库连接,关闭数据库连接等等,代码如下:
public class DBManager {
private static Configuration configuration;
private static DBPool pool;
/**
* 初始化configuration
*/
static {
Properties properties = new Properties();
try {
properties.load(
Thread.currentThread().getContextClassLoader().getResourceAsStream("applications.properties"));
configuration = new Configuration();
// 根据properties构建configuration
configuration.setDriver(properties.getProperty("driver"));
configuration.setPackageName(properties.getProperty("packageName"));
configuration.setPassword(properties.getProperty("password"));
configuration.setUrl(properties.getProperty("url"));
configuration.setUseDB(properties.getProperty("useDB"));
configuration.setUsername(properties.getProperty("username"));
configuration.setSqlfile(properties.getProperty("sqlfile"));
configuration.setPoolMinSize(Integer.parseInt(properties.getProperty("poolMinSize")));
configuration.setPoolMaxSize(Integer.parseInt(properties.getProperty("poolMaxSize")));
} catch (IOException e) {
e.printStackTrace();
} finally {
}
}
/**
* 得到数据库连接
*
* @param
* @return java.sql.Connection
*/
public static Connection getConnection() {
if (pool == null) {
pool = new DBPool();
}
return pool.getConnection();
}
/**
* 创建数据库连接
* @param
* @return java.sql.Connection
*/
public static Connection createConnection() {
try {
Class.forName(configuration.getDriver());
return DriverManager.getConnection(configuration.getUrl(), configuration.getUsername(),
configuration.getPassword());
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 关闭数据库连接
* @param connection
* @param preparedStatement
* @return void
*/
public static void close(Connection connection, PreparedStatement preparedStatement) {
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
public static Configuration getConfiguration() {
return configuration;
}
}
- 缓冲池的实现
实现原理其实很简单,当首次建立的时候,根据配置的最小的连接缓冲数,自动创建规定的数据库连接connection,以备访问中间件的应用调用,这里建立起来的连接放在idealpool中,每次有数据库连接请求查看一下idealpool有没有可用连接,如果有则将最后一个返回即可,如果没,看一下inServingPool超过缓冲池的最大值否,没有则创建一个con返回,并放入inServingPool,应用关闭连接的时候不是真正关闭,而是从inServingPool移动到idealpool,等待服务下一个连接请求。
public class DBPool {
/*** 连接池对象*/
private List<Connection> idealpool;//空闲缓冲池
private List<Connection> inServingPool;//在服务缓冲池
/**最大最小连接数*/
private static final int POOL_MAX_SIZE = DBManager.getConfiguration().getPoolMaxSize();
private static final int POOL_MIN_SIZE = DBManager.getConfiguration().getPoolMinSize();
/*** 初始化连接池,使池中的连接数达到最小值*/
public void initPool() {
if (idealpool == null) {
idealpool = new ArrayList<Connection>();
}
inServingPool = new ArrayList<Connection>();
while (idealpool.size() < DBPool.POOL_MIN_SIZE) {
idealpool.add(DBManager.createConnection());
System.out.println("初始化池,池中连接数:" + idealpool.size());
}
}
/**
* 从连接池中取出一个连接
* @return
*/
public synchronized Connection getConnection() {
int last_index = idealpool.size() - 1;
Connection conn = null;
if (last_index >= 0) {
conn = idealpool.get(last_index);
idealpool.remove(last_index);
inServingPool.add(conn);
}else if (inServingPool.size()<POOL_MAX_SIZE) {
conn=DBManager.createConnection();
inServingPool.add(conn);
}
return conn;
}
/**
* 将连接放回池中
* @param conn
*/
public synchronized void close(Connection conn) {
if (idealpool.size()+inServingPool.size() >= POOL_MAX_SIZE) {
try {
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
} else {
idealpool.add(conn);
inServingPool.remove(conn);
}
}
public DBPool() {
this.initPool();
}
}
3.3.1.2对象和数据库表的操作自动映射的实现
这一块的业务逻辑其实很简单,就是把我们过去自己写的从Java对象转SQL并操作数据库的过程给提炼出来的,形成一个通用过程,下面我们来看一原生操作过程的每个操作的步骤,数据库的操作是增删改查,我们这里逐一讲解这个转换过程,拿学生基本信息管理为例:
1、Insert动作的实现
向数据库添加一个学生,常规代码如下:
Connection conn = DBManager.getConnection(); //0、连接到数据库
Student user = new Student(); //1、建立对象
user.setSid("5170204100ww2"); //2、给对象赋值开始
user.setSname("测试");
user.setSaddr("安徽蚌埠");
user.setSage(23);
user.setSclass("计算机科学");
user.setSdept("计算机工程");
user.setSsex("男"); //3、给对象赋值结束
String sql = "insert into student values (?,?,?,?,?,?,?);"; //4、根据对象生成数据库insert语句
String sid = user.getSid();
String sname = user.getSname();
String ssex = user.getSsex();
String sage = String.valueOf(user.getSage());
String sclass = user.getSclass();
String sdept = user.getSdept();
String saddr = user.getSaddr();
//在文本框显示 SQL 命令
String cmd = "insert into student values ('" + sid + "', '" + sname + "', '" +
ssex + "', " + sage + ", '" + sclass + "', '" + sdept + "', '" + saddr + "');";
//5、生成语句结束
PreparedStatement ps; //6、准备执行语句
try {
ps = conn.prepareStatement(sql);
ps.setString(1, sid);
ps.setString(2, sname);
ps.setString(3, ssex);
ps.setInt(4, Integer.valueOf(sage));
ps.setString(5, sclass);
ps.setString(6, sdept);
ps.setString(7, saddr); //7、参数传递
ps.executeUpdate(); //8、执行语句,数据提交
} catch (SQLException e1) {
e1.printStackTrace();
}
如果你们过去写原生数据库操纵代码足够得多的,这个标准的过程好像有一定的共性,可以抽象成一个标准的适应于所有对象的Insert动作,转换思路整理如下:
第一步 连接数据库
第二步 把含有我们要输入数据库值的对象从前端接收过来,可能是单个,也可能是对象数组,这些传过来的数据对象基本稳定。
第三步 生成标准的insert语句的标准部分 insert into,然后加表名,这个通常我们会根据实体类名称写明确的表,但是想一下我们如果想写个通用的呢?这个表名可以怎么取?这个时候你会突然想到反射,任何对象都可以调用getClass方法,获取其类名,如果我们前面已经建立好数据库表和实体类的名称对应关系的话,这个时候通过查表就可以动态的根据类名填写这个表名,这个时候思路亦应该一下子就出来了,如果动态构造 应该这么写:
insert into [object.getClass]
第四步:构造所需字段名称,想想我们实体类的定义,所有字段都和数据表的列对应,学习反射机制的时候,我们知道可以通过Class.getDeclaredField获取一个对象的所有定义字段信息,因为我们定义实体类的要求字段和表的列一一对应,所以现在我们可以通过反射机制,查询出对象中的所有field名称,然后通过一个合理的约定或者查表,获得数据表的字段,因为有多少字段就需要多少具体的值进行填充,所以同时构造出values(?)语句,至于需要多少个(?)显然getDeclaredField时就能知道,因此最后的范式是这样的:
insert into [object.getClass] ([object.getClass().getgetDeclaredField()]) values (?)
第五步 如何完成对象的字段和字段值的精确对应,这个很简单啊,在object.getClass().getgetDeclaredField()肯定会返回一个字段列表,在生成object.getClass().getgetDeclaredField()这段SQL的时候同时利用反射机制调用对应对象上对应字段的get方法获取值,生成一个和字段对应的值的ArrayList,这样最终就可以形成一个字段完全一致的值的列表。
第六步 使用连接的PrepareStatement,完成拼装后的SQL的值的动态传入和的执行
我们按照这个思路写代码如下:
public void insert(Object object){
// 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); //调用对象上字段的get方法获取传进来的值
if (value!=null){
sb.append(name+","); //构建(字段1,字段2,......)
fieldValueList.add(value); //构建字段对应的值的数组(字段1值,字段2值, .......)
}
}
//将最后一个,换成)
sb.setCharAt(sb.length()-1,')'); //加封闭括号
sb.append(" values("); //添加valuses部分
for (int i=0;i<fieldValueList.size();i++){
sb.append("?,"); //增加 值的占位符
}
sb.setCharAt(sb.length()-1,')'); //加封闭括号
executeDML(sb.toString(),fieldValueList.toArray()); //执行SQL 并把转换后的值传递进去
}
执行SQL的函数
/**
* 执行sql语句
* @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;
}
动态装配参数的类和函数:
public class JdbcUtils {
/**
* 给sql语句设值值
* @param ps
* @param params
* @return void
*/
public static void handlerParams(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();
}
}
}
}
}
显然这么定义好之后就可以传入任意的实体类,做对应实体类的Insert操作
- 通用Delete的实现
原理是样的,通过反射机制动态获取表名,构建适用于该对象的对应表的delete语句,我这里仅仅给出核心代码:
/**
* 删除一个对象
* @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的对象
* @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});
}
- 通用Update的实现
/**
* 更新对象字段的信息
* @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());
}
- 通用Select的实现
/**
* 根据参数,查询指定的数据,多行记录,单行记录可直接get(0)
* @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.getDeclaredConstructor().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;
}
/**
* 查询某个字段的数据
* @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;
}
3.3.1.3从数据库自动生成实体类
我们大多数的时候经常碰到别人已经把数据库建好了,然后让我们在这个数据库结构的基础上写业务逻辑,然后给你了一堆文档,最不爽的就是要自己根据定义文档写实体类,然后还没人给你说,这个版本的文档和数据库可能有点出入,然后你写完代码,生成的SQL各种语法错误,显然好的中间件应该可以根据数据库的库表结构自动生成对应的实体类,这样就可以帮助开发人员省去大量的麻烦,这里简单讲解一下如何从数据库的结构中自动生成实体类的代码。
想完成这个转换首先要有方法获取数据库的结构信息,所幸JDBC已经给我封装好了获取数据库基本结构信息的API:
DatabaseMetaData Connection.getMetaData()
想自动生成POJO文件现在要获取的就是数据表的结构信息,这里定义一个
public static Map<String, TableInfo> tables = new HashMap<String, TableInfo>();
用于保存书库中所有表的结构信息,获取数据库中表结构信息的代码如下:
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); // 获取表的主键
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();
}
其中TableInfo的定义如下,不解释各位也应该可以轻松看懂,就是每张表的结构信息,tables 变量就是数据库表的集合,每张表的具体信息,存储在对应的TableInfo中:
/**
* 封装表信息
* 表名
* 字段信息
* 主键
**/
public class TableInfo {
/**
* 表名
*/
private String name;
/**
* 存放字段信息, 字段名和字段信息
*/
private Map<String,ColumnInfo> columns;
/**
* 主键信息
*/
private ColumnInfo onlyPriKey;
/**
* 联合主键
*/
private List<ColumnInfo> priKeys;
public TableInfo(String name, Map<String, ColumnInfo> columns, List<ColumnInfo> priKeys) {
this.name = name;
this.columns = columns;
this.priKeys = priKeys;
}
public List<ColumnInfo> getPriKeys() {
return priKeys;
}
public void setPriKeys(List<ColumnInfo> priKeys) {
this.priKeys = priKeys;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Map<String, ColumnInfo> getColumns() {
return columns;
}
public void setColumns(Map<String, ColumnInfo> columns) {
this.columns = columns;
}
public ColumnInfo getOnlyPriKey() {
return onlyPriKey;
}
public void setOnlyPriKey(ColumnInfo onlyPriKey) {
this.onlyPriKey = onlyPriKey;
}
public TableInfo() {
}
public TableInfo(String name, Map<String, ColumnInfo> columns, ColumnInfo onlyPriKey) {
this.name = name;
this.columns = columns;
this.onlyPriKey = onlyPriKey;
}
}
有了这个结构显然后面生成对应表的POJO文件就不是很难的事了,就是解析tables 这个数组根据POJO文件格式拼装成实体类即可,为了能够自动根据关键字,我这里定义了一个@ID注解用于实体类标注关键字属性,这里演示原理,仅仅单关键字的情形,多关键字以及字段改名之类的注解请读者自己完善,实现代码如下:
package cn.edu.bbc.computer.anotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD })
public @interface ID {
String value() default "主键";
}
根据表结构自动生成Java实体类的方法:
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方法
StringBuilder toString = new StringBuilder();
StringBuilder constructFunName = new StringBuilder();
StringBuilder constructFunBody = new StringBuilder();
toString.append("\t@Override\n");
toString.append("\tpublic String toString() {\n");
toString.append("\t\treturn " + "\"" + StringUtils.UpFirstString(tableInfo.getName()) + " [");
constructFunName.append("\tpublic " + StringUtils.UpFirstString(tableInfo.getName()) + "(");
constructFunBody.append("\t\tsuper();\n");
Iterator valuesIterator = values.iterator();
ColumnInfo c;
while (valuesIterator.hasNext()) {
c = (ColumnInfo) valuesIterator.next();
String javaType = typeConvertorHandler.JdbcType2JavaType(c.getDataType());
if (valuesIterator.hasNext()) {
toString.append(c.getName().toLowerCase() + "=\" + " + c.getName().toLowerCase() + " + \", ");
constructFunName.append(javaType + " " + c.getName().toLowerCase() + ", ");
constructFunBody
.append("\t\tthis." + c.getName().toLowerCase() + " = " + c.getName().toLowerCase() + ";\n");
} else {
toString.append(
c.getName().toLowerCase() + "=\" + " + c.getName().toLowerCase() + " + \"]\";\n\t}\n\n");
constructFunName.append(javaType + " " + c.getName().toLowerCase() + "){\n");
constructFunBody.append(
"\t\tthis." + c.getName().toLowerCase() + " = " + c.getName().toLowerCase() + ";\n\t}\n");
}
JavaFeildInfo javaFeild = createJavaFeild(c, 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("import java.io.Serializable;\n");
sb.append("public class " + StringUtils.UpFirstString(tableInfo.getName()) + " implements Serializable{\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("\tpublic "+StringUtils.UpFirstString(tableInfo.getName())+"(){\n");
sb.append("\t\tsuper();\n\t}\n\n");
sb.append(constructFunName);
sb.append(constructFunBody);
sb.append(toString);
sb.append("}\n");
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()) + "已自动生成..");
}
TypeConvertorHandler的作用就是数据库中数据类型和对应数据库的转换,接口如下:
public interface TypeConvertorHandler {
/**
* java数据类型转换成数据库数据类型
* @param javaTypeData java数据类型
* @return java.lang.String 数据库数据类型
*/
public String JavaType2JdbcType(String javaTypeData);
/**
* 数据库数据类型转换成java数据类型
* @param jdbcTypeData 数据库类型
* @return java.lang.String java类型
*/
public String JdbcType2JavaType(String jdbcTypeData);
}
针对MariaDB和MySQL是实现如下:
public class MysqlConvertorHandler implements TypeConvertorHandler {
private static Map<String, String> map = new HashMap<>();
@Override
public String JavaType2JdbcType(String javaTypeData) {
return map.get(javaTypeData);
}
public MysqlConvertorHandler() {
super();
map.put("class java.lang.String", "varchar(255)");
map.put("class java.lang.Integer", "int");
map.put("class java.lang.Long", "integer unsigned");
map.put("class java.lang.byte[]", "blob");
map.put("class java.lang.Boolean", "bit");
map.put("class java.math.BigInteger", "bigint unsigned");
map.put("class java.lang.Float", "float");
map.put("class java.lang.Double", "float");
map.put("class java.sql.Date", "datetime");
map.put("class java.sql.Time", "time");
map.put("class java.sql.Timestamp", "datetime");
map.put("class java.util.Date", "datetime");
map.put("class java.lang.Byte", "tinyint");
}
@Override
public String JdbcType2JavaType(String jdbcTypeData) {
//varchar-->String
if("varchar".equalsIgnoreCase(jdbcTypeData)||"char".equalsIgnoreCase(jdbcTypeData)){
return "String";
}else if("int".equalsIgnoreCase(jdbcTypeData)
||"tinyint".equalsIgnoreCase(jdbcTypeData)
||"smallint".equalsIgnoreCase(jdbcTypeData)
||"integer".equalsIgnoreCase(jdbcTypeData)
){
return "Integer";
}else if("bigint".equalsIgnoreCase(jdbcTypeData)){
return "Long";
}else if("double".equalsIgnoreCase(jdbcTypeData)||"float".equalsIgnoreCase(jdbcTypeData)){
return "Double";
}else if("clob".equalsIgnoreCase(jdbcTypeData)){
return "CLob";
}else if("blob".equalsIgnoreCase(jdbcTypeData)){
return "BLob";
}else if("date".equalsIgnoreCase(jdbcTypeData)){
return "Date";
}else if("time".equalsIgnoreCase(jdbcTypeData)){
return "Time";
}else if("timestamp".equalsIgnoreCase(jdbcTypeData)){
return "Timestamp";
}
return null;
}
}
字段转换函数createJavaFeild如下,作用是生成实体类的属性,对应属性的getter和Setter:
public static JavaFeildInfo createJavaFeild(ColumnInfo columnInfo, ColumnInfo sKey,
TypeConvertorHandler convertorHandler) {
// 将字段数据类型转换成java数据类型
String javaType = convertorHandler.JdbcType2JavaType(columnInfo.getDataType());
String columnName = columnInfo.getName().toLowerCase();
String sKeyname = sKey.getName().toLowerCase();//关键字
JavaFeildInfo feildInfo = new JavaFeildInfo();
// 生成属性语句
if (sKeyname.equals(columnName)) {
feildInfo.setFeildInfo("\t@ID\n\tprivate " + javaType + " " + StringUtils.trimUnderLine(columnName) + ";\n");
} else
feildInfo.setFeildInfo("\tprivate " + javaType + " " + StringUtils.trimUnderLine(columnName) + ";\n");
StringBuilder sbSet = new StringBuilder();
sbSet.append("\tpublic " + javaType + " " + "get" + StringUtils.UpFirstString(columnName) + "() {\n");
sbSet.append("\t\treturn " + columnName + ";\n");
sbSet.append("\t}\n");
feildInfo.setGetFeildInfo(sbSet.toString());
StringBuilder sbGet = new StringBuilder();
sbGet.append("\tpublic void " + "set" + StringUtils.UpFirstString(columnName) + "(" + javaType + " " + columnName
+ ") {\n");
sbGet.append("\t\t this." + columnName + " = " + columnName + ";\n");
sbGet.append("\t}\n");
feildInfo.setSetFeildInfo(sbGet.toString());
return feildInfo;
}
3.3.1.4从实体类生成数据库表
同样我们在成熟的ORM中间件也看到只有定义是实体类,然后数据库自动生成的,这个原理也很简单,和我们过去构造数据操作语句的套路一样,只不过转为构成数据库的定义语句create和alter,下面在实现这个过程:
public static String generateSql(String className, TypeConvertorHandler convertorHandler) {
try {
Class<?> clz = Class.forName(className);
className = clz.getSimpleName();
// 类名就是表名,这里不做什么转换,仅仅是演示原理
className = getStandardFields(className);
Field[] fields = clz.getDeclaredFields();
StringBuffer column = new StringBuffer();
String varchar = " CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,";
String pkey = "";
for (Field f : fields) {
if ("class java.lang.String".equals(f.getType().toString())) {
column.append(" \n `" + getStandardFields(f.getName()) + "`" + " "
+ convertorHandler.JavaType2JdbcType(f.getType().toString())).append(varchar);
} else {
column.append(" \n `" + getStandardFields(f.getName()) + "`" + " "
+ convertorHandler.JavaType2JdbcType(f.getType().toString())).append(",");
}
if (f.isAnnotationPresent(ID.class)) {
pkey = f.getName();// 这里仅演示单关键字的
}
}
StringBuffer sql = new StringBuffer();
sql.append("\n DROP TABLE IF EXISTS `" + className + "`; ")
.append(" \n CREATE TABLE `" + className + "` (").append(column)
.append(" \n PRIMARY KEY (`" + pkey + "`) USING BTREE,")
.append("\n INDEX `" + pkey + "`(`" + pkey + "`) USING BTREE")
.append(" \n ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci; \n");
return sql.toString();
} catch (ClassNotFoundException e) {
System.out.println("该类未找到!");
return null;
}
}
然后的套路就是执行SQL
注意:默认连接mysql的时候一次只能执行一条sql。要批量执行sql需要在jdbcUrl中增加“allowMultiQueries=true”参数,完整jdbcUrl如下:
jdbc:mysql://localhost/database1?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
使用此连接串后,才能一次批量执行上面的多条sql。此方法简单,对程序改动小。
另外还有一种方法,就是在程序中对SQL语句以分号拆分成多条SQL语句,然后使用Statement的addBatch方法,最后executeBatch就行。此方法复杂,需要自行解析sql脚本串,然后分步添加。对程序改动比较大。
0 条 查看最新 评论
没有评论