Java-数据库(JDBC,Lombok,Mybatis)

笔记参考与柏码知识库

JDBC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//1. 通过DriverManager来获得数据库连接
try (Connection connection = DriverManager.getConnection("连接URL","用户名","密码");
//2. 创建一个用于执行SQL的Statement对象
Statement statement = connection.createStatement()){ //注意前两步都放在try()中,因为在最后需要释放资源!
//3. 执行SQL语句,并得到结果集
ResultSet set = statement.executeQuery("select * from 表名");
//4. 查看结果
while (set.next()){
...
}
}catch (SQLException e){
e.printStackTrace();
}
//5. 释放资源,try-with-resource语法会自动帮助我们close

DriverManager

用于管理数据库驱动。

添加日志打印:

1
2
3
static {
DriverManager.setLogWriter(new PrintWriter(System.out)); //这里直接设定为控制台输出
}

Statement

用来执行各种数据库操作。

批处理操作

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws ClassNotFoundException {
try (Connection connection = DriverManager.getConnection();
Statement statement = connection.createStatement()){

statement.addBatch("insert into user values ('f', 1234)");
statement.addBatch("insert into user values ('e', 1234)"); //添加每一条批处理语句
statement.executeBatch(); //一起执行

}catch (SQLException e){
e.printStackTrace();
}
}

登陆与SQL注入攻击

一般的登陆操作:

1
2
3
4
5
6
7
8
9
10
11
try (Connection connection = DriverManager.getConnection("URL","用户名","密码");
Statement statement = connection.createStatement();
Scanner scanner = new Scanner(System.in)){
ResultSet res = statement.executeQuery("select * from user where username='"+scanner.nextLine()+"'and pwd='"+scanner.nextLine()+"';");
while (res.next()){
String username = res.getString(1);
System.out.println(username+" 登陆成功!");
}
}catch (SQLException e){
e.printStackTrace();
}

输入:

1
2
3
Test
1111' or 1=1; --
# Test 登陆成功!

此时执行的SQL语句变为:

1
select * from user where username='Test' and pwd='1111' or 1=1; -- '

PreparedStatement

可以使用?作为占位符,来将输入进行替换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws ClassNotFoundException {
try (Connection connection = DriverManager.getConnection("URL","用户名","密码");
PreparedStatement statement = connection.prepareStatement("select * from user where username= ? and pwd=?;");
Scanner scanner = new Scanner(System.in)){

statement.setString(1, scanner.nextLine());
statement.setString(2, scanner.nextLine());
System.out.println(statement); //打印查看一下最终执行的
ResultSet res = statement.executeQuery();
while (res.next()){
String username = res.getString(1);
System.out.println(username+" 登陆成功!");
}
}catch (SQLException e){
e.printStackTrace();
}
}

管理事务

通过Connection对象调用setAutoCommit(false) 方法, 将SQL语句的提交(commit)由驱动程序转交给应用程序负责。也可以使用rollback()来手动回滚之前的全部操作, 回滚时可以设置回滚点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) throws ClassNotFoundException {
try (Connection connection = DriverManager.getConnection("URL","用户名","密码");
Statement statement = connection.createStatement()){

connection.setAutoCommit(false); //关闭自动提交,现在将变为我们手动提交
statement.executeUpdate("insert into user values ('a', 1234)");

Savepoint savepoint = connection.setSavepoint(); //创建回滚点
statement.executeUpdate("insert into user values ('b', 1234)");

connection.rollback(savepoint); //回滚到回滚点,撤销前面全部操作

statement.executeUpdate("insert into user values ('c', 1234)");

connection.commit(); //提交事务(注意,回滚之前的内容都没了)

}catch (SQLException e){
e.printStackTrace();
}
}

Lombok

使用注解来简化数据的定义。

Getter与Setter

1
2
3
4
5
6
7
8
@Getter
@Setter
@AllArgsConstructor
public class Student {
private Integer sid;
private String name;
private String sex;
}
1
2
@Accessors(chain = true)//可以链式调用方法
@Accessors(fluent = true)//去掉get与set,用成员变量作为函数名

Mybatis

使用xml文件来进行配置,减少代码量。

通过配置mybatis-config.xml来配置数据库信息,并基于此创建SqlSessionFactory对象,并创建SqlSession就可以完成几乎所有的数据库操作。

概述

可以在项目根目录下新建名为mybatis-config.xml的文件,来配置需要连接的数据库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${驱动类(含包名)}"/>
<property name="url" value="${数据库连接URL}"/>
<property name="username" value="${用户名}"/>
<property name="password" value="${密码}"/>
</dataSource>
</environment>
</environments>
</configuration>

读取配置文件:

1
2
3
4
5
6
7

public static void main(String[] args) throws FileNotFoundException {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(new FileInputStream("mybatis-config.xml"));
try (SqlSession sqlSession = sqlSessionFactory.openSession(true)){
//暂时还没有业务
}
}

读取实体类时,使用xml文件来建立映射规则。

实体类:

1
2
3
4
5
6
7
8
import lombok.Data;

@Data
public class Student {
int sid; //名称最好和数据库字段名称保持一致,不然可能会映射失败导致查询结果丢失
String name;
String sex;
}

在根目录下重新创建一个mapper文件夹,新建名为TestMapper.xml的文件作为映射器,填写映射规则:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="TestMapper">
<select id="selectStudent" resultType="com.test.entity.Student">
select * from student
</select>
</mapper>

之后在配置文件中添加Mapper映射器:

1
2
3
4
<mappers>
<mapper url="file:mappers/TestMapper.xml"/>
<!-- 这里用的是url,也可以使用其他类型,我们会在后面讲解 -->
</mappers>

之后可以在程序中使用:

1
2
3
4
5
6
7
public static void main(String[] args) throws FileNotFoundException {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(new FileInputStream("mybatis-config.xml"));
try (SqlSession sqlSession = sqlSessionFactory.openSession(true)){
List<Student> student = sqlSession.selectList("selectStudent");
student.forEach(System.out::println);
}
}

配置Mybatis

配置SqlSessionFactory

由于SqlSessionFactory一般只需要创建一次,因此我们可以创建一个工具类来集中创建SqlSession:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MybatisUtil {

//在类加载时就进行创建
private static SqlSessionFactory sqlSessionFactory;
static {
try {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(new FileInputStream("mybatis-config.xml"));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}

/**
* 获取一个新的会话
* @param autoCommit 是否开启自动提交(跟JDBC是一样的,如果不自动提交,则会变成事务操作)
* @return SqlSession对象
*/
public static SqlSession getSession(boolean autoCommit){
return sqlSessionFactory.openSession(autoCommit);
}
}

之后进行查询:

1
2
3
4
5
6
public static void main(String[] args) {
try (SqlSession sqlSession = MybatisUtil.getSession(true)){
List<Student> student = sqlSession.selectList("selectStudent");
student.forEach(System.out::println);
}
}

namespace绑定接口

还可以将namespace与一个接口绑定,同时修改配置文件中mapper定义。

1
2
3
public interface TestMapper {
List<Student> selectStudent();
}
1
2
3
4
5
<mapper namespace="com.test.mapper.TestMapper">
<select id="selectStudent" resultType="com.test.entity.Student">
select * from student
</select>
</mapper>
1
2
3
<mappers>
<mapper resource="com/test/mapper/TestMapper.xml"/>
</mappers>

之后通过SqlSession来获取结果。

1
2
3
4
5
6
7
public static void main(String[] args) {
try (SqlSession sqlSession = MybatisUtil.getSession(true)){
TestMapper testMapper = sqlSession.getMapper(TestMapper.class);
List<Student> student = testMapper.selectStudent();
student.forEach(System.out::println);
}
}

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/study"/>
<property name="username" value="test"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/test/mapper/TestMapper.xml"/>
</mappers>
</configuration>

可以使用多个环境,并通过id指定。

1
2
sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(new FileInputStream("mybatis-config.xml"), "环境ID");

别名

可以给类型起别名来简化Mapper编写。

1
2
3
4
<!-- 需要在environments的上方 -->
<typeAliases>
<typeAlias type="com.test.entity.Student" alias="Student"/>
</typeAliases>
1
2
3
4
5
<mapper namespace="com.test.mapper.TestMapper">
<select id="selectStudent" resultType="Student">
select * from student
</select>
</mapper>

也可以直接让Mybatis去扫描一个包,并将包下的所有类自动起别名(别名为首字母小写的类名)

1
2
3
<typeAliases>
<package name="com.test.entity"/>
</typeAliases>

也可以为指定实体类添加一个注解,来指定别名:

1
2
3
4
5
6
7
@Data
@Alias("lbwnb")
public class Student {
private int sid;
private String name;
private String sex;
}

增删改查

条件查询

1
Student getStudentBySid(int sid);
1
2
3
<select id="getStudentBySid" resultType="Student">
select * from student where sid = #{sid}
</select>

尽量使用#{}而不是${},前者使用PreparedStatement首先进行一次预编译,有效地防止SQL注入问题,但后者是直接传值。

插入、更新、删除

更换标签即可

1
int addStudent(Student student);
1
2
3
<insert id="addStudent" parameterType="Student">
insert into student(name, sex) values(#{name}, #{sex})
</insert>

复杂查询

一对多:

1
2
3
4
5
6
@Data
public class Teacher {
int tid;
String name;
List<Student> studentList;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="getTeacherByTid" resultMap="asTeacher">
select *, teacher.name as tname from student inner join teach on student.sid = teach.sid
inner join teacher on teach.tid = teacher.tid where teach.tid = #{tid}
</select>

<resultMap id="asTeacher" type="Teacher">
<id column="tid" property="tid"/>
<result column="tname" property="name"/>
<collection property="studentList" ofType="Student">
<id property="sid" column="sid"/>
<result column="name" property="name"/>
<result column="sex" property="sex"/>
</collection>
</resultMap>

多对一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
@Accessors(chain = true)
public class Student {
private int sid;
private String name;
private String sex;
private Teacher teacher;
}

@Data
public class Teacher {
int tid;
String name;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<resultMap id="test2" type="Student">
<id column="sid" property="sid"/>
<result column="name" property="name"/>
<result column="sex" property="sex"/>
<association property="teacher" javaType="Teacher">
<id column="tid" property="tid"/>
<result column="tname" property="name"/>
</association>
</resultMap>
<select id="selectStudent" resultMap="test2">
select *, teacher.name as tname from student left join teach on student.sid = teach.sid
left join teacher on teach.tid = teacher.tid
</select>

事务操作

和JDBC差不多,这里使用SqlSession来进行事务操作。

1
2
3
4
5
6
7
8
9
try (SqlSession sqlSession = MybatisUtil.getSession(false)){
TestMapper testMapper = sqlSession.getMapper(TestMapper.class);

testMapper.addStudent(new Student().setSex("男").setName("小王"));

testMapper.selectStudent().forEach(System.out::println);
sqlSession.rollback();
sqlSession.commit();
}

动态SQL

if

使用if标签进行可选查询。

1
2
3
4
5
6
7
8
9
10
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>

choose(when, otherwise)

类似switch语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null and author.name != null">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
</select>

trim(where, set)

where也可以作为标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
<where>
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</where>
</select>

可以通过trim元素定制等价于where的元素。

1
2
3
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>

prefixOverrides 属性会忽略通过管道符分隔的文本序列(注意此例中的空格是必要的)。上述例子会移除所有 prefixOverrides 属性中指定的内容,并且插入 prefix 属性中指定的内容。

set元素可以用于动态更新。

1
2
3
4
5
6
7
8
9
10
<update id="updateAuthorIfNecessary">
update Author
<set>
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email},</if>
<if test="bio != null">bio=#{bio}</if>
</set>
where id=#{id}
</update>

foreach

当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。

缓存机制

分为一级缓存和二级缓存。一级缓存在进行DML操作后会失效,且只针对于单个会话。

二级缓存需要在映射器xml文件中手动添加。

1
2
3
4
5
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>

可以添加useCache属性来为某个方法关闭缓存:

1
2
3
<select id="getStudentBySid" resultType="Student" useCache="false">
select * from student where sid = #{sid}
</select>

可以使用flushCache=”false”在每次执行后都清空缓存,还可以控制DML操作完成之后不清空缓存。

1
2
3
<select id="getStudentBySid" resultType="Student" flushCache="true">
select * from student where sid = #{sid}
</select>

读取顺序:二级>一级>数据库

如此使用缓存存在缓存一致性问题。可以通过关闭缓存来保证一致性。

1
2
3
<settings>
<setting name="cacheEnabled" value="false"/>
</settings>
1
2
3
<select id="getStudentBySid" resultType="Student" useCache="false" flushCache="true">
select * from student where sid = #{sid}
</select>

使用注解

使用注解编写SQL语句可以进一步简化接口映射和绑定。

1
2
3
4
5
6
7
8
9
10
11
12
@Results({
@Result(id = true, column = "tid", property = "tid"),
@Result(column = "name", property = "name"),
@Result(column = "tid", property = "studentList", many =
@Many(select = "getStudentByTid")
)
})
@Select("select * from teacher where tid = #{tid}")
Teacher getTeacherBySid(int tid);

@Select("select * from student inner join teach on student.sid = teach.sid where tid = #{tid}")
List<Student> getStudentByTid(int tid);

需要修改配置文件中的映射器注册:

1
2
3
4
<mappers>
<mapper class="com.test.mapper.MyMapper"/>
<!-- 也可以直接注册整个包下的 <package name="com.test.mapper"/> -->
</mappers>

还可以使用注解编写SQL语句,使用XML实现映射规则。

1
2
3
@ResultMap("test")
@Select("select * from student")
List<Student> getAllStudent();

使用@ConstructorArgs注解来指定构造方法,当出现两个以上参数时,需要使用@Param来指定参数名称:

1
2
3
4
5
6
@ConstructorArgs({
@Arg(column = "sid", javaType = int.class),
@Arg(column = "name", javaType = String.class)
})
@Select("select * from student where sid = #{sid} and sex = #{sex}")
Student getStudentBySidAndSex(@Param("sid") int sid, @Param("sex") String sex);

若两个参数一个是基本类型一个是对象类型,可以通过参数名称.属性的方式:

1
2
@Insert("insert into student(sid, name, sex) values(#{sid}, #{student.name}, #{student.sex})")
int addStudent(@Param("sid") int sid, @Param("student") Student student);

使用注解控制缓存:

1
2
3
4
5
6
@CacheNamespace(readWrite = false)
public interface MyMapper {

@Select("select * from student")
@Options(useCache = false)
List<Student> getAllStudent();

动态代理

使用反射框架实现的动态代理。