Qida's Blog

纸上得来终觉浅,绝知此事要躬行。

包装类型作为日常开发最常用的数据载体,使用时有一些点需要特别注意,否则容易踩坑,本文总结下。

常见问题

值、引用比较问题

首先看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Integer a = 1000;
Integer b = 1000;
Integer c = 100;
Integer d = 100;
Integer e = new Integer(100);
Integer f = new Integer(100);

// 值比较
System.out.println(("a equals b is " + (a.equals(b)))); // a equals b is true

// 引用比较,结果为 false
System.out.println("a == b is " + (a == b)); // a == b is false
// 引用比较,结果意外的为 true。这是由于 Integer 自动装箱时 [-128, 127] 使用了缓存,详见其 valueOf 方法的源码实现。
System.out.println(("c == d is " + (c == d))); // c == d is true
// 引用比较,结果为 false。因为 new Integer(int) 构造方法没有使用缓存。
System.out.println(("e == f is " + (e == f))); // e == f is false

值的比较务必使用 equals 方法。可以遵循《阿里巴巴 Java 开发规范》这条规范:

notes

自动装箱的性能问题

Java 语言提供了八种基本类型。其中包括:

  • 一种布尔类型
  • 一种字符类型
  • 六种数字类型(四种整数型,两种浮点型)

这些基本类型(primitive type)都有对应的包装类型(boxed primitive type),具有类的特性。基本类型和包装类型之间的转换通过装箱和拆箱方法:

基本数据类型 对应的包装类 拆箱方法 装箱方法 存储空间 取值范围 缓存范围
boolean Boolean booleanValue() valueOf(boolean) 1 bit true & false
char Character charValue() valueOf(char) 16 bit \u005Cu0000~\u005Cu007F
byte Byte byteValue() valueOf(byte) 8 bit -2^7~2^7-1 -2^7~2^7-1
short Short shortValue() valueOf(short) 16 bit -2^15~2^15-1 -2^7~2^7-1
int Integer intValue() valueOf(int) 32 bit -2^31~2^31-1 -2^7~2^7-1
long Long longValue() valueOf(long) 64 bit -2^63~2^63-1 -2^7~2^7-1
float Float floatValue() valueOf(float) 32 bit
double Double doubleValue() valueOf(double) 64 bit

其中数字类型的拆箱方法由共同的父类 java.lang.Number 定义:

Number方法

Number

自 JDK 1.5 版本后,Java 引入了自动装箱(autoboxing)、拆箱(auto-unboxing)作为语法糖,使基本类型和包装类型可以直接转换,减少使用包装类型的繁琐性。javac 编译会做相应处理,例如:

1
2
3
4
public static void main(String[] args) {
Integer i = 10;
int n = i;
}

反编译后代码如下:

1
2
3
4
public static void main(String args[]) {
Integer i = Integer.valueOf(10); // 自动装箱
int n = i.intValue(); // 自动拆箱
}

但在进行大批量数据操作时,装箱操作会有一定的性能损耗,看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 程序一:包装类型
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum); // 耗时 10390 ms

// 程序二:基本类型
long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum); // 耗时 1101 ms

程序一运行起来比程序二慢了十倍,因为变量 sum 的每次计算结果都被反复装箱,导致不必要的对象创建和较高的资源消耗,从而影响性能。代码反编译如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 程序一:包装类型
Long sum = Long.valueOf(0L);
for (long i = 0L; i < 2147483647L; i++) {
sum = Long.valueOf(sum.longValue() + i);
}
System.out.println(sum);

// 程序二:基本类型
long sum = 0L;
long i;
for (i = 0L; i < 2147483647L; i++) {
sum += i;
}
System.out.println(sum);

这提醒我们,在进行大批量数据操作时,要优先使用基本类型。为此 Java 8 还专门引入了:

  • IntStreamLongStreamDoubleStream 作为泛型类 Stream 的补充;

  • OptionalIntOptionalLongOptionalDouble 作为泛型类 Optional 的补充;

  • 以及一堆配套的基本类型特化的函数式接口。

那么什么时候应该使用装箱类型呢?

  1. 必须使用装箱基本类型作为类型参数,因为 Java 不允许使用基本类型。例如作为泛型集合中的元素、键和值,由于不能将基本类型作为类型参数,因此必须使用装箱基本类型。又例如,不能将变量声明为 ThreadLocal<int> 类型,因此必须使用 ThreadLocal<Integer>
  2. 在进行反射的方法调用时,必须使用装箱基本类型。

自动拆箱的 NPE 风险

要注意,当程序进行涉及装箱和拆箱基本类型的混合类型计算时,它会进行自动拆箱。当程序进行拆箱时,会有 NPE 风险:

1
2
3
4
5
Long sum = null;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i; // java.lang.NullPointerException
}
System.out.println(sum);

在日常开发中,数据库字段定义、或查询结果都可能为 null,因为自动拆箱,用基本类型接收会有 NPE 风险。可以遵循《阿里巴巴 Java 开发规范》这条规范:

notes2

进制转换

https://en.wikipedia.org/wiki/Category:Numeral_systems

https://en.wikipedia.org/wiki/Numeral_system

https://en.wikipedia.org/wiki/List_of_numeral_systems

https://en.wikipedia.org/wiki/Positional_notation

Base/Radix
底/基数
Name 英文简写 字面量前缀 备注
2 Binary 二进制 BIN 0B0b 参考官方文档
8 Octal 八进制 OCT 0
10 Decimal 十进制 DEC
16 Hexadecimal 十六进制 HEX 0X0x
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 十进制转成二进制:11111111
Integer.toBinaryString(255);
// 二进制转十进制:255
Integer.valueOf("11111111", 2);

// 十进制转成八进制:10
Integer.toOctalString(8);
// 八进制转成十进制:8
Integer.valueOf("10", 8);

// 十进制转成十六进制:F
Integer.toHexString(15);
// 十六进制转成十进制:15
Integer.valueOf("F", 16);

源码解析

最后,来总结下包装类型特点:

  • 所有包装类型都为 final,底层被包装的基本类型也都为 final。因此包装类型一旦初始化后,在运行期值是不可变的,非常安全。
  • ByteShortIntegerLong 装箱方法的某个数据段使用了缓存,因此 == 引用比较时可能会出现预期外的结果(true),详见源码。

java.lang.Byte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public final class Byte extends Number implements Comparable<Byte> {

/**
* The value of the {@code Byte}.
*
* @serial
*/
private final byte value;

/**
* Returns a {@code Byte} instance representing the specified
* {@code byte} value.
* If a new {@code Byte} instance is not required, this method
* should generally be used in preference to the constructor
* {@link #Byte(byte)}, as this method is likely to yield
* significantly better space and time performance since
* all byte values are cached.
*
* @param b a byte value.
* @return a {@code Byte} instance representing {@code b}.
* @since 1.5
*/
public static Byte valueOf(byte b) {
final int offset = 128;
return ByteCache.cache[(int)b + offset];
}

/**
* Returns the value of this {@code Byte} as a
* {@code byte}.
*/
public byte byteValue() {
return value;
}

/**
* Compares this object to the specified object. The result is
* {@code true} if and only if the argument is not
* {@code null} and is a {@code Byte} object that
* contains the same {@code byte} value as this object.
*
* @param obj the object to compare with
* @return {@code true} if the objects are the same;
* {@code false} otherwise.
*/
public boolean equals(Object obj) {
if (obj instanceof Byte) {
return value == ((Byte)obj).byteValue();
}
return false;
}
}

java.lang.Short

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public final class Short extends Number implements Comparable<Short> {

/**
* The value of the {@code Short}.
*
* @serial
*/
private final short value;

/**
* Returns a {@code Short} instance representing the specified
* {@code short} value.
* If a new {@code Short} instance is not required, this method
* should generally be used in preference to the constructor
* {@link #Short(short)}, as this method is likely to yield
* significantly better space and time performance by caching
* frequently requested values.
*
* This method will always cache values in the range -128 to 127,
* inclusive, and may cache other values outside of this range.
*
* @param s a short value.
* @return a {@code Short} instance representing {@code s}.
* @since 1.5
*/
public static Short valueOf(short s) {
final int offset = 128;
int sAsInt = s;
if (sAsInt >= -128 && sAsInt <= 127) { // must cache
return ShortCache.cache[sAsInt + offset];
}
return new Short(s);
}

/**
* Returns the value of this {@code Short} as a
* {@code short}.
*/
public short shortValue() {
return value;
}

/**
* Compares this object to the specified object. The result is
* {@code true} if and only if the argument is not
* {@code null} and is a {@code Short} object that
* contains the same {@code short} value as this object.
*
* @param obj the object to compare with
* @return {@code true} if the objects are the same;
* {@code false} otherwise.
*/
public boolean equals(Object obj) {
if (obj instanceof Short) {
return value == ((Short)obj).shortValue();
}
return false;
}

}

java.lang.Integer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public final class Integer extends Number implements Comparable<Integer> {

/**
* The value of the {@code Integer}.
*
* @serial
*/
private final int value;

/**
* Returns an {@code Integer} instance representing the specified
* {@code int} value. If a new {@code Integer} instance is not
* required, this method should generally be used in preference to
* the constructor {@link #Integer(int)}, as this method is likely
* to yield significantly better space and time performance by
* caching frequently requested values.
*
* This method will always cache values in the range -128 to 127,
* inclusive, and may cache other values outside of this range.
*
* @param i an {@code int} value.
* @return an {@code Integer} instance representing {@code i}.
* @since 1.5
*/
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

/**
* Returns the value of this {@code Integer} as an
* {@code int}.
*/
public int intValue() {
return value;
}

/**
* Compares this object to the specified object. The result is
* {@code true} if and only if the argument is not
* {@code null} and is an {@code Integer} object that
* contains the same {@code int} value as this object.
*
* @param obj the object to compare with.
* @return {@code true} if the objects are the same;
* {@code false} otherwise.
*/
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}

}

java.lang.Long

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public final class Long extends Number implements Comparable<Long> {

/**
* The value of the {@code Long}.
*
* @serial
*/
private final long value;

/**
* Returns a {@code Long} instance representing the specified
* {@code long} value.
* If a new {@code Long} instance is not required, this method
* should generally be used in preference to the constructor
* {@link #Long(long)}, as this method is likely to yield
* significantly better space and time performance by caching
* frequently requested values.
*
* Note that unlike the {@linkplain valueOf(int)
* corresponding method} in the {@code Integer} class, this method
* is <em>not</em> required to cache values within a particular
* range.
*
* @param l a long value.
* @return a {@code Long} instance representing {@code l}.
* @since 1.5
*/
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
return LongCache.cache[(int)l + offset];
}
return new Long(l);
}

/**
* Returns the value of this {@code Long} as a
* {@code long} value.
*/
public long longValue() {
return value;
}

/**
* Compares this object to the specified object. The result is
* {@code true} if and only if the argument is not
* {@code null} and is a {@code Long} object that
* contains the same {@code long} value as this object.
*
* @param obj the object to compare with.
* @return {@code true} if the objects are the same;
* {@code false} otherwise.
*/
public boolean equals(Object obj) {
if (obj instanceof Long) {
return value == ((Long)obj).longValue();
}
return false;
}

}

java.lang.Float

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public final class Float extends Number implements Comparable<Float> {

/**
* The value of the Float.
*
* @serial
*/
private final float value;

/**
* Returns a {@code Float} instance representing the specified
* {@code float} value.
* If a new {@code Float} instance is not required, this method
* should generally be used in preference to the constructor
* {@link #Float(float)}, as this method is likely to yield
* significantly better space and time performance by caching
* frequently requested values.
*
* @param f a float value.
* @return a {@code Float} instance representing {@code f}.
* @since 1.5
*/
public static Float valueOf(float f) {
return new Float(f);
}

/**

* Compares this object against the specified object. The result
* is {@code true} if and only if the argument is not
* {@code null} and is a {@code Float} object that
* represents a {@code float} with the same value as the
* {@code float} represented by this object. For this
* purpose, two {@code float} values are considered to be the
* same if and only if the method {@link #floatToIntBits(float)}
* returns the identical {@code int} value when applied to
* each.
*
* <p>Note that in most cases, for two instances of class
* {@code Float}, {@code f1} and {@code f2}, the value
* of {@code f1.equals(f2)} is {@code true} if and only if
*
* <blockquote><pre>
* f1.floatValue() == f2.floatValue()
* </pre></blockquote>
*
* <p>also has the value {@code true}. However, there are two exceptions:
* <ul>
* <li>If {@code f1} and {@code f2} both represent
* {@code Float.NaN}, then the {@code equals} method returns
* {@code true}, even though {@code Float.NaN==Float.NaN}
* has the value {@code false}.
* <li>If {@code f1} represents {@code +0.0f} while
* {@code f2} represents {@code -0.0f}, or vice
* versa, the {@code equal} test has the value
* {@code false}, even though {@code 0.0f==-0.0f}
* has the value {@code true}.
* </ul>
*
* This definition allows hash tables to operate properly.
*
* @param obj the object to be compared
* @return {@code true} if the objects are the same;
* {@code false} otherwise.
* @see java.lang.floatToIntBits(float)
*/
public boolean equals(Object obj) {
return (obj instanceof Float)
&& (floatToIntBits(((Float)obj).value) == floatToIntBits(value));
}

}

