一、JDBC操纵数据库_问题分析
JDBC使用流程
- 加载数据库驱动
- 创建数据库毗连
- 创建编译对象
- 设置入参实行SQL
- 返回效果集
代码示例
- public class JDBCTest {
- public static void main(String[] args) throws Exception {
- Connection connection = null;
- PreparedStatement preparedStatement = null;
- ResultSet resultSet = null;
- try {
- // 加载数据库驱动
- Class.forName("com.mysql.jdbc.Driver");
- // 通过驱动管理类获取数据库链接
- connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis",
- "root","123456789");
- // 定义sql语句?表示占位符
- String sql = "select * from user where name = ?";
- // 获取预处理statement
- preparedStatement = connection.prepareStatement(sql);
- // 设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数值
- preparedStatement.setString(1, "zhangsan");
- // 向数据库发出sql执行查询,查询出结果集
- resultSet = preparedStatement.executeQuery();
- // 遍历查询结果集
- while (resultSet.next()) {
- int id = resultSet.getInt("id");
- String username = resultSet.getString("name");
- // 封装User
- User user = new User();
- user.setId(id);
- user.setName(username);
- System.out.println(user);
- }
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- // 释放资源
- if (resultSet != null) {
- try {
- resultSet.close();
- } catch (SQLException e) {
- e.printStackTrace();
- }
- }
- if (preparedStatement != null) {
- try {
- preparedStatement.close();
- } catch (SQLException e) {
- e.printStackTrace();
- }
- }
- if (connection != null) {
- try {
- connection.close();
- } catch (SQLException e) {
- e.printStackTrace();
- }
- }
- }
- }
- }
复制代码 问题分析
存在问题解决思绪数据库配置信息存在硬编码问题使用配置文件频仍创建、开释数据库毗连问题使用数据库毗连池SQL语句使用配置文件数据库配置信息存在硬编码问题使用配置文件 二、自界说恒久层框架_思绪分析
主要分两部分,项目使用端:平常写代码所说的背景服务;恒久层框架:即项目使用端引入的jar包
核心接口/类重点说明:
类名界说角色定位分工协作Resources资源辅助类负责读取配置文件转化为输入流Configuration数据库资源类负责存储数据库毗连信息MappedStatementSQL与效果集资源类负责存储SQL映射界说、存储效果集映射界说SqlSessionFactoryBuilder会话工厂构建者负责解析配置文件,创建会话工厂SqlSessionFactorySqlSessionFactory会话工厂负责创建会话SqlSessionSqlSession会话指派实行器ExecutorExecutor实行器负责实行SQL (共同指定资源Mapped Statement) 项目使用端:
- 引入自界说恒久层框架的jar包
- sqlMapConfig.xml:数据库配置信息,以及mapper.xml的全路径
- mapper.xml:SQL配置信息,存放SQL语句、参数范例、返回值范例相关信息
留意: sqlMapConfig.xml中引入mapper.xml是为了只读取一次配置文件,否则每个实体类会有一个mapper.xml,则需要读取很多次
自界说框架本身:
- 加载配置文件:根据配置文件的路径,加载配置文件成字节输入流,存储在内存中
- 创建两个javaBean(容器对象):存放配置文件解析出来的内容
- 解析配置文件(使用dom4j),并创建SqlSession会话对象
- 创建SqlSessionFactory接口以及实现类DefaultSqlSessionFactory
- 创建SqlSession接口以及实现类DefaultSqlSession
SqlSession接口界说以上方法,DefaultSqlSession来决定什么操纵调用对应的sql实行器
- 创建Executor实行器接口以及实现类SimpleExecutor简朴实行器
三、自界说框架_编码
项目使用端
创建sqlMapConfig核心配置文件:
- <configuration>
- <!--配置数据库信息-->
- <dataSource>
- <property name="driverClassname" value="com.mysql.jdbc.Driver" />
- <property name="url" value="jdbc:mysql://localhost:3306/mybatis" />
- <property name="username" value="root" />
- <property name="password" value="123456789" />
- </dataSource>
- <!--引入映射配置文件,为了只加载一次xml-->
- <mappers>
- <mapper resource="mapper/UserMapper.xml"/>
- </mappers>
- </configuration>
复制代码 创建映射配置文件:
获取某个sql语句的唯一标示statementId:namespace.id 如:user.selectList
- <mapper namespace="user">
- <!--查询所有-->
- <select id="selectList" resultType="com.xc.pojo.User" >
- select * from user
- </select>
- <!--按条件查询-->
- <select id="selectOne" resultType="com.xc.pojo.User" parameterType="com.xc.pojo.User" >
- select * from user where id = #{id} and name = #{name}
- </select>
- </mapper>
复制代码 User实体:
- @Data
- public class User {
- private Integer id;
- private String name;
- }
复制代码 pom.xml引入自界说框架
- <dependency>
- <groupId>com.xc</groupId>
- <artifactId>own-mybatis</artifactId>
- <version>1.0-SNAPSHOT</version>
- </dependency>
复制代码 自界说框架本身
1、加载配置文件
- 根据配置文件的路径,加载配置文件成字节输入流,存到内存中
- public class Resources {
- public static InputStream getResourceAsStream(String path){
- return Resources.class.getClassLoader().getResourceAsStream(path);
- }
- }
复制代码 2、创建两个配置类对象
- 映射配置类:mapper.xml解析出来内容
- 每个pojo实体都会对应一个mapper.xml文件即一个MapperStatement对象
- sqlCommandType:第四节 自界说框架_优化才会用到
- @Data
- public class MapperStatement {
- //唯一标识 statementId:namespace.id
- private String statementId;
- //返回值类型
- private String resultType;
- //参数类型
- private String parameterType;
- //sql语句
- private String sql;
-
- // 判断当前是什么操作的一个属性-增删改查
- private String sqlCommandType;
- }
复制代码
- 核心配置类:数据库配置信息以及映射配置类的map集合
- 将多个MapperStatement对象存入Map集合,statementId(namespace.id)作为key
- 将所有的配置文件都聚合到Configuration中,方便一次读取以及统一管理
- @Data
- public class Configuration {
- //数据源对象
- private DataSource dataSource;
- //map.xml对象集合 key:statementId
- private Map<String,MapperStatement> mapperStatementMap = new HashMap<>();
- }
复制代码 3、解析配置文件,填充配置类对象
- XMLConfigBuilder类的parse方法:解析核心配置类,返回Configuration对象
- 创建SqlSession工厂对象,以便之后创建SqlSession会话
- public class SqlSessionFactoryBuilder {
- public SqlSessionFactory build(InputStream inputStream) throws Exception {
- //1.解析配置文件,封装容器对象:专门解析核心配置文件的解析类
- XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
- Configuration configuration = xmlConfigBuilder.parse(inputStream);
- //2.创建SqlSessionFactory工厂对象
- return new DefaultSqlSessionFactory(configuration);
- }
- }
复制代码
- XMLConfigBuilder核心配置解析类内里嵌套着XMLMapperBuilder映射配置文件解析类
- 输入流转化为Document对象,一是根据property标签获取数据库配置信息并创建数据源添加到configuration
- 二是根据mapper标签通过XMLMapperBuilder解析类遍历解析配置文件同样添加到configuration的map集合类
- public class XMLConfigBuilder {
- private Configuration configuration;
- public XMLConfigBuilder() {
- this.configuration = new Configuration();
- }
- // 使用dom4j+xpath解析
- public Configuration parse(InputStream inputStream) throws Exception {
- //将xml转化为Document对象
- Document document = new SAXReader().read(inputStream);
- //获取跟节点,对于sqlMapConfig.xml来说就是<Configuration>标签
- Element rootElement = document.getRootElement();
- // -------------解析数据库配置文件----------------
- // "//"表示从匹配选择的当前节点,而不考虑它们的位置
- // 即这里获取数据源url用户密码信息
- // 例:<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
- List<Element> propertyList = rootElement.selectNodes("//property");
- Properties properties = new Properties();
- for (Element element : propertyList) {
- // 获取<property>标签中,name和value属性的值
- String name = element.attributeValue("name");
- String value = element.attributeValue("value");
- properties.setProperty(name,value);
- }
- // 创建数据源对象
- DruidDataSource dataSource = new DruidDataSource();
- dataSource.setDriverClassName(properties.getProperty("driverClassName"));
- dataSource.setUrl(properties.getProperty("url"));
- dataSource.setUsername(properties.getProperty("username"));
- dataSource.setPassword(properties.getProperty("password"));
- // 将创建好的数据源添加到Configuration对象中
- configuration.setDataSource(dataSource);
- // -------------解析映射配置文件----------------
- /*
- 1.获取映射配置文件路径
- 2.根据路径进行映射文件的加载解析
- 3.封装到MapperStatement,存入configuration的map集合中
- */
- // 例:<mapper resource="mapper/UserMapper.xml"></mapper>
- List<Element> mapperList = rootElement.selectNodes("//mapper");
- for (Element element : mapperList) {
- String resource = element.attributeValue("resource");
- InputStream resourceAsStream = Resources.getResourceAsStream(resource);
- // XMLMapperBuilder 专门解析映射配置文件的对象-最后会存入configuration的map集合对象中
- XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
- xmlMapperBuilder.parse(resourceAsStream);
- }
- return configuration;
- }
- }
复制代码
- 与XMLConfigBuilder解析类原理一样
- 传入configuration,并将解析好的MapperStatement对象添加到mapperStatementMap
- public class XMLMapperBuilder {
- private Configuration configuration;
- public XMLMapperBuilder(Configuration configuration) {
- this.configuration = configuration;
- }
- public void parse(InputStream resourceAsStream) throws Exception {
- // 将输入流转化为Document对象,并获取跟节点<mapper>
- Document document = new SAXReader().read(resourceAsStream);
- Element rootElement = document.getRootElement();
- // 例:<mapper namespace="user">
- String namespace = rootElement.attributeValue("namespace");
- /* 例:
- <select id="selectOne" resultType="com.xc.pojo.User" parameterType="com.xc.pojo.User" >
- select * from user where id = #{id} and name = #{name}
- </select>
- */
- List<Element> selectList = rootElement.selectNodes("//select");
- for (Element element : selectList) {
- String id = element.attributeValue("id");
- String resultType = element.attributeValue("resultType");
- String parameterType = element.attributeValue("parameterType");
- String sql = element.getTextTrim();
- // 封装MapperStatement对象
- MapperStatement mapperStatement = new MapperStatement();
- String statementId = namespace + "." + id;
- mapperStatement.setStatementId(statementId);
- mapperStatement.setParameterType(parameterType);
- mapperStatement.setResultType(resultType);
- mapperStatement.setSql(sql);
- //第四节 自定义框架_优化才会用到
- mapperStatement.setSqlCommandType("select");
- // 添加到configurations的map集合中
- configuration.getMapperStatementMap().put(statementId,mapperStatement);
- }
- }
- }
复制代码 4、创建SqlSessionFactory工厂接口及DefaultSqlSessionFactory实现类
- 为了创建SqlSession会话,调用增删改查方法
- public interface SqlSessionFactory {
- // 创建SqlSession对象
- SqlSession openSession();
- }
复制代码
- 创建简朴实行器,与核心配置类共同创建SqlSession会话实现类
- configuration提供数据配置和sql以及参数和效果集封装
- simpleExecutor提供JDBC实行sql底层原理
- public class DefaultSqlSessionFactory implements SqlSessionFactory {
- private Configuration configuration;
- public DefaultSqlSessionFactory(Configuration configuration) {
- this.configuration = configuration;
- }
- @Override
- public SqlSession openSession() {
- // 1.创建执行器对象-具体的包装jdbc的sql操作,关闭连接等
- Executor simpleExecutor = new SimpleExecutor();
- // 2.创建sqlSession对象-判断执行增删改查哪些操作等
- return new DefaultSqlSession(configuration,simpleExecutor);
- }
- }
复制代码 5、创建SqlSession会话接口及DefaultSqlSession实现类
- statementId(“namespace.id”):定位具体Mapper.xml的sql语句以及入参和返回
- param:替换sql语句中的占位符?,可能字符串、对象、Map、集合
- public interface SqlSession {
- // 查询多个结果
- <E> List<E> selectList(String statementId, Object param) throws Exception;
- // 查询单个结果
- <T> T selectOne(String statementId, Object param) throws Exception;
- // 清理资源
- void close();
-
- }
复制代码
- 使用聚合进来的configuration对象获取MapperStatement映射配置对象向下传给实行器
- 另外一个聚合进来的executor简朴实行器来实行底层JDBC操纵
- DefaultSqlSession的作用则是聚合配置类分发到不同实行器的不同方法
- 实行器种类:简朴实行器、可重用实行器、批量实行器(这里只模仿第一种)
- public class DefaultSqlSession implements SqlSession {
- private Configuration configuration;
- private Executor executor;
- public DefaultSqlSession(Configuration configuration, Executor executor) {
- this.configuration = configuration;
- this.executor = executor;
- }
- @Override
- public <E> List<E> selectList(String statementId, Object param) throws Exception {
- // 根据StatementId获取映射配置对象MapperStatement
- MapperStatement mapperStatement = configuration.getMapperStatementMap().get(statementId);
- // 然后将具体的查询操作委派给SimpleExecutor执行器
- // 执行底层jdbc需要:1.数据库配置,2.sql配置信息
- return executor.query(configuration,mapperStatement,param);
- }
- @Override
- public <T> T selectOne(String statementId, Object param) throws Exception {
- // 调用selectList()
- List<Object> selectList = selectList(statementId, param);
- if (selectList.size() == 1){
- return (T) selectList.get(0);
- }else if (selectList.size() > 1){
- throw new Exception("返回数据不止一条!!!");
- }else {
- return null;
- }
- }
- @Override
- public void close() {
- executor.close();
- }
- }
复制代码 6、创建Executor实行器接口及SimpleExecutor实现类
- 实行器接口界说增删改查方法,具体的JDBC底层操纵由它的实现类来完成
- public interface Executor {
- <E> List<E> query(Configuration configuration, MapperStatement mapperStatement, Object param)
- throws Exception;
- void close();
- }
复制代码
- 实行器实现类团体流程就是JDBC那一套,从加载驱动随处理效果集
- getBoundSql方法功能:
- 一是将 <select>标签的sql语句“#{字段名}”替换成?赋值给finalSql。(这里使用的mybatis代码,不消深究就是根据正则表达式替换,最后有gitee代码链接)
- 二是将替换?时间的字段名取出来放到ParameterMapping对象中,有多个,根据?次序,放入parameterMappingList集合
- 入参根据<select>标签的parameterType属性获取全限定类名,反射获取Class对象
- 遍历parameterMappingList,获取字段名,加上Class对象获取Field属性类
- query查询方法有个param参数,即入参对象(有可能字符串,集合这里只考虑对象),通过Field属性和param对象通过反射获取属性值
- 效果集根据<select>标签的resultType属性获取全限定类名,反射获取实例对象
- 通过效果集元数据获取字段名columnName,再resultSet.getObject获取数据库中对应字段值
- 通过java实体字段名和Class对象获取对应字段的Get方法的Method
- 这里说下1和2步骤中两个字段不是一回事,如果数据库和实体中不一样,这里需要转化成一致
- @Data
- @AllArgsConstructor
- public class BoundSql {
- // 用?做占位符的sql语句
- private String finalSql;
- // 字段名称的集合
- private List<ParameterMapping> parameterMappingList;
- }
复制代码- @Data
- @AllArgsConstructor
- public class ParameterMapping {
- // 保存#{}中对于的字段名称
- private String content;
- }
复制代码 7、项目使用端使用自界说框架测试
- 优点:完成一张表的增删改查只需要创建一个实体类。(其他类接口都是必须要有的)
- 缺点1:sqlSession会话对象创建频仍,每次需要User的CRUD,就需要创建
- 缺点2:调用方法需要手动添加参数statementId:namespace.id
- @Test
- public void test1() throws Exception {
- // 1.根据配置文件的路径,加载成字节输入流,存到内存中 注意:配置文件还未解析
- InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
- // 2.解析配置文件,封装到Configuration对象,创建sqlSessionFactory工厂对象
- SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
- // 3.生产sqlSession 创建执行器对象
- SqlSession sqlSession = sqlSessionFactory.openSession();
- // 4.调用sqlSession方法
- User user = new User();
- user.setId(100);
- user.setName("zhangsan");
- User userOne = sqlSession.selectOne("user.selectOne", user);
- System.out.println("查询单个用户:"+userOne);
- List<User> userList = sqlSession.selectList("user.selectList",null);
- System.out.println("查询所有用户:"+userList.toString());
- // 5.释放资源
- sqlSession.close();
- }
复制代码 输出效果:
- 查询单个用户:User(id=100, name=zhangsan)
- 查询所有用户:[User(id=100, name=zhangsan), User(id=120, name=Lisi)]
复制代码 四、自界说框架_优化
1、优化思绪
- 去掉statementId硬编码:创建一个UserMapper或IUserDao接口,每个方法名、入参、返回值和mapper.xml的<select>标签的id、parameterType、resultType一一对应
- sqlSession创建频仍:通过动态署理创建IUserDao的实现类,内容则是sqlSession.selectOne和sqlSession.selectList;这样sqlSession只需要创建一次,以后每次需要User的CRUD,则调用署理对象对应方法即可
2、优化代码
在sqlSession中添加方法
- public interface SqlSession {
- ...
- // 生成代理对象
- <T> T getMapper(Class<?> mapperClass);
- }
复制代码 对应实现类方法
- 动态署理对象:不需要编译,运行期间生成字节码,通过传入的类加载器加载,只存在于内存中
- mapperClass:UserMapper或IUserDao接口的Class对象
- 署理类调用接口中的方法,则会被拦截进入invoke方法,因为没有目标类,则具体的实现都在invoke内里了
- 通过mapperClass和方法名获取到statementId
- statementId和参数有了,但是DefaultSqlSession要有CRUD查询和更新操纵,所以sqlCommandType来区分
- public class DefaultSqlSession implements SqlSession {
- private Configuration configuration;
- private Executor executor;
- public DefaultSqlSession(Configuration configuration, Executor executor) {
- this.configuration = configuration;
- this.executor = executor;
- }
- @Override
- public <E> List<E> selectList(String statementId, Object param) throws Exception {
- // 根据StatementId获取映射配置对象MapperStatement
- MapperStatement mapperStatement = configuration.getMapperStatementMap().get(statementId);
- // 然后将具体的查询操作委派给SimpleExecutor执行器
- // 执行底层jdbc需要:1.数据库配置,2.sql配置信息
- return executor.query(configuration,mapperStatement,param);
- }
- @Override
- public <T> T selectOne(String statementId, Object param) throws Exception {
- // 调用selectList()
- List<Object> selectList = selectList(statementId, param);
- if (selectList.size() == 1){
- return (T) selectList.get(0);
- }else if (selectList.size() > 1){
- throw new Exception("返回数据不止一条!!!");
- }else {
- return null;
- }
- }
- @Override
- public void close() {
- executor.close();
- }
- @Override
- public <T> T getMapper(Class<?> mapperClass) {
- // 使用JDK动态代理生成基于接口的对象
- // 1、创建一个类(代理类),实现目标接口,实现所有的方法实现
- // 2、动态代理类:代码运行期间生成的,而非编译期
- Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(),
- new Class[]{mapperClass}, new InvocationHandler() {
- // proxy:生成的代理对象本身,很少用
- // method:调用接口中哪个方法,则执行对应代理里的对应方法
- // args:调用方法的参数
- @Override
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- // 执行底层JDBC
- // 1.获取statementId
- // ps:约定接口中的方法名要与<select>标签的id属性名一致,
- // 这样就可以通过接口获取statementId = namespace.id
- String methodName = method.getName();// 方法名 - findAll
- String className = mapperClass.getName();//接口类名 - com.xc.do.IUserDao
- String statementId = className + "." + methodName;
- // 2.判断调用sqlSession中CRUD的什么方法
- // MapperStatement类添加属性sqlCommandType-sql增删改查类型
- MapperStatement mapperStatement = configuration.getMapperStatementMap().get(statementId);
- // select update delete insert
- String sqlCommandType = mapperStatement.getSqlCommandType();
- switch (sqlCommandType){
- case "select" :
- // 3.判断调用selectOne还是selectList
- // 获取返回值类型,
- Type genericReturnType = method.getGenericReturnType();
- // ParameterizedType是Type的子接口,表示一个有参数的类型,例如Collection<T>,Map<K,V>等
- if(genericReturnType instanceof ParameterizedType){
- if(args != null) {
- return selectList(statementId, args[0]);
- }
- return selectList(statementId, null);
- }
- return selectOne(statementId,args[0]);
- case "update":
- // 执行更新方法调用
- break;
- case "delete":
- // 执行delete方法调用
- break;
- case "insert":
- // 执行insert方法调用
- break;
- }
- return null;
- }
- });
- return (T) proxyInstance;
- }
- }
复制代码 3、优化后测试
- 缺点:相较于优化前,需要新添加一个接口
- 优点:不需要手动添加statementId,SqlSession不消频仍创建
- @Test
- public void test2() throws Exception {
- // 1.根据配置文件的路径,加载成字节输入流,存到内存中 注意:配置文件还未解析
- InputStream resourceAsSteam = Resources.getResourceAsStream("sqlMapConfig.xml");
- // 2.解析了配置文件,封装了Configuration对象 2.创建sqlSessionFactory工厂对象
- SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsSteam);
- // 3.生产sqlSession 创建了执行器对象
- SqlSession sqlSession = sqlSessionFactory.openSession();
- // 4.调用sqlSession方法
- IUserDao userDao = sqlSession.getMapper(IUserDao.class);
- List<User> all = userDao.findAll();
- for (User user : all) {
- System.out.println(user);
- }
- // 5.释放资源
- sqlSession.close();
- }
复制代码 五、Spring整合优化
- 与Spring整合之后,InputStream、SqlSessionFactory、SqlSession、IUserDao署理对象齐备不需要自己创建
- 全都交给了spring容器管理,我们要做的就是@Autowired IUserDao userDao
- 然后就可以用署理对象调用增删改查方法了
六、gitee代码
- 手写源码框架项目: https://gitee.com/xuchang614/own-mybatis
- 测试框架项目: https://gitee.com/xuchang614/own-mybatis-test
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |