一、jdbc访问数据库及其缺陷
1.1 jdbc访问数据库的代码
public static void main(String[] args) throws Exception {
Connection connection =null;
PreparedStatement preparedStatement =null;
ResultSet resultSet = null;
try{
// Q1:数据库链接信息直接硬编码在代码理
//Q2 :每次执行该方法都需要与数据库新建一个链接
Class.forName("com.mysql.jdbc.Driver");
connection= DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis? characterEncoding=utf-8"
, "root", "root");
//Q3 : 直接代码理写sql存在硬编码,这个经常会改动
String sql = "select * from user where id = ?";
preparedStatement = connection.prepareStatement(sql);
// Q4 :设置参数和封装返回值都很繁琐,且参数个数改动,代码都要跟着改,扩展性低
preparedStatement.setObject(1,2);
resultSet = preparedStatement.executeQuery();
User user = new User();
while (resultSet.next()){
int id = resultSet.getInt("id");
String username = resultSet.getString("username");
user.setId(id);
user.setUsername(username);
}
System.out.println(user);
}catch (Exception e ){
e.printStackTrace();
} finally {
//略
}
}
1.2 jdbc访问数据库存在得问题
- 数据库链接信息直接硬编码在代码里
- 每次执行该方法都需要与数据库新建一个连接,耗资源。
- 直接代码理写sql存在硬编码,这个经常会改动
- 设置参数和封装返回值都很繁琐,且参数个数改动,代码都要跟着改,扩展性低
二、自定义orm框架
2.1 针对jdbc问题的解决思路
针对jdbc存在的问题,可以做如下设计
- 用xml方式来管理数据库链接信息,sql文本、以及一些其他资源。
- 利用数据库连接池来避免每次获取新连接。
- 利用java反射来设置参数,和封装结果集
2.2 orm框架设计
2.2.1 使用端使用思路
- 调用orm jar包方法,加载
2.2.2 框架端设计思路
- 创建SqlMapConf.xml文件,该文件为全局文件,用来管理数据库连接等信息。
- 创建XxxMapper.xml文件,用来管理不同表得sql文本。
- 创建Resources类来加载xml文件到内存
- 利用dom4j来解析xml文件。
4.1 创建XmlConfBuilder类来解析SqlMapConf.xml
4.2 创建XmlMapperbuilder类来解析XxxMapper.xml - 封装xml的解析结果
5.1 用Configuration类来封装SqlMapConf.xml的解析结果。
5.2 用MapperStatement类来封装XxxMapper.xml文件中的每个crud标签内容。 - 用SqlSessionFactory创建SqlSession (数据库会话对象)来实现不同的curd方法
- 创建Executor 用于具体编写jdbc代码,封装sql,设置参数,封装结果集等。
2.3 orm 具体实现
2.3.1使用端
SqlMapCOnfig.xml
该xml配置文件主要存放一些数据库连接信息,以及xxxMapper.xml文件路径
<Configuration>
<datasource>
<property name="driver" value="com.mysql.jdbc.Driver"></property>
<property name="jdbc-url" value="jdbc:mysql:///hupeng"></property>
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
</datasource>
<mapper resource="UserMapper.xml">
</mapper>
</Configuration>
UserMapper.xml
该xml文件用于存放各种访问数据的sql。
- 其中namespace.id也就是statementid需要全局唯一用于定位需要执行的sql。
- paramterType用于封装参数
- resultType表示封装成什么类型的结果集
<Mapper namespace="user">
<!--user.findAll 表示statementId-->
<select id = "findAll" resultType = "com.hp.pojo.User">
select * from user
</select>
<select id = "findByCondition" resultType="com.hp.pojo.User" paramterType="com.hp.pojo.User">
select * from user where id = #{id} and username = #{username}
</select>
</Mapper>
Test
@Test
public void test1() throws Exception {
InputStream resources = Resources.getResources("SqlMapConf.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder(resources).build();
Sqlsession sqlsession = sqlSessionFactory.openSession();
List<User> users = sqlsession.selectList("com.hp.dao.UserDao.findAll");
for (User user : users) {
System.out.println(user);
}
}
2.3.2 自定义的orm端
pom.xml需要导入的依赖以及配置一些maven编译以及jdk版本信息
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.17</version>
</dependency>
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.6</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
</dependency>
</dependencies>
Configuration
- 这里用单例获取全局配置文件,全局只需要一份即可。
- Configuration里放了一个容器mapperstatementMap,用来装所有的sql。其中key值为全局唯一的statementId。有namespace.id组成。
@Data
public class Configuration {
private static volatile Configuration configuration;
private DataSource dataSource;
private Map<String,MapperStatement> mapperStatementMap = new HashMap<>();
private Configuration() {
}
public static Configuration getInstance(){
if(configuration == null){
synchronized (Configuration.class) {
if(configuration == null){
configuration = new Configuration();
}
}
}
return configuration;
}
}
MapperStatement
用于封装XxxMapper.xml文件里具体的执行sql。
@Data
public class MapperStatement {
private String id ;
private String sql;
private String parameterType;
private String resultType;
}
Resources
用于加载资源文件到内存
public class Resources {
public static InputStream getResources(String path){
InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path);
return resourceAsStream;
}
}
SqlSessionFactoryBuilder
public class SqlSessionFactoryBuilder {
private InputStream inputStream;
private Configuration configuration;
public SqlSessionFactoryBuilder(InputStream inputStream) {
this.inputStream = inputStream;
this.configuration = Configuration.getInstance();
}
/**
* 构建SqlSessionFactory工chang
*/
public SqlSessionFactory build() throws Exception{
//解析 SqlConfXml
XmlConfigerBuilder.parse(inputStream);
return new SqlSessionFactory(configuration);
}
}
XmlConfigerBuilder 解析SqlMapConf.xml
public class XmlConfigerBuilder {
private static Configuration configuration;
/**
* 具体解析SqlMapXml的类
* @param inputStream
* @return
*/
public static Configuration parse(InputStream inputStream) throws Exception {
configuration = Configuration.getInstance();
Document document =new SAXReader().read(inputStream);
Element rootElement = document.getRootElement();
List<Element> elements = rootElement.selectNodes("//property");
Properties properties =new Properties();
for (Element element : elements) {
String name = element.attributeValue("name");
String value = element.attributeValue("value");
properties.setProperty(name,value);
}
ComboPooledDataSource c3P0PooledDataSource = new ComboPooledDataSource();
c3P0PooledDataSource.setDriverClass(properties.getProperty("driver"));
c3P0PooledDataSource.setJdbcUrl(properties.getProperty("jdbc-url"));
c3P0PooledDataSource.setUser(properties.getProperty("username"));
c3P0PooledDataSource.setPassword(properties.getProperty("password"));
//解析UserMapper.xml
List<Element> mapperElements = rootElement.selectNodes("//mapper");
for (Element mapperElement : mapperElements) {
String resource = mapperElement.attributeValue("resource");
XmlMapperBuilder.parse(Resources.getResources(resource));
}
configuration.setDataSource(c3P0PooledDataSource);
return configuration;
}
}
XmlMapperBuilder 解析XxxMapper.xml
public class XmlMapperBuilder {
private static Configuration configuration;
public static void parse(InputStream inputStream) throws Exception{
configuration =Configuration.getInstance();
Document document = new SAXReader().read(inputStream);
Element rootElement = document.getRootElement();
String namespace = rootElement.attributeValue("namespace");
List<Element> selectElements = rootElement.selectNodes("//select");
for (Element selectElement : selectElements) {
String id = selectElement.attributeValue("id");
String resultType = selectElement.attributeValue("resultType");
String paramterType = selectElement.attributeValue("paramterType");
String sql = selectElement.getTextTrim();
MapperStatement mapperStatement = new MapperStatement();
mapperStatement.setId(id);
mapperStatement.setResultType(resultType);
mapperStatement.setParameterType(paramterType);
mapperStatement.setSql(sql);
String statementId = namespace+"."+id;
configuration.getMapperStatementMap().put(statementId,mapperStatement);
}
}
}
SqlSessionFactory
/**
* 工厂模式, 生产sqlSession会话对象
*/
public class SqlSessionFactory {
private static Configuration configuration;
public SqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
public Sqlsession openSession(){
return new DefultSqlSession(configuration);
}
}
SqlSession接口 以及默认实现类DefaultSqlSession
public interface Sqlsession {
<E> List<E> selectList(String statementId,Object... params) throws Exception;
<T> T selectOne(String statementId,Object... params) throws Exception;
<T> T getMapper(Class c);
}
public class DefultSqlSession implements Sqlsession {
private Configuration configurationn;
public DefultSqlSession(Configuration configuration) {
this.configurationn = configuration;
}
public <E> List<E> selectList(String statementId, Object... params) throws Exception {
//根据statementId来获取MapperStatement对象
MapperStatement mapperStatement = configurationn.getMapperStatementMap().get(statementId);
//调用SqlExecutor(与jdbc打交道的执行器)
SimpleExecutor simpleExecutor = new SimpleExecutor();
List<E> list = simpleExecutor.query(mapperStatement, configurationn, params);
return list;
}
public <T> T selectOne(String statementId, Object... params) throws Exception {
List<T> list = selectList(statementId, params);
if(list ==null || list.size()==0){
return null;
}
if(list.size()>1){
throw new RuntimeException("返回的不止一条数据");
}
return list.get(0);
}
}
Executor
public interface Executor {
<E> List<E> query(MapperStatement mapperStatement, Configuration configuration, Object... param) throws Exception;
}
Executor实现类 SimplerExecutor
/**
* 与jdbc交互
*/
public class SimpleExecutor implements Executor {
public <E> List<E> query(MapperStatement mapperStatement, Configuration configuration, Object... param) throws Exception {
Connection connection = configuration.getDataSource().getConnection();
//sql里面的参数用?代替
BoundSql boundSql = getBoundSql(mapperStatement.getSql());
PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());
// 反射设置参数
Class<?> aClass = getClassType(mapperStatement.getParameterType());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
for (int i =0 ;i< parameterMappings.size();i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
//参数
String content = parameterMapping.getContent();
Field field = aClass.getDeclaredField(content);
field.setAccessible(true);
//通过传递具体对象即可得到具体对象该field的值
Object value = field.get(param[0]);
//设置值
preparedStatement.setObject(i+1,value);
}
//封装结果集
Class<?> resultTypeClass = getClassType(mapperStatement.getResultType());
ResultSet resultSet = preparedStatement.executeQuery();
List<E> resultList =new ArrayList<>();
while (resultSet.next()){
ResultSetMetaData metaData = resultSet.getMetaData();
Object o = resultTypeClass.newInstance();
for (int i=1;i<=metaData.getColumnCount();i++){
//属性名和属性值
String columnName = metaData.getColumnName(i);
Object columnValue = resultSet.getObject(columnName);
//创建属性描述器,为属性生成读写方法
PropertyDescriptor propertyDescriptor =new PropertyDescriptor(columnName,resultTypeClass);
Method writeMethod = propertyDescriptor.getWriteMethod();
writeMethod.invoke(o,columnValue);
}
resultList.add((E)o);
}
return resultList;
}
private Class<?> getClassType(String paramterType) throws ClassNotFoundException {
if(paramterType!=null){
Class<?> aClass = Class.forName(paramterType);
return aClass;
}
return null;
}
private BoundSql getBoundSql(String sql) {
ParameterMappingTokenHandler tokenHandler = new ParameterMappingTokenHandler();
GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", tokenHandler);
String sqlText = genericTokenParser.parse(sql);
BoundSql boundSql =new BoundSql();
boundSql.setSqlText(sqlText);
boundSql.setParameterMappings(tokenHandler.getParameterMappings());
return boundSql;
}
}
BounderSql
- 用来封装sql以及参数集,方便上面的exector类处理。
- 其中用到了几个工具类,ParameterMappingTokenHandler、GenericTokenParser 主要用于替换sql文本中的#{}表达式
@Data
public class BoundSql {
//用?做占位符的sql文本
private String sqlText;
private List<ParameterMapping> parameterMappings = new ArrayList<>();
}
三、自定义orm框架优化
通过上述我们的自定义框架,我们解决了JDBC操作数据库带来的一些问题:例如频繁创建释放数据库连接,硬编码,手动封装返回结果集等问题,但是现在我们继续来分析刚刚完成的自定义框架代码,有没有什么问题?问题如下
- dao的实现类中存在重复的代码,整个操作的过程模板重复
- dao的实现类中存在硬编码,调用sqlsession的方法时,参数statement的id硬编码。
解决:用代理模式来创建接口的代理对象。
使用端测试类编写
@Test
public void test3() throws Exception{
InputStream resources = Resources.getResources("SqlMapConf.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder(resources).build();
Sqlsession sqlsession = sqlSessionFactory.openSession();
User user =new User();
user.setId(1);
user.setUsername("zhangsan");
UserDao userDao = sqlsession.getMapper(UserDao.class);
List<User> all = userDao.findAll();
for (User user1 : all) {
System.out.println(user1);
}
}
使用jdk动态代理解决硬编码
在sqlSession中添加方法
<T> T getMapper(Class c);
Default默认实现该方法
@Override
public <T> T getMapper(Class c) {
//动态代理生成Dao层的实现类、
Object proxy = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{c}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//通过接口和类,找到statementId
String methodName = method.getName();
Class<?> declaringClass = method.getDeclaringClass();
String name = declaringClass.getName();
String statementId = name+"."+methodName;
//如果方法的返回类型参数化了,说明是集合
if(method.getGenericReturnType() instanceof ParameterizedType){
return selectList(statementId, args);
}
return selectOne(statementId,args);
}
});
return (T) proxy;
}