java.lang.Double

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public final class Double extends Number implements Comparable<Double> {

/**
* The value of the Double.
*
* @serial
*/
private final double value;

/**
* Returns a {@code Double} instance representing the specified
* {@code double} value.
* If a new {@code Double} instance is not required, this method
* should generally be used in preference to the constructor
* {@link #Double(double)}, as this method is likely to yield
* significantly better space and time performance by caching
* frequently requested values.
*
* @param d a double value.
* @return a {@code Double} instance representing {@code d}.
* @since 1.5
*/
public static Double valueOf(double d) {
return new Double(d);
}

/**
* Compares this object against the specified object. The result
* is {@code true} if and only if the argument is not
* {@code null} and is a {@code Double} object that
* represents a {@code double} that has the same value as the
* {@code double} represented by this object. For this
* purpose, two {@code double} values are considered to be
* the same if and only if the method {@link
* #doubleToLongBits(double)} returns the identical
* {@code long} value when applied to each.
*
* <p>Note that in most cases, for two instances of class
* {@code Double}, {@code d1} and {@code d2}, the
* value of {@code d1.equals(d2)} is {@code true} if and
* only if
*
* <blockquote>
* {@code d1.doubleValue() == d2.doubleValue()}
* </blockquote>
*
* <p>also has the value {@code true}. However, there are two
* exceptions:
* <ul>
* <li>If {@code d1} and {@code d2} both represent
* {@code Double.NaN}, then the {@code equals} method
* returns {@code true}, even though
* {@code Double.NaN==Double.NaN} has the value
* {@code false}.
* <li>If {@code d1} represents {@code +0.0} while
* {@code d2} represents {@code -0.0}, or vice versa,
* the {@code equal} test has the value {@code false},
* even though {@code +0.0==-0.0} has the value {@code true}.
* </ul>
* This definition allows hash tables to operate properly.
* @param obj the object to compare with.
* @return {@code true} if the objects are the same;
* {@code false} otherwise.
* @see java.lang.Double#doubleToLongBits(double)
*/
public boolean equals(Object obj) {
return (obj instanceof Double)
&& (doubleToLongBits(((Double)obj).value) ==
doubleToLongBits(value));
}

}

java.lang.Boolean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public final class Boolean implements java.io.Serializable, Comparable<Boolean> {

/**
* The value of the Boolean.
*
* @serial
*/
private final boolean value;

/**
* Returns a {@code Boolean} instance representing the specified
* {@code boolean} value. If the specified {@code boolean} value
* is {@code true}, this method returns {@code Boolean.TRUE};
* if it is {@code false}, this method returns {@code Boolean.FALSE}.
* If a new {@code Boolean} instance is not required, this method
* should generally be used in preference to the constructor
* {@link #Boolean(boolean)}, as this method is likely to yield
* significantly better space and time performance.
*
* @param b a boolean value.
* @return a {@code Boolean} instance representing {@code b}.
* @since 1.4
*/
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}

/**
* Returns the value of this {@code Boolean} object as a boolean
* primitive.
*
* @return the primitive {@code boolean} value of this object.
*/
public boolean booleanValue() {
return value;
}

/**
* Returns {@code true} if and only if the argument is not
* {@code null} and is a {@code Boolean} object that
* represents the same {@code boolean} value as this object.
*
* @param obj the object to compare with.
* @return {@code true} if the Boolean objects represent the
* same value; {@code false} otherwise.
*/
public boolean equals(Object obj) {
if (obj instanceof Boolean) {
return value == ((Boolean)obj).booleanValue();
}
return false;
}

}

java.lang.Character

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public final class Character implements java.io.Serializable, Comparable<Character> {

/**
* The value of the {@code Character}.
*
* @serial
*/
private final char value;

/**
* Returns a <tt>Character</tt> instance representing the specified
* <tt>char</tt> value.
* If a new <tt>Character</tt> instance is not required, this method
* should generally be used in preference to the constructor
* {@link #Character(char)}, as this method is likely to yield
* significantly better space and time performance by caching
* frequently requested values.
*
* This method will always cache values in the range {@code
* '\u005Cu0000'} to {@code '\u005Cu007F'}, inclusive, and may
* cache other values outside of this range.
*
* @param c a char value.
* @return a <tt>Character</tt> instance representing <tt>c</tt>.
* @since 1.5
*/
public static Character valueOf(char c) {
if (c <= 127) { // must cache
return CharacterCache.cache[(int)c];
}
return new Character(c);
}

/**
* Returns the value of this {@code Character} object.
* @return the primitive {@code char} value represented by
* this object.
*/
public char charValue() {
return value;
}

/**
* Compares this object against the specified object.
* The result is {@code true} if and only if the argument is not
* {@code null} and is a {@code Character} object that
* represents the same {@code char} value as this object.
*
* @param obj the object to compare with.
* @return {@code true} if the objects are the same;
* {@code false} otherwise.
*/
public boolean equals(Object obj) {
if (obj instanceof Character) {
return value == ((Character)obj).charValue();
}
return false;
}

}

参考

《Effective Java 第三版》

  • 第 44 条:坚持使用标准的函数接口
  • 第 61 条:基本类型优先于装箱基本类型

《Java 8 函数式编程》

《Java 8 实战》

https://docs.oracle.com/javase/tutorial/java/data/autoboxing.html

一文读懂什么是Java中的自动拆装箱

「传统的 OLTP 应用程序」和「现代的 WEB 应用程序」通常会执行许多小型数据更改操作,其中并发性能至关重要。本文记录如何优化这些批量插入与更新操作。

优化思路

https://dev.mysql.com/doc/refman/8.0/en/data-change-optimization.html

INSERT

If you are inserting many rows from the same client at the same time, use INSERT statements with multiple VALUES lists to insert several rows at a time. This is considerably faster (many times faster in some cases) than using separate single-row INSERT statements.

UPDATE

JDBC

使用 PreparedStatementaddBatch() 方法并执行 executeBatch() 批量发送多个操作给数据库,减少网络交互次数。

MyBatis

INSERT

MyBatis 批量插入几千条数据慎用 foreach。应使用 BATCH 模式,只发出一条 SQL 语句,预编译一次,但设置参数 N 次,执行一次。

1
2
3
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
...
}

ExecutorType.BATCH

相比之下,默认的 SIMPLE 模式,每一条语句都会发出一条 SQL,进行一次预编译,设置一次参数,执行一次。

ExecutorType.SIMPLE

两者性能相差很大。

参考:

UPDATE

MyBatis Plus

INSERT

UPDATE

