使用过mybatis的都清楚底层封装了jdbc的操作,将繁琐的jdbc的操作给屏蔽了。所以分析mybatis的原理之前,先来看看jdbc是如何工作的,statement有哪些特点。
DBC是由SUN公司提出的一些列规范,只定义了接口规范,具体实现由各个数据库厂商去实现,它是一种典型的桥接模式。
jdbc是一种规范,所谓规范,就是自己定义了标准接口,做了如下抽象:用Connection代表和数据库的连接,用Statement执行SQL,用ResultSet表示SQL返回的结果。
上面说的Connection、Statement、ResultSet都应该是接口,具体实现由各个数据库提供商提供。有了规范,可以通过统一的接口,访问多种类型的数据库,可随便切换数据库。
如下的测试代码就是对jdbc的使用。
public class JdbcTest {
public static final String URL = "jdbc:mysql://127.0.0.1:3306/mybatis?characterEncoding=UTF-8";
public static final String USERNAME = "root";
public static final String PASSWORD = "341341";
public Connection connection;
@Before
public void init() throws SQLException {
connection = DriverManager.getConnection(URL, USERNAME, PASSWORD); // 1.获取连接
}
@After
public void over() throws SQLException {
connection.close();
}
@Test
public void jdbcTest() throws SQLException {
String sql = "select * from users where `name` = ?";
PreparedStatement statement = connection.prepareStatement(sql);//2. 预编译sql
statement.setString(1, "森林");//3. 给占位符?设置参数
statement.execute();//4. 执行sql
ResultSet resultSet = statement.getResultSet();
while (resultSet.next()) {
System.out.println(resultSet.getString(1));
}
System.out.println("===============================");
statement.setString(1, "依依");//重复使用这个statement 设置参数即可执行
statement.execute();
resultSet = statement.getResultSet();
while (resultSet.next()) {
System.out.println(resultSet.getString(1));
}
// 释放资源
resultSet.close();
statement.close();
}
}
接口的由各个厂商来提供,实现类的类名无法得到统一,去创建Connection对象的时候,代码就会写死某个实现类。例如
Connection con=MySqlConnectionImpl("127.0.0.1",3306,"mybatis",userName,pwd);
为了解决这个问题,抽象出了驱动的概念Driver。Driver是通过反射的机制来动态的创建连接。而不同的Driver又交给DriverManager来管理。这样可以在URL中加上前缀,来识别使用哪个数据库的驱动来创建连接。例如
public static final String URL = "jdbc:mysql://127.0.0.1:3306/mybatis?characterEncoding=UTF-8";
这里的前缀中加了mysql。所以当执行
connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
DriverManager会根据前缀获得mysql的Driver驱动来创建数据库连接。
有了数据库连接后,通过如下的代码可以预编译sql得到一个sql声明。
PreparedStatement statement = connection.prepareStatement(sql);
可以一次编译多次执行。sql语句一样的时候,只需要设置参数就可以执行。
第一个参数是索引,表示第几个’?'占位符,从1开始 并不是从0开始。
有多个占位符,则需要执行多次这条语句。
statement.setString(1, "森林");
执行sql,返回结果是个boolean类型。执行的结果集放在了statement当中
statement.execute();
也可以使用executeQuery(),或者executeUpdate()。
两者的区别在于executeQuery只能执行查询操作。
executeUpdate执行增删改操作。
执行完execute操作后,会将执行结果保存在statement中。通过getResultSet可以获得执行的结果。
ResultSet resultSet = statement.getResultSet();
最基本的功能是执行静态的sql语句。
传输相关的功能,可以执行批处理和设置数据库返回的行数。
批处理
这里使用PreparedStatement无关紧要,因为这个接口继承了Statment接口。
以下代码是执行了100条插入,通过addBatch方法添加批处理参数。
设置完100条的参数后,executeBatch批处理执行这100条sql语句。记录时间,为了对比批处理的效率,设置了对照试验,不使用批处理,而是一条一条的执行,记录时间。
@Test
public void prepareBatchTest() throws SQLException {
String sql = "INSERT INTO `users` (`name`,age) VALUES (?,18);";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
long l = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
preparedStatement.setString(1, UUID.randomUUID().toString());
preparedStatement.addBatch(); // 添加批处理参数
}
preparedStatement.executeBatch(); // 批处理 一次发射
System.out.print("批处理:");
System.out.println(System.currentTimeMillis() - l);
preparedStatement.close();
}
@Test
public void Test() throws SQLException {
String sql = "INSERT INTO `users` (`name`,age) VALUES (?,18);";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
long l = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
preparedStatement.setString(1, UUID.randomUUID().toString());
preparedStatement.execute(); //单条执行
}
System.out.print("一条条执行:");
System.out.println(System.currentTimeMillis() - l);
preparedStatement.close();
}
设置数据库返回行数setFetchSize
setFetchSize最主要是为了减少网络交互次数设计的。访问ResultSet时,如果它每次只从服务器上取一行数据,则会产生大量的开销。setFetchSize的意思是当调用rs.next时,ResultSet会一次性从服务器上取得多少行数据回来,这样在下次rs.next时,它可以直接从内存中获取出数据而不需要网络交互,提高了效率。 这个设置可能会被某些JDBC驱动忽略的,而且设置过大也会造成内存的上升。
预处理sql语句,可以一次编译多次执行。只需要设置参数就可以执行。第一个jdbc使用的例子中已经体现了这一点。
还有一个更重要的优点,就是防止sql注入
sql注入
什么是sql注入呢,例如
String sql = "select * from users where `name` ='" + name + "'";
实验
做对比实验,分别使用Statment和PreparedStatment去执行相同的sql语句,看看有什么差别。代码如下
public int selectByName(String name) throws SQLException {
String sql = "select * from users where `name` ='" + name + "'";
System.out.println(sql);
Statement statement = connection.prepareStatement(sql);
statement.executeQuery(sql);
ResultSet resultSet = statement.getResultSet();
int count = 0;
while (resultSet.next()) {
count++;
}
statement.close();
return count;
}
public int selectByName2(String name) throws SQLException {
String sql = "SELECT * FROM users WHERE `name`=?";
PreparedStatement statement = connection.prepareStatement(sql);
statement.setString(1, name);
System.out.println(statement);
statement.executeQuery();
ResultSet resultSet = statement.getResultSet();
int count = 0;
while (resultSet.next()) {
count++;
}
statement.close();
return count;
}
@Test//防止sql注入 测试
public void injectTest() throws SQLException {
System.out.println(selectByName("森林"));
System.out.println(selectByName("森林' or '1'='1"));
System.out.println(selectByName2("森林' or '1'='1"));
}
两个几乎相同的方法,只是使用的Statement不同。selectByName使用的是Statment,selectByName2使用的是PreparedStatment。得到的结果如下。
正常操作查询name为森林得到结果有3个。但是通过sql注入之后,将数据库中所有的数据都查了出来,这就造成了安全隐患。而最后一行的结果可以看出PreparedStatment可以防止sql注入的发生。
那么PreparedStatment是如何做到防止sql注入的呢?
答:在设置参数的时候,setString方法中会给引号加上转译字符,转移操作在数据库端执行,这样就起到了防止sql注入的功能。
这是一个和存储过程相关的statement
存储过程
存储过程 (Stored Procedure) 是在大型数据库系统中 , 一组为了完成特定功能的 SQL 语句集 , 存储在数据库中 , 经过第一次编译后再次调用不需要再次编译 , 用户通过指定存储过程的名字并给出参数 (如果该存储过程带有参数) 来执行它 , 存储过程是数据库中的一个重要对象 ; 存储过程中可以包含 逻辑控制语句
和 数据操纵语句
, 它可以接受参数 , 输出参数 , 返回单个或多个结果集以及返回值 ;
mysql中的存储过程
第一行存储过程的名字后面跟着入参和出参。
sql语句中结果传入给出参 使用INTO
jdbc使用存储过程
可以设置入参、出参、读取出参。
@Test // 存储过程
public void processTest() throws SQLException {
CallableStatement statement = connection.prepareCall("call select_all(?,?)");
// 设置入参
statement.setString(1, "森林");
// 设置出参
statement.registerOutParameter(2, Types.INTEGER);
statement.execute();
ResultSet resultSet = statement.getResultSet();
int count = 0;
while (resultSet.next()) {
count++;
}
System.out.println(count);
// 读取出参
int totalCount = statement.getInt(2);
System.out.println("total:" + totalCount);
statement.close();
}