MyBatis Plus 只提供了根据主键 ID 进行批量更新的 updateBatchById 的方法,以下是根据某个或者多个非 ID 字段进行批量更新的自定义方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public boolean batchUpdate(Collection<SettlementDetailDO> entityList) {
String sqlStatement = getSqlStatement(SqlMethod.UPDATE);
return executeBatch(entityList, DEFAULT_BATCH_SIZE, (sqlSession, entity) -> {
MapperMethod.ParamMap<Object> param = new MapperMethod.ParamMap<>();
param.put(Constants.ENTITY, SettlementDetailDO.builder()
...
.build());
param.put(Constants.WRAPPER, Wrappers.<SettlementDetailDO>lambdaQuery()
.eq(SettlementDetailDO::getTransactionId, entity.getTransactionId())
.eq(SettlementDetailDO::getSettlementParty, entity.getSettlementParty())
.eq(SettlementDetailDO::getSettlementStatus, SettlementStatusEnum.INIT)
);
sqlSession.update(sqlStatement, param);
});

参考:《MyBatis Plus 根据某个指定字段批量更新数据库

参考

Configuration XML

TypeHandler

https://mybatis.org/mybatis-3/configuration.html#typeHandlers

Whenever MyBatis sets a parameter on a PreparedStatement or retrieves a value from a ResultSet, a TypeHandler is used to retrieve the value in a means appropriate to the Java type.

org.apache.ibatis.type.TypeHandler 接口实现类:

TypeHandler

TypeHandler 可用于以下场景:

参考例子:https://github.com/qidawu/mybatis-test/tree/master/src/main/java/org/mybatis/example/typehandler

使用方式:

1
2
<!-- 手动结果映射 -->
<result column="..." jdbcType="..." typehandler="..." />
1
2
// 自动结果映射(MyBatis Plus)
@TableField(typeHandler = ...class)

Mapper XML Files

https://mybatis.org/mybatis-3/sqlmap-xml.html

查询

1
2
3
4
5
<!-- 手动结果映射 -->
<select id="selectPage" resultMap="BaseResultMap">

<!-- 自动结果映射(automatic mapping) -->
<select id="selectPage" resultType="...">

手动结果映射

https://mybatis.org/mybatis-3/sqlmap-xml.html#Result_Maps

结果映射,用于自定义结果集(Result Maps):

参考源码:

DefaultResultSetHandler#handleRowValues

DefaultResultSetHandler#createResultObject

可变/不可变类的结果映射

如果是可变类(mutable classes),可以使用 Setter injection 进行结果映射(注意,必须指定 property 属性):

id – an ID result; flagging results as ID will help improve overall performance
result – a normal result injected into a field or JavaBean property

1
2
3
4
5
<resultMap id="BaseResultMap" type="...">
<id column="id" jdbcType="BIGINT" property="id" />
<result column="create_time" jdbcType="BIGINT" property="createTime" />
<result column="update_time" jdbcType="BIGINT" property="updateTime" />
</resultMap>

但如果是不可变类(immutable classes),由于都是 final field,没有 Setter 方法,则只能通过 Constructor injection 进行结果映射:

constructor - used for injecting results into the constructor of a class upon instantiation

  • idArg - ID argument; flagging results as ID will help improve overall performance
  • arg - a normal result injected into the constructor

注意,arg 元素有两种设置方式:

  • 有序的 arg。必须指定 javaType 属性,否则构造方法参数默认使用 java.lang.Object 类型会构造报错。

    1
    2
    3
    4
    5
    6
    7
    <resultMap id="BaseResultMap" type="...">
    <constructor>
    <idArg column="id" javaType="java.lang.Long" />
    <arg column="create_time" javaType="java.lang.Long" />
    <arg column="update_time" javaType="java.lang.Long" />
    </constructor>
    </resultMap>
  • 无序的 arg。通过指定 name 属性,可以忽略 javaType 属性。

    1
    2
    3
    4
    5
    6
    7
    <resultMap id="BaseResultMap" type="...">
    <constructor>
    <idArg column="id" name="id" />
    <arg column="create_time" name="createTime" />
    <arg column="update_time" name="updateTime" />
    </constructor>
    </resultMap>

When you are dealing with a constructor with many parameters, maintaining the order of arg elements is error-prone.

Since 3.4.3, by specifying the name of each parameter, you can write arg elements in any order. To reference constructor parameters by their names, you can

  • either add @Param annotation to them

  • or compile the project with -parameters‘ compiler option and enable useActualParamName (this option is enabled by default).

    设置名 描述 有效值 默认值
    useActualParamName 允许使用方法签名中的名称作为语句参数名称。 为了使用该特性,你的项目必须采用 Java 8 编译,并且加上 -parameters 选项。 true, false true
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <!-- https://maven.apache.org/plugins/maven-compiler-plugin/examples/pass-compiler-arguments.html -->
    <project>
    <build>
    <plugins>
    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
    <compilerArgs>
    <arg>-parameters</arg>
    </compilerArgs>
    </configuration>
    </plugin>
    </plugins>
    </build>
    </project>

javaType can be omitted if there is a property with the same name and type.

注意,如果抛出异常如下:

1
org.apache.ibatis.executor.ExecutorException: No constructor found in ...

解决方案:

  • 如果是可变类(mutable classes),为类添加无参构造方法。
  • 如果是不可变类(immutable classes),Mapper XML 结果映射 resultMap 必须使用正确参数的 constructor

嵌套结果映射

两种嵌套关系配置:

  • association “has-one” type relationship
  • collection “has many” type relationship

两种查询方式:

  • 嵌套查询(Nested Select),即分开多次查询。
  • 嵌套结果映射(Nested Results),即利用表连接语法进行一次性的连表查询;

The association element deals with a “has-one” type relationship. For example, in our example, a Blog has one Author. An association mapping works mostly like any other result. You specify the target property, the javaType of the property (which MyBatis can figure out most of the time), the jdbcType if necessary and a typeHandler if you want to override the retrieval of the result values.

Where the association differs is that you need to tell MyBatis how to load the association. MyBatis can do so in two different ways:

  • Nested Select: By executing another mapped SQL statement that returns the complex type desired.
  • Nested Results: By using nested result mappings to deal with repeating subsets of joined results.

总结:

嵌套查询(Nested Select) 嵌套结果映射(Nested Results)
association 多对一关联 禁用 可用
collection 一对多关联 可用 可用
  • association 多对一关联,嵌套查询(Nested Select)存在 N+1 次查询的性能问题,不建议使用。

    While this approach Nested Select is simple, it will not perform well for large data sets or lists. This problem is known as the “N+1 Selects Problem”. In a nutshell, the N+1 selects problem is caused like this:

    • You execute a single SQL statement to retrieve a list of records (the “+1”).
    • For each record returned, you execute a select statement to load details for each (the “N”).

    This problem could result in hundreds or thousands of SQL statements to be executed. This is not always desirable.

    The upside is that MyBatis can lazy load such queries, thus you might be spared the cost of these statements all at once. However, if you load such a list and then immediately iterate through it to access the nested data, you will invoke all of the lazy loads, and thus performance could be very bad.

    And so, there is another way Nested Results.

  • collection 一对多关联,两种配置方式的区别如下:

1
2
3
4
5
6
7
8
9
<!-- 一对多的嵌套结果映射(Nested Results)。注意表连接查询字段务必使用 AS 别名,避免手工映射时 column 取错列 -->
<resultMap id="ExtendResultMap" extends="BaseResultMap" type="...XxxQO">
<collection property="extendList" columnPrefix="sub_task_" resultMap="...XxxMapper.BaseResultMap" ofType="...XxxPO" />
</resultMap>

<!-- 一对多的嵌套查询(Nested Select),column 为嵌套查询的参数列,即“一”方的列 -->
<resultMap id="ExtendResultMap" extends="BaseResultMap" type="...XxxQO">
<collection property="extendList" column="task_no" select="...XxxMapper.getByTaskNo" ofType="...XxxPO" />
</resultMap>

下面演示一个例子,涉及的关联关系如下:学生多对一关联学校、一对多关联书本:

StudentMapper.java

1
List<StudentQO> listStudents(int age);

StudentQO.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Getter
@Setter
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class StudentQO extends StudentPO {

/**
* 多对一关联学校
*/
private SchoolPO school;

/**
* 一对多关联书本
*/
private List<BookPO> books;

}

StudentMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<sql id="Base_column_list">
T.id,
......
T.create_time,
T.update_time,
T.version
</sql>

<sql id="Extend_column_list">
<include refid="Base_column_list" />
,
<!-- 见 SchoolMapper.xml -->
<include refid="com.test.school.mapper.SchoolMapper.Base_Column_List" />
</sql>

<resultMap id="BaseResultMap" type="com.test.student.po.StudentPO">
<id column="id" property="id" jdbcType="BIGINT"/>
......
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
<result column="version" property="version" jdbcType="TINYINT"/>
</resultMap>

<resultMap id="ExtendResultMap" extends="BaseResultMap" type="com.test.student.qo.StudentQO">
<!-- 使用嵌套结果映射,即利用表连接语法进行一次性的连表查询。resultMap 配置见 SchoolMapper.xml -->
<association property="school" columnPrefix="school_" resultMap="com.test.school.mapper.SchoolMapper.BaseResultMap" />
<!-- 使用嵌套查询,即分开多次查询。column 为嵌套查询的参数列,select 引用见 BookMapper.xml -->
<collection property="books" column="student_no" select="com.test.book.mapper.BookMapper.getByStudentNo" ofType="com.test.book.po.BookPO" />
</resultMap>

<!-- 使用表连接嵌套查询学生、学校 -->
<select id="listStudents" resultMap="ExtendResultMap">
SELECT
<include refid="Extend_column_list" />
FROM t_student T
INNER JOIN t_school E
ON T.school_no = E.school_no
WHERE T.age = #{age}
</select>

SchoolMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<sql id="Base_Column_List">
E.id AS school_id,
......
E.create_time AS school_create_time,
E.update_time AS school_update_time
</sql>

<resultMap id="BaseResultMap" type="com.test.school.po.SchoolPO">
<id column="id" property="id" jdbcType="BIGINT"/>
......
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
</resultMap>

自动结果映射

https://mybatis.org/mybatis-3/sqlmap-xml.html#Auto-mapping

相关配置

设置名 描述 有效值 默认值
autoMappingBehavior 指定 MyBatis 应如何自动映射列到字段或属性。
* NONE 表示关闭自动映射
* PARTIAL 只会自动映射没有定义嵌套结果映射的字段
* FULL 会自动映射任何复杂的结果集(无论是否嵌套)
NONE, PARTIAL, FULL PARTIAL
autoMappingUnknownColumnBehavior 指定发现自动映射目标未知列(或未知属性类型)的行为。
* NONE: 不做任何反应
* WARNING: 输出警告日志('org.apache.ibatis.session.AutoMappingUnknownColumnBehavior' 的日志等级必须设置为 WARN
* FAILING: 映射失败 (抛出 SqlSessionException)
NONE, WARNING, FAILING NONE
mapUnderscoreToCamelCase 是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn。 true, false false
argNameBasedConstructorAutoMapping 当应用构造器自动映射时,参数名称被用来搜索要映射的列,而不再依赖列的顺序。(新增于 3.5.10) true, false false

自动结果映射常用于搭配 MyBatis 注解 @Select 使用。

MyBatis 自动结果映射时,如果有多个构造方法,可以通过 @AutomapConstructor 指定自动映射使用哪个。例如搭配 lombok 使用:

1
@AllArgsConstructor(onConstructor=@__({@AutomapConstructor}))

如果构造方法未指定 @AutomapConstructor,会按字段类型查找匹配的构造方法。如果找不到该构造方法,则会报错。

缓存

一级缓存

也称为本地缓存,与 SqlSession 绑定,只存在于 SqlSession 生命周期。

二级缓存

https://github.com/mybatis/memcached-cache

https://github.com/mybatis/ignite-cache

https://github.com/mybatis/redis-cache

https://github.com/mybatis/hazelcast-cache

https://github.com/mybatis/caffeine-cache

https://github.com/mybatis/couchbase-cache

https://github.com/mybatis/oscache-cache

https://github.com/mybatis/ehcache-cache

参考:

MyBatis 二级缓存 关联刷新实现

插入

https://mybatis.org/mybatis-3/sqlmap-xml.html#insert_update_and_delete

回写主键

MyBatis 回写主键利用了 JDBC 的特性,适用于支持自动生成主键的数据库,比如 MySQL 和 SQL Server。

First, if your database supports auto-generated key fields (e.g. MySQL and SQL Server), then you can simply set useGeneratedKeys="true" and set the keyProperty to the target property and you’re done.

1
2
3
4
<insert id="insertAuthor" useGeneratedKeys="true" keyProperty="id">
insert into Author (username,password,email,bio)
values (#{username},#{password},#{email},#{bio})
</insert>

keyProperty attribute:

(insert and update only) Identifies a property into which MyBatis will set the key value returned by getGeneratedKeys, or by a selectKey child element of the insert statement. Default: unset.

自定义生成主键

适用于不支持自动生成主键的数据库,比如 Oracle。

MyBatis has another way to deal with key generation for databases that don’t support auto-generated column types, or perhaps don’t yet support the JDBC driver support for auto-generated keys.

Here’s a simple (silly) example that would generate a random ID (something you’d likely never do, but this demonstrates the flexibility and how MyBatis really doesn’t mind).

The selectKey statement would be run first, the Author id property would be set, and then the insert statement would be called. This gives you a similar behavior to an auto-generated key in your database without complicating your Java code.

1
2
3
4
5
6
7
8
9
<insert id="insertAuthor">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
</selectKey>
insert into Author
(id, username, password, email,bio, favourite_section)
values
(#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR})
</insert>

The selectKey element is described as follows:

1
2
3
4
5
<selectKey
keyProperty="id"
resultType="int"
order="BEFORE"
statementType="PREPARED">

注解方式:

1
2
@Insert("insert into Author(id, username, password, email,bio, favourite_section) values (#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR})")
@SelectKey(keyProperty = "id", resultType = Long.class, before = false, statement = "SELECT LAST_INSERT_ID()")

Dynamic SQL

https://mybatis.org/mybatis-3/dynamic-sql.html

script

For using dynamic SQL in annotated mapper class, script element can be used.

与 Spring 整合

依赖安装

基础依赖安装:

1
2
3
4
5
6
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>x.x.x</version>
</dependency>

Spring 整合依赖安装:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- MyBatis Spring -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>x.x.x</version>
</dependency>

<!-- MyBatis Spring Boot Starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>x.x.x</version>
</dependency>

Spring Bean 配置

In base MyBatis, the SqlSessionFactory is built using SqlSessionFactoryBuilder. In MyBatis-Spring, SqlSessionFactoryBean is used instead.

MyBatis-Spring 中,SqlSessionFactoryBean 用于创建 SqlSessionFactory。主要的配置参数如下:

  • dataSource 数据源类
  • configLocation 配置文件的位置
  • mapperLocations Mapper XML 文件的位置
  • typeAliasesPackage

MyBatis-Spring 中,Mapper API 的扫描查找有两种方式:

  • 配置 org.mybatis.spring.mapper.MapperScannerConfigurer 主动扫描指定 basePackage 路径
  • 为 Mapper API 加上类注解:org.mybatis.spring.annotation.MapperScan

下面演示如何通过 Spring 的 XML 配置文件或 Java Config 进行配置。

XML

1
2
3
4
5
6
7
8
9
10
11
<!-- 配置 MyBatis 的 sqlSessionFactory。MyBatis Plus 使用 com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:mybatis-config.xml"/>
</bean>

<!-- DAO 接口所在包名,Spring 会自动查找其下的类 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.example.dao" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>

Java Config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import javax.sql.DataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;

@MapperScan(basePackages = {"..."})
public class MyBatisConfig {

/**
* 用于创建 SqlSessionFactory,以便获取 SqlSession
**/
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setConfigLocation(configLocation);
factoryBean.setMapperLocations(resource);
return factoryBean;
}

/**
* @MapperScan 或 MapperScannerConfigurer 二选一配置
**/
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
mapperScannerConfigurer.setBasePackage(basePackage);
return mapperScannerConfigurer;
}

}

事务

SpringManagedTransactionFactory

与 Spring Boot 整合

1
2
3
4
5
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>x.x.x</version>
</dependency>

http://www.mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/

插件

深入理解 Mybatis 插件开发

扩展

MyBatis Generator

https://github.com/mybatis/generator

https://github.com/zouzg/mybatis-generator-gui

MyBatis PageHelper

https://github.com/pagehelper/Mybatis-PageHelper

https://pagehelper.github.io/docs/howtouse/

Mybatis Plus

https://baomidou.com/

https://github.com/baomidou/mybatis-plus

https://github.com/yulichang/mybatis-plus-join

https://www.jianshu.com/p/ceb1df475021

IDEA Plugins

https://plugins.jetbrains.com/plugin/7293-mybatis-plugin

https://plugins.jetbrains.com/plugin/8321-free-mybatis-plugin

MyBatis 在 Java 业务开发领域可以说是首选的持久化框架,本文主要总结下 MyBatis 的核心配置和源码解析。

MyBatis 产品组成

MyBatis 常用的产品组成汇总如下:

Project Description Link
MyBatis SQL Mapper Framework for Java https://github.com/mybatis/mybatis-3
Integration with Spring https://github.com/mybatis/spring
Integration with Spring Boot https://github.com/mybatis/spring-boot-starter
Code generator for MyBatis and iBATIS https://github.com/mybatis/generator
SQL Generator for MyBatis and Spring JDBC Templates https://github.com/mybatis/mybatis-dynamic-sql
MyBatis Typehandlers JSR 310(日期时间API)
(merged into mybatis core since 3.4.5)
https://github.com/mybatis/typehandlers-jsr310

MyBatis 核心架构

MyBatis 语句执行时的层次结构:

涉及的主要 API 如下:

mybatis-api

Executor

mybatis_api_Executor

StatementHandler

mybatis_api_StatementHandler

ParameterHandler

mybatis_api_ParameterHandler

ResultSetHandler

mybatis_api_ResultSetHandler

TypeHandler

mybatis_api_TypeHandler

使用方式

基于 Statement ID 的传统方式

mybatis_statement_id

基于 Mapper 接口的推荐方式

mybatis_mapper

参考

http://www.mybatis.org/

https://github.com/mybatis

《MyBatis 从入门到精通》

《MyBatis 技术内幕》

本文梳理 Spring 事务管理的方方面面,总览如下:

Spring 框架的事务支持模型的优点

全面的事务支持是使用 Spring 框架的最有说服力的理由之一。Spring 框架为事务管理提供了一致的抽象层,并具有以下优势:

  • 跨不同事务 API 的一致编程模型,如 JTA (Java Transaction API)、JPA (Java Persistence API)、JDBC、Hibernate、MyBatis。
  • 支持声明式事务管理,可通过 XML 或注解进行配置。
  • 比复杂的事务 API(如 JTA)更简单的编程式事务管理 API
  • 与 Spring 框架的数据访问抽象层集成。

声明式事务管理

理解声明式事务实现

关于 Spring 框架的声明式事务支持,最重要的概念是掌握其通过 AOP 代理来启用此支持,并且事务 advice 由元数据(基于 XML 或注释 @Transactional)驱动。AOP 与事务元数据的组合产生 AOP 代理,该代理使用 TransactionInterceptor 搭配合适的 PlatformTransactionManager 实现来驱动围绕方法调用的事务代理。

TransactionInterceptor 的结构如下:

TransactionInterceptor

下图展示了调用事务代理方法的过程:

事务代理调用

基于 XML 方式配置事务管理

使用 <tx:advice/> 创建事务 advice,并创建切面通过 <aop:advisor/> 指定该事务 advice 须应用到哪些切点之上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- the transactional advice (what 'happens'; see the <aop:advisor/> bean below) -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<!-- the transactional semantics... -->
<tx:attributes>
<!-- all methods starting with 'get' are read-only -->
<tx:method name="get*" read-only="true"/>
<!-- other methods use the default transaction settings (see below) -->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>

<!-- ensure that the above transactional advice runs for any execution
of an operation defined by the FooService interface -->
<aop:config>
<aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
</aop:config>

......

事务配置可通过修改 <tx:method/> 的属性,详见脑图。

基于注解方式配置事务管理

除了使用基于 XML 的方式(<tx:advice/>)声明事务配置之外,您还可以使用基于注解的方式(@Transactional )。直接在 Java 源代码中声明事务语义会使声明更靠近受影响的代码,易于配置和修改。这样之所以不存在过度耦合的原因是因为,无论如何,用于事务处理的代码几乎总是以事务的方式进行部署。

您可以将 @Transactional 注解应用于:

  • 接口定义(interface)
  • 接口上的方法
  • 类定义(class)
  • 类上的公有方法(public method on class)

@Transactional 提供的配置属性如下:

@Transactional

开启事务支持

但是,仅仅使用 @Transactional 注解并不足以激活事务行为,还需要开启事务支持,可以使用以下方式:

  • <tx:annotation-driven/>
  • @EnableTransactionManagement

配置参数如下:

XML 属性 注解属性 默认 描述
transaction-manager N/A (see TransactionManagementConfigurer javadoc) transactionManager 要使用的事务管理器的名称。仅在事务管理器的名称不是 transactionManager 时才需要设置。
mode mode proxy 默认为代理模式(proxy),使用 Spring AOP 框架处理被 @Transactional 注解的 bean,仅适用于通过代理进入的方法调用。
相反,替代模式(aspectj)使用Spring AspectJ 事务切面织入到受影响的类,修改目标类的字节码以应用于任何类型的方法调用(支持任意访问修饰符、支持自调用)。AspectJ 织入需要在类路径中包含 spring-aspects.jar 以及开启类加载期织入(load-time weaving)或编译期织入(compile-time weaving)。(参阅 Spring 配置
proxy-target-class proxyTargetClass false 仅适用于 proxy 模式。控制为使用 @Transactional 注解的类所创建的事务代理类型。如果 proxy-target-class 属性设置为 true,则创建基于类的代理(CGLib Proxy)。如果为 false 或者省略该属性,则创建基于标准 JDK 接口的代理(JDK Proxy)。(参阅代理机制
order order Ordered.LOWEST_PRECEDENCE 参阅 Advice 排序

更多注意点,详见官方文档:

The default advice mode for processing @Transactional annotations is proxy, which allows for interception of calls through the proxy only. Local calls within the same class cannot get intercepted that way. For a more advanced mode of interception, consider switching to aspectj mode in combination with compile-time or load-time weaving.

In proxy mode (which is the default), only external method calls coming in through the proxy are intercepted. This means that self-invocation (in effect, a method within the target object calling another method of the target object) does not lead to an actual transaction at runtime even if the invoked method is marked with @Transactional.

The proxy-target-class attribute controls what type of transactional proxies are created for classes annotated with the @Transactional annotation. If proxy-target-class is set to true, class-based proxies are created. Ifproxy-target-class is false or if the attribute is omitted, standard JDK interface-based proxies are created. (See [aop-proxying] for a discussion of the different proxy types.)

The Spring team recommends that you annotate only concrete classes (and methods of concrete classes) with the @Transactional annotation, as opposed to annotating interfaces. You certainly can place the @Transactional annotation on an interface (or an interface method), but this works only as you would expect it to if you use interface-based proxies. The fact that Java annotations are not inherited from interfaces means that, if you use class-based proxies (proxy-target-class="true") or the weaving-based aspect (mode="aspectj"), the transaction settings are not recognized by the proxying and weaving infrastructure, and the object is not wrapped in a transactional proxy.

When you use proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. If you need to annotate non-public methods, consider using AspectJ.

@EnableTransactionManagement 注解主要用于导入 TransactionManagementConfigurationSelector,其判断 mode 属性:

  • modeAdviceMode.PROXY,返回配置 org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration,该 Java Config 用于配置以下 bean:
    • TransactionInterceptor 最关键的类
    • TransactionAttributeSource
    • BeanFactoryTransactionAttributeSourceAdvisor
  • modeAdviceMode.ASPECTJ,默认返回配置 org.springframework.transaction.aspectj.AspectJTransactionManagementConfiguration

事务的传播行为

其中事务的传播行为需要留意下,是 Spring 特有的概念,与数据库无关。它是为了解决业务层方法之间互相调用的事务问题而引入的。当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。有以下几种方式:

传播行为 描述
REQUIRED 支持当前事务,如果不存在,就新建一个。默认配置。
SUPPORTS 支持当前事务,如果不存在,就以非事务方式执行。
MANDATORY 支持当前事务,如果不存在,就抛出异常。
REQUIRES_NEW 如果当前存在事务,挂起当前事务,创建一个新事务。
NOT_SUPPORTED 以非事务方式执行,如果当前存在事务,则挂起当前事务。
NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与 REQUIRED 类似的操作。

强烈不建议使用非事务方式执行,因此上述标注删除线的传播行为不建议使用。

在 Spring 管理的事务中,请注意物理事务和逻辑事务之间的区别,以及传播行为应用于两者之上时的区别。

REQUIRED

REQUIRED

REQUIRED_NEW

REQUIRED_NEW

编程式事务管理

Spring 框架提供了两种编程式事务管理方法:

  • 直接使用 Spring 框架最底层的 PlatformTransactionManager 的实现类;
  • 更建议使用 Spring 框架封装过的 TransactionTemplate 事务模板类。

使用 PlatformTransactionManager

Spring 事务抽象的关键在于事务策略的概念。事务策略由org.springframework.transaction.PlatformTransactionManager接口定义 ,如下所示:

1
2
3
4
5
6
7
8
public interface PlatformTransactionManager {

TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;

void commit(TransactionStatus status) throws TransactionException;

void rollback(TransactionStatus status) throws TransactionException;
}

由于 PlatformTransactionManager 是一个接口,因此很容易按需 mock 或 stub。它与查找策略无关,例如JNDI。PlatformTransactionManager 的实现就像其它对象或 bean 一样在 Spring 框架的 IoC 容器中定义。仅此优势就让 Spring 框架事务成为一种有价值的抽象,即便是使用 JTA。与直接使用 JTA 相比,您可以更轻松地测试事务代码。

同时,为了与 Spring 的理念保持一致,PlatformTransactionManager 接口的所有方法抛出的 TransactionException 异常都是非受检的(即继承自 java.lang.RuntimeException 类)。事务基础设施故障几乎都是致命性的。只有极少数情况下,应用程序能够从事务故障中恢复过来。开发人员仍然可以选择 try catch TransactionException,但重点是开发人员不会被迫这样做。

getTransaction(..) 方法根据 TransactionDefinition 参数返回一个 TransactionStatus 对象 。TransactionStatus 表示一个新事务,但如果当前调用堆栈中存在匹配事务,则表示该已有事务,即 TransactionStatus 是与执行的线程相关联的。

TransactionDefinition 接口可以控制事务的传播行为、隔离级别、超时时间、只读状态,其结构如下:

TransactionDefinition

TransactionStatus 接口为事务代码提供了一种简单的方法来控制事务执行和查询事务状态,其结构如下:

TransactionDefinition

在 Spring 中无论选择使用声明式还是编程式事务管理,定义正确的 PlatformTransactionManager 实现都是绝对必要的。Spring 提供了下面几种实现:

实现类 工作环境
DataSourceTransactionManager spring-jdbc JDBC、Mybatis
HibernateTransactionManager spring-orm Hibernate
JpaTransactionManager spring-orm JPA
JtaTransactionManager spring-tx JTA

其继承关系如下:

PlatformTransactionManager 实现

以最常用的 DataSourceTransactionManager 为例,重点看下都提供了哪些方法:

DataSourceTransactionManager

使用 TransactionTemplate

和 Spring 框架的其它模板类一样,TransactionTemplate 也采用了回调方法来减少样板代码。相比起直接使用 PlatformTransactionManager 接口,TransactionTemplate 可以让开发人员无须重复编写获取与释放事务资源的代码,从而更聚焦于业务代码。

TransactionTemplate

你需要编写一个 TransactionCallback 实现(通常为匿名内部类),其中包含需要在事务上下文中执行的代码。然后传递给 TransactionTemplateexecute(..) 方法去执行:

TransactionCallback

由于 TransactionTemplate 继承自 DefaultTransactionDefinition,因此可以直接修改其属性进行事务配置(如传播行为、隔离级别、超时时间等)。TransactionTemplate 类的实例是线程安全的,实例并不维护任何会话状态,但是却会维护配置状态。因此,当多个类共享使用同一个 TransactionTemplate 类的实例时,如果其中一个需要使用不同的配置(例如不同的隔离级别),你需要创建两个不同的 TransactionTemplate 类的实例。

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@Slf4j
@Service
public class TestDbService {

@Autowired
private TestDAO testDAO;
@Autowired
private PlatformTransactionManager transactionManager;
@Autowired
private TransactionTemplate transactionTemplate;

/**
* 要使用底层 `PlatformTransactionManager` 接口直接管理事务,请先注入所需的实现类。
* 然后,通过 `TransactionDefinition` 和 `TransactionStatus` 对象启动、回滚和提交事务。
*/
public Integer save() {
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// explicitly setting the transaction name is something that can be done only programmatically
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

TransactionStatus status = transactionManager.getTransaction(def);
try {
Integer result = insert();
transactionManager.commit(status);
return result;
} catch (Exception e) {
log.error(e.getMessage(), e);
transactionManager.rollback(status);
return 0;
}
}

/**
* TransactionTemplate 采用了回调方法来减少样板代码。
*/
public Integer save1() {
return transactionTemplate.execute(status -> {
try {
return insert();
} catch (Exception e) {
log.error(e.getMessage(), e);
status.setRollbackOnly();
return 0;
}
});
}

/**
* 使用注解方式配置事务,是最简单最推荐的方式。事务的参数配置详见脑图。
*/
@Transactional(rollbackFor = Exception.class)
public Integer save3() {
return insert();
}

}

参考

https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Transactional.html

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Propagation.html

@Transactional 注解失效的 3 种原因及解决方案

如何将 @Transactional 事务注解运用到炉火纯青?

Spring JDBC 简介

Spring 的 JDBC 框架承担了资源管理和异常处理的工作,从而简化了底层 JDBC API 代码,让我们只需编写从数据库读写数据所需的代码。具体特性如下:

  • Spring 为读取和写入数据库的几乎所有错误提供了丰富的异常,且不与特定的持久化框架相关联(如下图)。异常都继承自的父类 DataAccessException,是一个非受检异常,无需捕获,因为 Spring 认为触发异常的很多问题是不能在 catch 代码块中修复,因此不强制开发人员编写 catch 代码块。这把是否要捕获异常的权利留给了开发人员。

    data-access-exceptions

  • Spring 将数据访问过程中固定的和可变的部分明确划分为两个不同的类:模板(template)回调(callback)。模板管理过程中固定的部分(如事务控制、资源管理、异常处理),而回调处理自定义的数据访问代码(如 SQL 语句、绑定参数、整理结果集)。针对不同的持久化平台,Spring 提供了多个可选的模板:

    data-access-templates

依赖安装

要在 Spring 中使用 JDBC,需要依赖 spring-jdbc。如果使用 Spring Boot 的话,可以直接导入起步依赖 spring-boot-starter-jdbc

1
2
3
4
5
6
7
8
9
10
11
<!-- Spring JDBC 起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<!-- MySQL JDBC 驱动程序 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

mvn dependency:tree 分析传递依赖如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[INFO] +- mysql:mysql-connector-java:jar:8.0.13:compile
[INFO] \- org.springframework.boot:spring-boot-starter-jdbc:jar:2.1.2.RELEASE:compile
[INFO] +- org.springframework.boot:spring-boot-starter:jar:2.1.2.RELEASE:compile
[INFO] | +- org.springframework.boot:spring-boot:jar:2.1.2.RELEASE:compile
[INFO] | | \- org.springframework:spring-context:jar:5.1.4.RELEASE:compile
[INFO] | | +- org.springframework:spring-aop:jar:5.1.4.RELEASE:compile
[INFO] | | \- org.springframework:spring-expression:jar:5.1.4.RELEASE:compile
[INFO] | +- org.springframework.boot:spring-boot-autoconfigure:jar:2.1.2.RELEASE:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-logging:jar:2.1.2.RELEASE:compile
[INFO] | | +- ch.qos.logback:logback-classic:jar:1.2.3:compile
[INFO] | | | \- ch.qos.logback:logback-core:jar:1.2.3:compile
[INFO] | | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.11.1:compile
[INFO] | | | \- org.apache.logging.log4j:log4j-api:jar:2.11.1:compile
[INFO] | | \- org.slf4j:jul-to-slf4j:jar:1.7.25:compile
[INFO] | +- javax.annotation:javax.annotation-api:jar:1.3.2:compile
[INFO] | +- org.springframework:spring-core:jar:5.1.4.RELEASE:compile
[INFO] | | \- org.springframework:spring-jcl:jar:5.1.4.RELEASE:compile
[INFO] | \- org.yaml:snakeyaml:jar:1.23:runtime
[INFO] +- com.zaxxer:HikariCP:jar:3.2.0:compile
[INFO] | \- org.slf4j:slf4j-api:jar:1.7.25:compile
[INFO] \- org.springframework:spring-jdbc:jar:5.1.4.RELEASE:compile
[INFO] +- org.springframework:spring-beans:jar:5.1.4.RELEASE:compile
[INFO] \- org.springframework:spring-tx:jar:5.1.4.RELEASE:compile

可见,spring-boot-starter-jdbc 引入了如下传递依赖:

  • spring-boot-starter
    • spring-boot-autoconfigure Spring Boot 自动配置类
  • spring-jdbc Spring JDBC 核心库
  • HikariCP,Spring Boot 2 的默认数据库连接池
  • ……

配置解析

spring-boot-autoconfigure 依赖内含几个关键的配置类,提供了如下外部配置:

  • org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,用于自动配置嵌入式数据源 或 连接池数据源

    1
    2
    3
    4
    5
    6
    spring:
    datasource:
    driver-class-name:
    url:
    username:
    password:
  • org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration,用于自动配置 JNDI 数据源

    1
    2
    3
    spring:
    datasource:
    jndi-name:
  • org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,用于自动配置分布式事务的数据源

    1
    2
    3
    4
    5
    spring:
    datasource:
    xa:
    data-source-class-name:
    properties:
  • org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,用于导入配置类:JdbcTemplateConfigurationNamedParameterJdbcTemplateConfiguration

    1
    2
    3
    4
    5
    6
    spring:
    jdbc:
    template:
    fetch-size:
    max-rows:
    query-timeout:
  • org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration 用于自动配置 DataSourceTransactionManager

  • org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration

    1
    2
    3
    4
    spring:
    transaction:
    defaultTimeout:
    rollbackOnCommitFailure:

使用 JDBC Template

配置数据源

为了让 JdbcTemplate 正常工作,只需要为其设置 DataSource 数据源即可。Spring Boot 下直接使用外部配置:

1
2
3
4
5
6
7
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource #使用 HikariCP,Spring Boot 2 的默认数据库连接池
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?useUnicode=true;characterEncoding=utf-8
username:
password:

如果未使用 Spring Boot,Java Config 如下:

1
2
3
4
5
6
7
8
9
@Bean
public DataSource dataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
ds.setJdbcUrl("jdbc:mysql://localhost:3306/test?useUnicode=true;characterEncoding=utf-8");
ds.setUsername("");
ds.setPassword("");
return ds;
}

API 介绍

JdbcOperations

  • org.springframework.jdbc.core.JdbcOperations 是 Spring 封装 JDBC 操作的核心接口,提供的方法如下,基于索引参数进行 SQL 参数绑定。实现类为 org.springframework.jdbc.core.JdbcTemplate

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <T> T execute(...)
    <T> List<T> query(String, RowMapper<T>, Object...) // 多列查询
    Map<String, Object> queryForMap(String, Object...) // 单行多列查询
    <T> T queryForObject(String, Class<T>, Object...) // 单行单列查询
    <T> T queryForObject(String, RowMapper<T>, Object...) // 单行多列查询
    <T> List<T> queryForList(String, Class<T>, Object...) // 多行单列查询
    List<Map<String, Object>> queryForList(String, Object...) // 多行多列查询
    SqlRowSet queryForRowSet(...)
    int update(...) // 执行单个增删改
    int[] batchUpdate(...) // 执行批量增删改
    Map<String, Object> call(...) // 执行存储过程和函数
    ......
  • org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations 接口支持将值以命名参数的形式绑定到 SQL,实现类为 org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate,其底层使用的仍然是 JdbcOperations,是一个二次封装的 API,推荐使用。

如果使用 Spring Boot 的话,可以直接导入起步依赖 spring-boot-starter-jdbc,会引入自动配置类 org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,用于导入配置类:JdbcTemplateConfigurationNamedParameterJdbcTemplateConfiguration,源码如下:

1
2
3
4
5
6
7
8
9
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ DataSource.class, JdbcTemplate.class })
@ConditionalOnSingleCandidate(DataSource.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@EnableConfigurationProperties(JdbcProperties.class)
@Import({ JdbcTemplateConfiguration.class, NamedParameterJdbcTemplateConfiguration.class })
public class JdbcTemplateAutoConfiguration {

}

只要满足几个条件,该自动配置类就会生效:

  • classpath 包含 DataSourceJdbcTemplate
  • DataSource bean 有且只有一个

JdbcOperations

依赖注入 JdbcTemplate 实现之后,使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
List<TestPO> testPOList = jdbcOperations.query(
"SELECT id, name, city FROM test WHERE name = ? AND city = ?",
(rs, rowNum) -> new TestPO(
rs.getLong("id"),
rs.getString("name"),
rs.getString("city")
),
"李四", "beijing"
);
log.info("Result is {}", testPOList); // Result is [TestPO(id=2, name=李四, city=beijing)]

TestPO testPO = jdbcOperations.queryForObject(
"SELECT id, name, city FROM test WHERE id = ?",
(rs, rowNum) -> new TestPO(
rs.getLong("id"),
rs.getString("name"),
rs.getString("city")
),
2
);
log.info("Result is {}", testPO); // Result is TestPO(id=2, name=李四, city=beijing)

String name = jdbcOperations.queryForObject("SELECT name FROM test WHERE id = ?", String.class, 2);
log.info("Result is {}", name); // Result is 李四

List<String> names = jdbcOperations.queryForList("SELECT name FROM test WHERE city = ?", String.class, "beijing");
log.info("Result is {}", names); // Result is [李四, 王五]

List<Map<String, Object>> testMapList = jdbcOperations.queryForList("SELECT id, name, city FROM test WHERE city = ?", "beijing");
log.info("Result is {}", testMapList); // Result is [{id=2, name=李四, city=beijing}, {id=3, name=王五, city=beijing}]

Map<String, Object> testMap = jdbcOperations.queryForMap("SELECT id, name, city FROM test WHERE id = ?", 2);
log.info("Result is {}", testMap); // Result is {id=2, name=李四, city=beijing}

NamedParameterJdbcTemplate

使用 JdbcOperations 需要特别注意索引参数的正确顺序,如果在修改 SQL 时忘记修改参数顺序,将导致查询出错。因此更建议使用命名参数,按照名字来绑定值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Map<String, Object> cityParamMap = new HashMap<>();
paramMap.put("city", "beijing");
paramMap.put("name", "李四");

List<TestPO> testPOList = namedParameterJdbcOperations.query(
"SELECT id, name, city FROM test WHERE name = :name AND city = :city",
paramMap,
(rs, rowNum) -> new TestPO(
rs.getLong("id"),
rs.getString("name"),
rs.getString("city")
)
);

log.info("Result is {}", testPOList); // Result is [TestPO(id=2, name=李四, city=beijing)]

由于 SQL 中的数据类型和 Java 编程语言中的数据类型并不相同,因此需要使用某种机制在使用 Java 类型的应用程序和使用 SQL 类型的数据库之间传输数据。

SQL 类型映射到 Java 类型

不同数据库产品支持的 SQL 类型之间存在显着差异。即使不同的数据库支持具有相同语义的 SQL 类型,它们也可能为这些类型提供了不同的名称。例如,大多数主要数据库都支持 large binary 这种 SQL 类型,但是:

  • MySQL 的命名为 BINARYVARBINARY(详见:The BINARY and VARBINARY Types
  • Oracle 的命名为 LONG RAW
  • Sybase 的命名为 IMAGE
  • Informix 的命名为 BYTE
  • DB2 的命名为 LONG VARCHAR FOR BIT DATA

幸运的是,JDBC 开发通常不需要关心目标数据库使用的实际 SQL 类型名称。大多数情况下,JDBC 开发将针对现有数据库表进行编程,并不需要关心用于创建这些表的确切 SQL 类型名称。

JDBC API 在 java.sql.Types 类中定义了一组通用 SQL 类型标识符,旨在表达最常用的 SQL 类型。在使用 JDBC API 进行编程时,程序员通常可以使用这些 JDBC 类型来引用通用 SQL 类型,而无需关心目标数据库使用的确切 SQL 类型名称。

数据访问 API

为了在数据库和 Java 应用程序之间传输数据,JDBC API 提供了三组方法:

  • PreparedStatement 类提供的用于将 Java 类型作为 SQL 语句参数发送的方法;
  • ResultSet 类提供的用于将 SELECT 检索结果转换为 Java 类型的方法;
  • CallableStatement类提供的用于将 OUT 参数转换为 Java 类型的方法。

静态数据访问

Java 程序从数据库中检索数据时,都必然会有某种形式的数据映射和数据转换。大多数情况下,JDBC 开发是知道目标数据库的 schema 的,例如表结构及其每列的数据类型。因此,JDBC 开发可以使用 ResultSetPreparedStatementCallableStatement 接口的强类型访问方法进行类型转换,如下:

  • PreparedStatement 接口:

    1
    2
    3
    4
    void setBoolean(int parameterIndex, boolean x) throws SQLException;
    void setByte(int parameterIndex, byte x) throws SQLException;
    void setInt(int parameterIndex, int x) throws SQLException;
    ...
  • ResultSet 接口:

    1
    2
    3
    4
    5
    6
    7
    boolean getBoolean(int columnIndex) throws SQLException;
    boolean getBoolean(String columnLabel) throws SQLException;
    byte getByte(int columnIndex) throws SQLException;
    byte getByte(String columnLabel) throws SQLException;
    int getInt(int columnIndex) throws SQLException;
    int getInt(String columnLabel) throws SQLException;
    ...

动态数据访问

在大多数情况下,用户都希望访问在编译期数据类型已知的结果或参数。但是某些情况下,应用程序在编译期无法获知它们访问的目标数据库的 schema。因此,除了静态的数据类型访问之外,JDBC 还提供了对动态的数据类型访问的支持。

访问在编译期数据类型未知的值,可以使用所有 Java 对象的共同父类 Object 类型:

  • PreparedStatement 接口:

    1
    2
    void setObject(int parameterIndex, Object x) throws SQLException;
    void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException;
  • ResultSet 接口:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Object getObject(int columnIndex) throws SQLException;
    Object getObject(String columnLabel) throws SQLException;

    Object getObject(int columnIndex, java.util.Map<String,Class<?>> map) throws SQLException;
    Object getObject(String columnLabel, java.util.Map<String,Class<?>> map) throws SQLException;

    // ... will convert from the SQL type of the column to the requested Java data type, if the conversion is supported. If the conversion is not supported or null is specified for the type, a `SQLException` is thrown.
    <T> T getObject(int columnIndex, Class<T> type) throws SQLException;
    <T> T getObject(String columnLabel, Class<T> type) throws SQLException;

    ⚠️ 特别注意:对于最后一组动态数据访问方法,参数二 type 的值要与 ResultSetMetaData.GetColumnClassName() 返回的类型相匹配,类型转换才能成功。否则抛出异常如下:

    java.sql.SQLException

    例如 MyBatis Plus com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler 就使用到了 ResultSet#getObject 方法,如果类型转换失败则报错如上。

    关于 MySQL 类型与 Java 类型的映射关系,参考:https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-type-conversions.html

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
try (Connection conn = DriverManager.getConnection(url)) {
try (PreparedStatement stmt = conn.prepareStatement("SELECT * FROM test WHERE name = ?;")) {
stmt.setObject(1, "李四", JDBCType.VARCHAR);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
Object id = rs.getObject("id");
String name = rs.getObject("name", String.class);
log.info("Result is {}, {}", id instanceof Long, name); // Result is true, 李四
}
}
}
}

boolean, char, byte, short, int, long, float, double 八种基本数据类型将返回其对应的包装类型,其它的则返回对应的类型。

设置 NULL

Some databases need to know the value’s type even if the value itself is NULL. For this reason, for maximum portability, it’s the JDBC specification itself that requires the java.sql.Types to be specified:

  • PreparedStatement 接口:

    1
    2
    3
    // Sets the designated parameter to SQL NULL.
    void setNull(int parameterIndex, int sqlType) throws SQLException;
    void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException;

参考:Is JdbcType necessary in a MyBatis mapper?

参考

https://docs.oracle.com/javase/6/docs/technotes/guides/jdbc/getstart/mapping.html

java.sql.Types

总览

首先,来总览下 JDBC API:

JDBC

JDBC API 规范

JDBC API 作为 Java SE™(Java 标准版)的一部分,由以下部分组成:

  • JDBC 核心 API —— java.sql package。

  • JDBC 可选 API —— javax.sql package,是 Java EE™(Java 企业版)的重要组成部分。

其中,java.sql package 包含下列 API:

  • 通过 java.sql.DriverManager 与数据库建立连接

    • java.sql.DriverManager 类 - 用于与驱动程序建立连接
    • java.sql.SQLPermission
    • java.sql.Driver 接口 - 提供用于注册和连接驱动程序的 API。
    • java.sql.DriverPropertyInfo 类 - 提供 JDBC 驱动程序的属性。
  • 发送 SQL 语句到数据库

    • java.sql.Connection 接口 - 提供创建语句、管理连接及其属性的方法
    • java.sql.Statement 接口 - 用于发送基本的 SQL 语句
    • java.sql.PreparedStatement 接口 - 用于发送预编译语句或基本 SQL 语句(继承自Statement
    • java.sql.CallableStatement 接口 - 用于调用数据库存储过程(继承自PreparedStatement
    • java.sql.Savepoint 接口 - 在事务中提供保存点
  • 检索和更新查询结果

    • java.sql.ResultSet 接口
  • 标准映射(SQL 数据类型到 Java 类或接口)

  • 自定义映射(SQL user-defined type (UDT) 到 Java 类)

  • 元数据

    • java.sql.DatabaseMetaData 接口 - 提供有关数据库的信息
    • java.sql.ResultSetMetaData 接口 - 提供有关 ResultSet 对象的列信息
    • java.sql.ParameterMetaData 接口 - 提供有关 PreparedStatement 命令的参数信息
  • 异常

    • java.sql.SQLException 类 - 被大多数方法抛出,当数据访问出现问题或出于其它原因
    • java.sql.SQLWarning 类 - 抛出表示警告
    • java.sql.DataTruncation 类 - 抛出表示数据可能已被截断
    • java.sql.BatchUpdateException 类 - 抛出表示批量更新中的部分命令未执行成功

下面重点看下常用的接口和类。

DriverManager 类

java.sql.DriverManager 类充当用户和驱动程序之间的接口。它跟踪可用的驱动程序并处理数据库与相应驱动程序之间的连接。DriverManager 类维护了一个通过调用 DriverManager.registerDriver() 方法来注册自己的 java.sql.Driver 类列表。

常用方法:

1
2
3
4
static void registerDriver(Driver driver) // 用于通过 `DriverManager` 注册给定的驱动程序。
static void deregisterDriver(Driver driver) // 用于从 `DriverManager` 取消注册给定的驱动程序(从列表中删除驱动程序)。
static Connection getConnection(String url) // 用于与指定的 URL 建立连接。
static Connection getConnection(String url, String userName, String password) // 用于与指定的 URL 建立连接,通过用户名和密码。

关于 Driver 驱动程序注册,详见《注册驱动程序》。

Connection 接口

java.sql.Connection 接口表示 Java 应用程序和数据库之间的会话(Session),它提供了许多事务管理方法如:

1
2
3
4
5
6
7
8
9
10
11
12
void setAutoCommit(boolean status)  // 修改当前 `Connection` 对象的事务自动提交模式。默认为 `true`。
void setReadOnly(boolean readOnly) // 修改当前 `Connection` 对象的只读状态以提示驱动程序开启数据库优化。
void setTransactionIsolation(int level) // 修改当前 `Connection` 对象的事务隔离级别。
void commit() // 保存自上次提交/回滚以来所做的所有更改。
void rollback() // 丢弃自上次提交/回滚以来所做的所有更改。

// 在当前事务中设置或移除保存点。
Savepoint setSavepoint()
Savepoint setSavepoint(String name)
void releaseSavepoint(Savepoint savepoint)

void close() // 关闭连接并立即释放 JDBC 资源。

Connection 接口同时也是一个工厂类,用于获取 StatementPreparedStatementDatabaseMetaData 对象:

1
2
3
4
Statement createStatement(...)  // 创建一个可用于执行 SQL 查询或更新的语句对象。
PreparedStatement prepareStatement(...) // 创建一个可用于执行 SQL 参数化查询或更新的语句对象。
CallableStatement prepareCall(...) // 用于调用存储过程和函数。
DatabaseMetaData getMetaData() // 用于获取数据库的元数据,例如数据库产品名称,数据库产品版本,驱动程序名称,表总数名称,总视图名称等。

Statement 接口

java.sql.Statement 接口提供用于执行数据库查询与更新的方法。Statement 接口是 ResultSet 的工厂,即它提供工厂方法来获取 ResultSet 的对象。

1
2
3
ResultSet executeQuery(String sql)  // 用于执行 `SELECT` 查询并返回 `ResultSet` 的对象。
int executeUpdate(String sql) // 用于执行指定的更新,如 `create`,`drop`,`insert`,`update`,`delete` 等。
boolean execute(String sql) // 用于执行可能返回多种结果的查询。

批处理

除了通过上述方法来执行单个查询或更新,还可以通过下列方法执行批量命令:

1
2
3
void addBatch(String sql)
void clearBatch()
int[] executeBatch()

使用批量命令前,记得先使用 setAutoCommit() 将事务的自动提交模式设置为 false

批处理允许您将相关的 SQL 语句分组到批处理中,并通过一次调用数据库来提交它们。当您一次性向数据库发送多个 SQL 语句时,可以减少通信开销,从而提高性能。参考:JDBC - Batch Processing

PreparedStatement 接口

java.sql.PreparedStatement 接口是 java.sql.Statement 的子接口。它用于执行参数化查询(parameterized query),例如:

1
PreparedStatement ps = connection.prepareStatement("insert into emp values(?, ?, ?)");

为什么要使用 PreparedStatement

  • 提升性能:应用程序的性能会更快,因为 SQL 语句只会编译一次。
  • 提升安全:预防 SQL 注入

创建预编译的参数化查询语句后,需要通过 setXxx 方法设置对应参数。参数设置完毕后,就可以通过下列方法执行 SQL 语句:

1
2
3
ResultSet executeQuery()  // 用于执行 `SELECT` 查询并返回 `ResultSet` 的对象。
int executeUpdate() // 用于执行指定的更新,如 `create`,`drop`,`insert`,`update`,`delete` 等。
boolean execute() // 用于执行可能返回多种结果的查询。

主键回写

java.sql.Connection 创建 java.sql.PreparedStatement 时,允许通过 autoGeneratedKeys 指定是否返回自增主键:

1
PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException;

autoGeneratedKeys - a flag indicating whether auto-generated keys should be returned; one of Statement.RETURN_GENERATED_KEYS or Statement.NO_GENERATED_KEYS

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* The constant indicating that generated keys should be made available for retrieval.
*
* @since 1.4
*/
int RETURN_GENERATED_KEYS = 1;

/**
* The constant indicating that generated keys should not be made available for retrieval.
*
* @since 1.4
*/
int NO_GENERATED_KEYS = 2;

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 构造 PreparedStatement 时,多了第二个参数,指定了需要主键回写
PreparedStatement ps = connection.prepareStatement("insert into emp values(?, ?, ?)",
Statement.RETURN_GENERATED_KEYS);
ps.setString(name);
ps.setLong(id);
ps.setInt(salary);
ps.executeUpdate();

// 调用 getGeneratedKeys ,然后又会获取到一个 ResultSet 对象,从这个游标中就可以获取到刚刚插入数据的 id
ResultSet rs = ps.getGeneratedKeys();
int id = 0;
if (rs.next()) {
id = rs.getInt(1);
}
return id;

批处理

PreparedStatement 还提供了批处理方式,减少网络请求,提升性能,API 如下:

1
2
3
4
5
6
7
8
9
10
/**
* Adds a set of parameters to this <code>PreparedStatement</code>
* object's batch of commands.
*
* @exception SQLException if a database access error occurs or
* this method is called on a closed <code>PreparedStatement</code>
* @see Statement#addBatch
* @since 1.2
*/
void addBatch() throws SQLException;

未使用批处理方法:

1
2
3
4
5
6
7
8
9
PreparedStatement ps = conn.prepareStatement("INSERT into employees values (?, ?, ?)");

for (n = 0; n < 100; n++) {
ps.setString(name[n]);
ps.setLong(id[n]);
ps.setInt(salary[n]);
// 多次执行PreparedStatement,多次数据库请求(网络请求)
ps.executeUpdate();
}

使用批处理方法,一次性执行多条 SQL:

1
2
3
4
5
6
7
8
9
10
11
PreparedStatement ps = conn.prepareStatement("INSERT into employees values (?, ?, ?)");

for (n = 0; n < 100; n++) {
ps.setString(name[n]);
ps.setLong(id[n]);
ps.setInt(salary[n]);
// 添加批次
ps.addBatch();
}
// 调用父接口 Statement#executeBatch() 执行批次
ps.executeBatch();

ResultSet 接口

java.sql.ResultSet 对象维护了一个指向 table 行的游标。游标初始值指向第 0 行。默认情况下,ResultSet 对象只能向前移动,并且不可更新。可以通过在 createStatement(int, int) 方法中传递指定参数修改该默认行为。

可以通过以下方法操作游标:

1
2
3
4
5
6
boolean next()  // 将游标移动到当前位置的下一行。
boolean previous() // 将游标移动到当前位置之前的一行。
boolean first() // 将游标移动到结果集的第一行。
boolean last() // 将游标移动到结果集的最后一行。
boolean absolute(int row) // 将游标移动到结果集的指定行号。
boolean relative(int row) // 将游标移动到结果集的相对行号,它可以是正数或负数。

将游标移动到指定行之后,可以通过 getXxx 方法获取当前行的指定列的数据。

此外,还可以直接获取 table 的元数据,例如列的总数,列名,列类型等:

1
ResultSetMetaData getMetaData()

ResultSetMetaData 接口

java.sql.ResultSetMetaData 用于获取 table 的元数据,例如列的总数,列名,列类型等。

DatabaseMetaData 接口

java.sql.DatabaseMetaData 用于获取数据库的元数据,例如数据库产品名称,数据库产品版本,驱动程序名称,表总数名称,总视图名称等。

RowSet 接口

javax.sql.RowSet 继承自 java.sql.ResultSet,是其包装器类。它包含类似 ResultSet 的表格数据,但使用起来非常简单灵活。其实现类如下:

RowSet

下面是一个不含事件处理代码的 JdbcRowSet 的简单示例:

1
2
3
4
5
6
7
8
9
10
try (JdbcRowSet rowSet = RowSetProvider.newFactory().createJdbcRowSet()) {
rowSet.setUrl(url);
rowSet.setCommand("SELECT * FROM test");
rowSet.execute();
while (rowSet.next()) {
int id = rowSet.getInt("id");
String name = rowSet.getString("name");
log.info("Result is {} {}", id, name);
}
}

对比下面传统的 JDBC API,代码更加直观,需要直接管理的资源也更少:

1
2
3
4
5
6
7
8
9
10
11
try (Connection conn = DriverManager.getConnection(url)) {
try (Statement stmt = conn.createStatement()) {
try (ResultSet rs = stmt.executeQuery("SELECT * FROM test")) {
while (rs.next()) {
int id = rs.getInt("id");
String name = rs.getString("name");
log.info("Result is {}, {}", id, name);
}
}
}
}

要使用 JdbcRowSet 执行事件处理,需要在 JdbcRowSetaddRowSetListener 方法中添加 RowSetListener 的实例。RowSetListener 接口提供了必须实现的三个方法,如下:

1
2
3
void cursorMoved(RowSetEvent event);
void rowChanged(RowSetEvent event);
void rowSetChanged(RowSetEvent event);

DataSource 接口

JDBC API 示例

JDBC API 的使用步骤如下:

JDBC 使用步骤

其中:

  1. 步骤一:JDBC API 从 4.0 开始利用 Java SPI 机制自动加载驱动程序,可以省略该步骤。
  2. 步骤二、三:如果使用如 Spring JdbcTempate、MyBatis 等框架,可以省略该步骤。
  3. 步骤五:使用 try-with-resources 语句,可以省略该步骤。

下面来两个示例:

存储图片

下例通过 PreparedStatement 接口的 setBinaryStream() 方法将图片(二进制信息)存储到数据库中。为了将图片存储到数据库中,需要在表中使用 BLOB(Binary Large Object)数据类型。

1
2
3
4
5
6
7
8
try (Connection conn = DriverManager.getConnection(url)) {
try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test(title, photo) VALUES(?, ?)")) {
FileInputStream fileInputStream = new FileInputStream("F:\\test.jpg");
stmt.setString(1, "pic1");
stmt.setBinaryStream(2, fileInputStream);
assertTrue(1 == stmt.executeUpdate());
}
}

注意:这只是一个例子,生产环境中是不会将这类二进制信息存储到数据库中的,而是存储到专门的文件系统,以提升性能,并节省宝贵的数据库资源 :)

检索图片

Using try-with-resources Statements to Automatically Close JDBC Resources:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
try (Connection conn = DriverManager.getConnection(url)) {
try (PreparedStatement stmt = conn.prepareStatement("SELECT title, photo FROM test")) {
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
String title = rs.getString(1);
log.info("title is {}", title);

Blob photo = rs.getBlob(2);
byte[] bytes = photo.getBytes(1, (int) photo.length());
String fileName = String.format("F:\\%s.png", title);
try (FileOutputStream fileOutputStream = new FileOutputStream(fileName)) {
fileOutputStream.write(bytes);
}
}
}
}
}

JDBC 4.1 (Java SE 7) introduces the ability to use a try-with-resources statement to automatically close java.sql.Connection, java.sql.Statement, and java.sql.ResultSet objects, regardless of whether a SQLException or any other exception has been thrown. See The try-with-resources Statement for more information.

参考

Getting Started with the JDBC API

https://docs.oracle.com/javase/8/docs/technotes/guides/jdbc/jdbc_41.html

https://docs.oracle.com/javase/8/docs/technotes/guides/jdbc/jdbc_42.html

https://docs.oracle.com/javase/9/docs/api/java/sql/package-summary.html

https://www.javatpoint.com/java-jdbc

https://www.tutorialspoint.com/jdbc/index.htm

https://www.tutorialspoint.com/dbutils/index.htm

总览

JDBC Driver

什么是 JDBC?

JDBC 表示 Java Database Connectivity,是 Java SE(Java 标准版)的一部分。JDBC API 用于在 Java 应用程序中执行以下活动:

  1. 连接到数据库
  2. 执行查询并将语句更新到数据库
  3. 检索从数据库收到的结果

JDBC API

为什么要使用 JDBC?

在 JDBC 之前,ODBC API 是用于连接和执行命令的数据库 API 标准。但是,ODBC API 是使用 C 语言编写的驱动程序,依赖于平台。这就是为什么 Java 定义了自己的 JDBC API,它使用的 JDBC 驱动程序,是用 Java 语言编写的,具有与平台无关的特性,支持跨平台部署,性能也较好。

什么是 JDBC 驱动程序?

由于 JDBC API 只是一套接口规范,因此要使用 JDBC API 操作数据库,首先需要选择合适的驱动程序:

驱动程序四种类型

有四种类型的 JDBC 驱动程序:

  1. JDBC-ODBC bridge driver (In Java 8, the JDBC-ODBC Bridge has been removed.)
  2. Native-API driver (partially java driver)
  3. Network-Protocol driver (Middleware driver, fully java driver)
  4. **Database-Protocol driver (Thin driver, fully java driver)**,目前最常用的驱动类型,日常开发中使用的驱动 jar 包基本都属于这种类型,通常由数据库厂商直接提供,例如 mysql-connector-java。驱动程序把 JDBC 调用直接转换为数据库特定的网络协议,因此性能更好。驱动程序纯 Java 实现,支持跨平台部署。

我们知道,ODBC几乎能在所有平台上连接几乎所有的数据库。为什么 Java 不使用 ODBC?

答案是:Java 可以使用 ODBC,但最好是以JDBC-ODBC桥的形式使用(Java连接总体分为Java直连和JDBC-ODBC桥两种形式)。

那为什么还需要 JDBC?

因为ODBC 不适合直接在 Java 中使用,因为它使用 C 语言接口。从Java 调用本地 C代码在安全性、实现、坚固性和程序的自动移植性方面都有许多缺点。从 ODBC C API 到 Java API 的字面翻译是不可取的。例如,Java 没有指针,而 ODBC 却对指针用得很广泛(包括很容易出错的指针”void *”)。

另外,ODBC 比较复杂,而JDBC 尽量保证简单功能的简便性,同时在必要时允许使用高级功能。如果使用ODBC,就必须手动地将 ODBC 驱动程序管理器和驱动程序安装在每台客户机上。如果完全用 Java 编写 JDBC 驱动程序则 JDBC代码在所有 Java 平台上(从网络计算机到大型机)都可以自 动安装、移植并保证安全性。

总之,JDBC 在很大程度上是借鉴了ODBC的,从他的基础上发展而来。JDBC 保留了 ODBC 的基本设计特征,因此,熟悉 ODBC 的程序员将发现 JDBC 很容易使用。它们之间最大的区别在于:JDBC 以 Java 风格与优点为基础并进行优化,因此更加易于使用。

各类型的优缺点详见:

https://en.wikipedia.org/wiki/JDBC_driver

https://www.javatpoint.com/jdbc-driver

https://blog.csdn.net/autfish/article/details/52170053

驱动程序厂商实现

如果选定使用推荐的第四种驱动程序类型,接下来需要下载对应厂商的驱动程序,目前提供这些支持列表

MySQL Connector/J

例如最常用的 MySQL 数据库,提供了 MySQL Connector/J(即 mysql-connector-java)。Maven 依赖配置如下:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>x.x.x</version>
</dependency>

关于 MySQL 驱动程序的更多信息,详见:https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-overview.html

  • 驱动程序/数据源类名
  • 连接 URL 语法
  • 配置属性
  • JDBC API 实现说明
  • Java,JDBC 和 MySQL 类型
  • 使用字符集和 Unicode
  • 各种连接方式(如 SSL 安全连接)
  • MySQL 错误码与 JDBC SQLState 代码的映射关系
  • ……

如何使用 JDBC?

配置 JDBC URL

JDBC URL 提供了一种标识数据源的方法,以便相应的驱动程序识别它并与之建立连接。

因此,首先需要先配置好 JDBC URL,以便驱动程序注册完毕之后通过 JDBC URL 与数据源建立连接。

JDBC URL 的标准语法如下:

1
protocol//[hosts][/database][?properties]

详见:https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-jdbc-url-format.html

例如:jdbc:mysql://localhost:3306/test?useUnicode=true;characterEncoding=utf-8

常用协议

常见的 JDBC URL 协议及对应 Driver Class 如下:

配置属性

MySQL Connector/J:

https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-configuration-properties.html

https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-configuration-properties.html

useSSL

javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Caught while disconnecting...

** BEGIN NESTED EXCEPTION **

javax.net.ssl.SSLException
MESSAGE: closing inbound before receiving peer's close_notify

STACKTRACE:

javax.net.ssl.SSLException: closing inbound before receiving peer's close_notify
at sun.security.ssl.Alert.createSSLException(Alert.java:133)
at sun.security.ssl.Alert.createSSLException(Alert.java:117)
at sun.security.ssl.TransportContext.fatal(TransportContext.java:340)
at sun.security.ssl.TransportContext.fatal(TransportContext.java:296)
at sun.security.ssl.TransportContext.fatal(TransportContext.java:287)
at sun.security.ssl.SSLSocketImpl.shutdownInput(SSLSocketImpl.java:737)
at sun.security.ssl.SSLSocketImpl.shutdownInput(SSLSocketImpl.java:716)
at com.mysql.cj.protocol.a.NativeProtocol.quit(NativeProtocol.java:1319)
at com.mysql.cj.NativeSession.quit(NativeSession.java:182)
at com.mysql.cj.jdbc.ConnectionImpl.realClose(ConnectionImpl.java:1750)
at com.mysql.cj.jdbc.ConnectionImpl.close(ConnectionImpl.java:720)
at com.zaxxer.hikari.pool.PoolBase.quietlyCloseConnection(PoolBase.java:135)
at com.zaxxer.hikari.pool.HikariPool.lambda$closeConnection$1(HikariPool.java:441)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)


** END NESTED EXCEPTION **

解决方法一:useSSL=false

解决方法二:注释掉 java.security 文件中的 jdk.tls.disabledAlgorithms 配置:

1
2
3
4
5
$ vim ~/.sdkman/candidates/java/8.0.362-zulu/zulu-8.jdk/Contents/Home/jre/lib/security/java.security

# jdk.tls.disabledAlgorithms=SSLv3, TLSv1, TLSv1.1, RC4, DES, MD5withRSA, \
# DH keySize < 1024, EC keySize < 224, 3DES_EDE_CBC, anon, NULL, \
# include jdk.disabled.namedCurves

connectionTimeZone

https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-datetime-types-processing.html#cj-conn-prop_connectionTimeZone

Setting the MySQL JDBC Timezone Using Spring Boot Configuration

useServerPrepStmts

https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-prepared-statements.html#cj-conn-prop_useServerPrepStmts

Use server-side prepared statements if the server supports them?

MySQL 是否默认开启预编译,与 MySQL Server 的版本无关,而与 MySQL Connector/J(驱动程序)的版本有关,Connector/J 5.0.5 之前的版本默认开启预编译。Connector/J 5.0.5 及以后的版本默认不开启预编译,想启用 MySQL 预编译,就必须设置 useServerPrepStmts=true

参考:《JDBC 的 PreparedStatement 预编译详解

allowMultiQueries

https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-security.html#cj-conn-prop_allowMultiQueries

Allow the use of ; to delimit multiple queries during one statement (true/false). Default is false, and it does not affect the addBatch() and executeBatch() methods, which rely on rewriteBatchedStatements instead.

基于安全考虑,默认情况下,MySQL Connector/J 禁用 ; 拼接 SQL。

如果想通过 MyBatis foreach 使用 ; 拼接 UPDATE/DELETE 语句进行批量提交(但强烈不建议),需要设置 useServerPrepStmts=true

rewriteBatchedStatements

https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-performance-extensions.html#cj-conn-prop_rewriteBatchedStatements

Should the driver use multi-queries (regardless of the setting of allowMultiQueries) as well as rewriting of prepared statements for INSERT into multi-value inserts when executeBatch() is called?

Notice that this has the potential for SQL injection if using plain java.sql.Statement and your code doesn’t sanitize input correctly.

Notice that for prepared statements, server-side prepared statements can not currently take advantage of this rewrite option, and that if you don’t specify stream lengths when using PreparedStatement.set*Stream(), the driver won’t be able to determine the optimum number of parameters per batch and you might receive an error from the driver that the resultant packet is too large.

Statement.getGeneratedKeys() for these rewritten statements only works when the entire batch includes INSERT statements.

Please be aware using rewriteBatchedStatements=true with INSERT .. ON DUPLICATE KEY UPDATE that for rewritten statement server returns only one value as sum of all affected (or found) rows in batch and it isn’t possible to map it correctly to initial statements; in this case driver returns 0 as a result of each batch statement if total count was 0, and the Statement.SUCCESS_NO_INFO as a result of each batch statement if total count was > 0.

在 MySQL 中,rewriteBatchedStatements 的默认值取决于 MySQL 版本和 JDBC 驱动程序的配置。

  • 在 MySQL 8.0 版本之前,rewriteBatchedStatements 的默认值是 false,这意味着批处理语句不会被重写优化。
  • 从 MySQL 8.0 版本开始,rewriteBatchedStatements 的默认值被更改为 true,以提高默认情况下的性能。

要减少 JDBC 的网络调用次数改善性能,你可以设置 rewriteBatchedStatements=true并使用 PreparedStatementaddBatch() 方法并执行 executeBatch() 批量发送多个操作给数据库

根据执行的 DML 语句类型,使用不同的处理方法:

  • 如果是 INSERT 语句,会整合成形如:insert into t values (xx),(yy),(zz),...
  • 如果是 UPDATE/DELETE 语句,会整合成形如:update t set … where id = 1; update t set … where id = 2; update t set … where id = 3; ...

然后按 maxAllowedPacket 分批拼接 SQL 语句,然后按批次提交 MySQL。

参考:《MySQL 批量操作

maxAllowedPacket

Maximum allowed packet size to send to server. If not set, the value of system variable max_allowed_packet will be used to initialize this upon connecting. This value will not take effect if set larger than the value of max_allowed_packet. Also, due to an internal dependency with the property “blobSendChunkSize“, this setting has a minimum value of “8203” if “useServerPrepStmts“ is set to “true“.

参考:《MySQL 批量插入数据,一次插入多少行数据效率最高?

注册驱动程序

有几种方式可以注册驱动程序,如下:

手工注册

1
2
3
Class.forName("com.mysql.jdbc.Driver");  // 方式一,底层实现其实就是方式二
DriverManager.registerDriver(new com.mysql.jdbc.Driver()); // 方式二
System.setProperty("jdbc.drivers", "com.mysql.jdbc.Driver"); // 方式三

自动注册

从 JDBC API 4.0 开始,java.sql.DriverManager 类得到了增强,利用 Java SPI 机制从厂商驱动程序的 META-INF/services/java.sql.Driver 文件中自动加载 java.sql.Driver 实现类。 因此应用程序无需再显式调用 Class.forNameDriverManager.registerDriver 方法来注册或加载驱动程序。java.sql.DriverManager 源码分析如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public class DriverManager {

/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}

private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()

AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();

/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});

println("DriverManager.initialize: jdbc.drivers = " + drivers);

if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}

}

从上面源码中可见,当类加载器载入 java.sql.DriverManager 类时,会执行其静态代码块,从而执行 loadInitialDrivers() 方法。该方法实现中通过 Java SPI ServiceLoader 查找 classpath 下所有 jar 包内的 META-INF/services 目录,找到 java.sql.Driver 文件,加载其中定义的实现类并通过反射创建实例。以 mysql-connector-java 8.x 为例,该类定义就是 com.mysql.cj.jdbc.Driver,此时由于该 Driver 类内含静态代码块,会用 new 关键字创建自身实例并反向注册到 DriverManager,从而达到自动注册驱动程序的效果:

1
2
3
4
5
6
7
8
9
10
11
12
public class Driver extends NonRegisteringDriver implements java.sql.Driver {

// Register ourselves with the DriverManager
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}

}

常见问题

如果有多个不同的驱动程序都被注册,调用 DriverManager.getConnection 方法通过 JDBC URL 获取数据源连接时,会使用第一个可用的驱动程序来创建连接。源码分析如下:

1
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?useUnicode=true;characterEncoding=utf-8")

DriverManager 会遍历已注册的驱动程序,尝试获取连接,关键代码:Connection con = aDriver.driver.connect(url, info);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// DriverManager 源码

private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {

...

for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}

} else {
println(" skipping: " + aDriver.getClass().getName());
}

}

...

}

MySQL 驱动程序实现类会判断该 JDBC URL 是否支持:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// com.mysql.cj.jdbc.NonRegisteringDriver 源码,实现 java.sql.Driver 接口

@Override
public java.sql.Connection connect(String url, Properties info) throws SQLException {

try {
if (!ConnectionUrl.acceptsUrl(url)) {
/*
* According to JDBC spec:
* The driver should return "null" if it realizes it is the wrong kind of driver to connect to the given URL. This will be common, as when the
* JDBC driver manager is asked to connect to a given URL it passes the URL to each loaded driver in turn.
*/
return null;
}

ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
switch (conStr.getType()) {
case SINGLE_CONNECTION:
return com.mysql.cj.jdbc.ConnectionImpl.getInstance(conStr.getMainHost());

case LOADBALANCE_CONNECTION:
return LoadBalancedConnectionProxy.createProxyInstance((LoadbalanceConnectionUrl) conStr);

case FAILOVER_CONNECTION:
return FailoverConnectionProxy.createProxyInstance(conStr);

case REPLICATION_CONNECTION:
return ReplicationConnectionProxy.createProxyInstance((ReplicationConnectionUrl) conStr);

default:
return null;
}

} catch (UnsupportedConnectionStringException e) {
// when Connector/J can't handle this connection string the Driver must return null
return null;

} catch (CJException ex) {
throw ExceptionFactory.createException(UnableToConnectException.class,
Messages.getString("NonRegisteringDriver.17", new Object[] { ex.toString() }), ex);
}
}

scheme 支持列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// com.mysql.cj.conf.Type 枚举源码

/**
* The database URL type which is determined by the scheme section of the connection string.
*/
public enum Type {
SINGLE_CONNECTION("jdbc:mysql:", HostsCardinality.SINGLE), //
FAILOVER_CONNECTION("jdbc:mysql:", HostsCardinality.MULTIPLE), //
LOADBALANCE_CONNECTION("jdbc:mysql:loadbalance:", HostsCardinality.ONE_OR_MORE), //
REPLICATION_CONNECTION("jdbc:mysql:replication:", HostsCardinality.ONE_OR_MORE), //
XDEVAPI_SESSION("mysqlx:", HostsCardinality.ONE_OR_MORE);

private String scheme;
private HostsCardinality cardinality;

...
}

如果该 JDBC URL 没有对应可用的驱动程序,程序将抛出异常:java.sql.SQLException: No suitable driver found for jdbc:...

参考

https://en.wikipedia.org/wiki/JDBC_driver

https://www.javatpoint.com/jdbc-driver

https://blog.csdn.net/autfish/article/details/52170053

https://dev.mysql.com/downloads/connector/j/

https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-overview.html

MySQL 驱动 Bug 引发的事务不回滚问题

基础语法

SELECT 基础语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
SELECT
[ALL | DISTINCT]
select_expr [, select_expr ...]
[
FROM table_references
[WHERE where_condition]
[GROUP BY {col_name | expr | position}
[ASC | DESC], ... [WITH ROLLUP]]
[HAVING where_condition]
[ORDER BY {col_name | expr | position}
[ASC | DESC], ...]
[LIMIT {[offset,] row_count | row_count OFFSET offset}]
[
INTO OUTFILE 'file_name'
[CHARACTER SET charset_name]
export_options
| INTO DUMPFILE 'file_name'
| INTO var_name [, var_name]
]
[FOR UPDATE | LOCK IN SHARE MODE]
]

SELECT 语句可用于检索单个、多个、所有列(星号 * 通配符)。每个 select_expr 表示您想要检索的列。必须至少有一个 select_expr

去重

修饰符 ALLDISTINCT 用于指定重复行是否应该返回(是否去重),作用于所有的列,而不仅仅是跟在其后的那一列。例如 SELECT DISTINCT vend_id, prod_price ,除非指定的两列完全相同,否则所有的行都会被检索出来。

修饰符 描述
ALL 默认值,指定应返回所有匹配的行,包括重复项。
DISTINCT 指定从结果集中删除重复的行。

检索表

table_references 指示检索表,其语法可参考 JOIN语法SELECT 也可以不使用 FROM 子句而用来检索计算出的行:

1
2
SELECT 1 + 1;
-> 2

也可以使用 FROM DUAL 指定虚拟表,MySQL 会忽略这个子句。

1
2
SELECT 1 + 1 FROM DUAL;
-> 2

过滤数据

WHERE 子句用于过滤数据,where_condition 可以使用以下表达式语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
expr:
expr OR expr
| expr || expr
| expr XOR expr
| expr AND expr
| expr && expr
| NOT expr
| ! expr
| boolean_primary IS [NOT] {TRUE | FALSE | UNKNOWN}
| boolean_primary

boolean_primary:
boolean_primary IS [NOT] NULL
| boolean_primary <=> predicate
| boolean_primary comparison_operator predicate
| boolean_primary comparison_operator {ALL | ANY} (subquery)
| predicate

comparison_operator: = | >= | > | <= | < | <> | !=

predicate:
bit_expr [NOT] IN (subquery)
| bit_expr [NOT] IN (expr [, expr] ...)
| bit_expr [NOT] BETWEEN bit_expr AND predicate
| bit_expr SOUNDS LIKE bit_expr
| bit_expr [NOT] LIKE simple_expr [ESCAPE simple_expr]
| bit_expr [NOT] REGEXP bit_expr
| bit_expr

bit_expr:
...

simple_expr:
...

LIKE 可使用以下通配符:

通配符 描述
% 匹配多个字符
_ 匹配单个字符
[] 匹配一个字符集

排序数据

ORDER BY 子句用于排序,使用以下关键字进行升序或降序排序,要注意关键字只应用于直接位于其前面的列名。如果想在多个列上进行降序排序,必须对每一列指定 DESC 关键字。

关键字 描述
ASC 升序排序(默认)
DESC 降序排序

限制结果集

LIMIT 子句可用于限制 SELECT 语句返回的结果集:

1
2
3
SELECT * FROM tbl LIMIT 5;     # Retrieve first 5 rows, equivalent to LIMIT 0, 5
SELECT * FROM tbl LIMIT 5,10; # Retrieve rows 6-15
SELECT * FROM tbl LIMIT 5 OFFSET 10; # Retrieve rows 11-15

汇总数据

我们经常需要汇总数据而不用把它们实际检索出来,为此 SQL 提供了五个聚集函数(aggregate function)。使用这些函数,SQL 查询可用于检索数据,以便分析和报表生成。这种类型的检索例子有:

  • 确定表中行数(或者满足某个条件或包含某个特定值的行数);
  • 获得表中某些行的和;
  • 找出表列(或所有行或某些特定的行)的最大值、最小值、平均值。
聚集函数 描述
COUNT() 返回某列的行数,忽略 NULL 值。COUNT(*) 则包含 NULL 值。
AVG() 返回某列的平均值,忽略 NULL 值。
MAX() 返回某列的最大值,忽略 NULL 值。
MIN() 返回某列的最小值,忽略 NULL 值。
SUM() 返回某列值之和,忽略 NULL 值。

修饰符 ALLDISTINCT 可用于指定重复行是否应该返回。例如:

1
2
3
4
5
6
7
8
9
10
11
# 返回特定供应商所提供产品的平均价格
SELECT AVG(prod_price) AS avg_price
FROM Products
WHERE vend_id = 'DLL01';
-> 3.8650

# 同上,但平均值只考虑各个不同的价格
SELECT AVG(DISTINCT prod_price) AS avg_price
FROM Products
WHERE vend_id = 'DLL01';
-> 4.2400

可以看到,使用了 DISTINCT 后的 avg_price 会比较高,因为此例子中有多个物品具有相同的较低价格,排除它们提升了平均价格。

SELECT 语句也可根据需要同时使用多个聚集函数:

1
2
3
4
5
SELECT COUNT(*) AS num_items,
MIN(prod_price) AS price_min,
MAX(prod_price) AS price_max,
AVG(prod_price) AS price_avg
FROM Products;

分组数据

GROUP BY 子句用于分组数据,注意:

  • GROUP BY 子句可以包含任意数目的列,因而可以对分组进行嵌套,更细致地进行数据分组。
  • 如果在 GROUP BY 子句中嵌套了分组,数据将在最后指定的分组上进行汇总。换句话说,在建立分组时,指定的所有列都一起计算(所以不能从个别的列取回数据)。
  • GROUP BY 子句中列出的每一列都必须是检索列或有效的表达式(但不能是聚集函数)。如果在 SELECT 中使用表达式,则必须在 GROUP BY 子句中指定相同的表达式。不能使用别名。
  • 除聚集计算语句外,SELECT 语句中的每一列都必须在 GROUP BY 子句中给出。否则,如果 SELECT 语句中出现了 GROUP BY 中没有的列,假如该分组内的条目数大于 1,这样的列显示的内容为第一个条目的值。
  • 如果分组列中包含具有 NULL 值的行,则 NULL 将作为一个分组返回。如果列中有多行 NULL 值,它们将分为一组。

GROUP BY 可以搭配使用 HAVING 过滤分组。HAVINGWHERE 的差别在于,WHERE 对分组前的数据进行过滤, HAVING 对分组后的数据进行过滤。

加锁读

InnoDB 支持两种类型的 加锁读(Locking Reads),为事务操作提供额外的安全性

  • SELECT ... LOCK IN SHARE MODE 设置共享(S)锁
  • SELECT ... FOR UPDATE 设置排它(X)锁

详情请参考《MySQL 锁机制总结》。

UNION 子句

语法:

1
2
3
SELECT ...
UNION [ALL | DISTINCT] SELECT ...
[UNION [ALL | DISTINCT] SELECT ...]

UNION 将来自多个 SELECT 语句的结果合并为一个结果集,结果集列名取自第一条 SELECT 语句的列名。

UNIONUNION DISTINCT 去重,而 UNION ALL 则不去重。

要为单独的一条 SELECT 语句应用 ORDER BYLIMIT 子句,需要将子句放在包含 SELECT 的括号内,例如:

1
2
3
(SELECT a FROM t1 WHERE a=10 AND B=1 ORDER BY a LIMIT 10)
UNION
(SELECT a FROM t2 WHERE a=11 AND B=2 ORDER BY a LIMIT 10);

反之,如要应用到整个 UNION 结果集,则在单个 SELECT 语句后面加上括号,并在最后一个语句后面加上子句,例如:

1
2
3
4
(SELECT a FROM t1 WHERE a=10 AND B=1)
UNION
(SELECT a FROM t2 WHERE a=11 AND B=2)
ORDER BY a LIMIT 10;

参考

《MySQL 必知必会》

https://dev.mysql.com/doc/refman/5.7/en/select.html

https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html