一、简介
Java DataBase Connectivity ,是Java程序访问数据库的标准接口
Java访问DB的时候,并不是直接通过TCP连接的,而是通过JDBC接口,而JDBC接口又是通过JDBC驱动来访问的
JDBC是Java标准库自带的,具体的JDBC驱动是由数据库厂商提供的,所以JBDC借口都是统一的
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
│ ┌───────────────┐ │
│ Java App │
│ └───────────────┘ │
│
│ ▼ │
┌───────────────┐
│ │JDBC Interface │◀─┼─── JDK
└───────────────┘
│ │ │
▼
│ ┌───────────────┐ │
│ MySQL Driver │◀───── Oracle
│ └───────────────┘ │
│
└ ─ ─ ─ ─ ─│─ ─ ─ ─ ─ ┘
▼
┌───────────────┐
│ MySQL │
└───────────────┘
一个MySQL的驱动就是一个jar包,本身也是纯java编写的,我们自己的代码只需要引用java.sql.*接口,接口再通过MySQL驱动的jar包访问DB
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
┌───────────────┐
│ │ App.class │ │
└───────────────┘
│ │ │
▼
│ ┌───────────────┐ │
│ java.sql.* │
│ └───────────────┘ │
│
│ ▼ │
┌───────────────┐ TCP ┌───────────────┐
│ │ mysql-xxx.jar │──┼────────▶│ MySQL │
└───────────────┘ └───────────────┘
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
JVM
二、JDBC查询
2.1 驱动
JDBC是一套标准的规范接口,在 java.sql 下 , 具体的实现类在厂家提供的驱动中
maven 添加驱动
<!-- https://mvnrepository.com/artifact/com.mysql/mysql-connector-j -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.31</version>
</dependency>
创造一些测试数据
-- 创建数据库learjdbc:
DROP DATABASE IF EXISTS learnjdbc;
CREATE DATABASE learnjdbc;
-- 创建登录用户learn/口令learnpassword
CREATE USER IF NOT EXISTS learn@'%' IDENTIFIED BY 'learnpassword';
GRANT ALL PRIVILEGES ON learnjdbc.* TO learn@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;
-- 创建表students:
USE learnjdbc;
CREATE TABLE students (
id BIGINT AUTO_INCREMENT NOT NULL,
name VARCHAR(50) NOT NULL,
gender TINYINT(1) NOT NULL,
grade INT NOT NULL,
score INT NOT NULL,
PRIMARY KEY(id)
) Engine=INNODB DEFAULT CHARSET=UTF8;
-- 插入初始数据:
INSERT INTO students (name, gender, grade, score) VALUES ('小明', 1, 1, 88);
INSERT INTO students (name, gender, grade, score) VALUES ('小红', 1, 1, 95);
INSERT INTO students (name, gender, grade, score) VALUES ('小军', 0, 1, 93);
INSERT INTO students (name, gender, grade, score) VALUES ('小白', 0, 1, 100);
INSERT INTO students (name, gender, grade, score) VALUES ('小牛', 1, 2, 96);
INSERT INTO students (name, gender, grade, score) VALUES ('小兵', 1, 2, 99);
INSERT INTO students (name, gender, grade, score) VALUES ('小强', 0, 2, 86);
INSERT INTO students (name, gender, grade, score) VALUES ('小乔', 0, 2, 79);
INSERT INTO students (name, gender, grade, score) VALUES ('小青', 1, 3, 85);
INSERT INTO students (name, gender, grade, score) VALUES ('小王', 1, 3, 90);
INSERT INTO students (name, gender, grade, score) VALUES ('小林', 0, 3, 91);
INSERT INTO students (name, gender, grade, score) VALUES ('小贝', 0, 3, 77);
2.2 连接
使用JDBC 需要首先了解什么是Connection:一个jdbc连接,相当于java程序到数据库的连接,链接DB需要:URL username pw 口令
url:jdbc:mysql://:/?key1=value1&key2=value2
jdbc:mysql://localhost:3306/learnjdbc?useSSL=false&characterEncoding=utf8
useSSL=false&characterEncoding=utf8 不使用SSL加密,使用UTF-8作为字符编码
// JDBC连接的URL, 不同数据库有不同的格式:
String JDBC_URL = "jdbc:mysql://localhost:3306/test";
String JDBC_USER = "root";
String JDBC_PASSWORD = "password";
// 获取连接:
Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
// TODO: 访问数据库...
// 关闭连接:
conn.close();
核心代码是DriverManager 提供的静态方法getConnection()。Driver会自动扫描classpath,找到所有的JDBC驱动,然后根据URL挑选一个合适的驱动
因为JDBC连接是昂贵的资源,用后及时释放 用 try(resource)来自动释放JDBC是一个好方法
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
...
}
2.3 查询
package com.ifeng;
import java.sql.*;
/**
* Hello world!
*
*/
public class App
{
public static void main( String[] args ) throws SQLException {
// JDBC连接的URL, 不同数据库有不同的格式:
String JDBC_URL = "jdbc:mysql://localhost:3306/learnjdbc";
String JDBC_USER = "root";
String JDBC_PASSWORD = "123456";
Connection connection;
Statement statement = null;
ResultSet resultSet = null;
//获取链接
try {
connection = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
statement = connection.createStatement();
resultSet = statement.executeQuery("select * from students");
while (resultSet.next()){
Long id = resultSet.getLong(1);
String name = resultSet.getString(2);
String gender = resultSet.getString(3);
String grade = resultSet.getString(4);
String score = resultSet.getString(5);
System.out.println("id = " + id + " , name = " + name + " , gender = " + gender + " , grade = " + grade + " , score = " + score);
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
if(statement != null) statement.close();
if(resultSet != null) resultSet.close();
}
}
}
statement & resultSet 都是稀缺资源,及时关闭
resultSet.next() 用于判断是否有下一行,如果有 自动移入下一行
ResultSet 获取列时,索引从1开始
2.4 SQL注入
使用statement 非常容易引发SQL注入的问题
例如:验证登陆的方法:
User login(String name, String pass) {
...
stmt.executeQuery("SELECT * FROM user WHERE login='" + name + "' AND pass='" + pass + "'");
...
}
name & pass 都是从前端传过来的
name = "bob' OR pass=", pass = " OR pass='"
SELECT * FROM user WHERE login='bob' OR pass=' AND pass=' OR pass=''
避免SQL注入,针对所有的字符串进行转义,但很麻烦
还有一个方法就是PreparedStatement :Prepared 始终使用 ? 作为占位符,并且把数据 & SQL 本身传递给DB
User login(String name, String pass) {
...
String sql = "SELECT * FROM user WHERE login=? AND pass=?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setObject(1, name);
ps.setObject(2, pass);
...
}
改造上面的查询
try {
connection = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
PreparedStatement preparedStatement = connection.prepareStatement("select * from students WHERE gender=? AND grade=?");
preparedStatement.setObject(1,"M"); // 设置占位符的值
preparedStatement.setObject(2,3);
resultSet1 = preparedStatement.executeQuery();// 最后仍然是ResultSet
while (resultSet1.next()){
Long id = resultSet1.getLong(1);
String name = resultSet1.getString(2);
String gender = resultSet1.getString(3);
String grade = resultSet1.getString(4);
String score = resultSet1.getString(5);
System.out.println("id = " + id + " , name = " + name + " , gender = " + gender + " , grade = " + grade + " , score = " + score);
}
}
三、JDBC更新
DB操作总结起来就是增删改查,CRUD:Create Retrieve Update Delete
查询用上面的PreparedStatement
3.1 插入
insert ,本质上也是用PreparedStatement执行一条SQL,不过执行者不是executeQuery() 而是executeUpdate()
connection = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO students (id, grade, name, gender, score) VALUES (?,?,?,?,?)");
preparedStatement.setObject(1,10);
preparedStatement.setObject(2,3);
preparedStatement.setObject(3,"Bob");
preparedStatement.setObject(4,2);
preparedStatement.setObject(5,101);
int i = preparedStatement.executeUpdate(); // 使用executeUpdate来更新
System.out.println("executeUpdate = " + i);
设置参数和查询是一样的,有几个?占位符就设置几个对应参数,
要严格执行不能手动拼接SQL字符串的原则,避免安全漏洞
3.2 插入并获取主键
表设置了自增主键,insert后 数据库会自动分配主键,如何获取主键?
在创建PreparedStatement的时候,指定一个RETURN_GENERATED_KEYS标志位,表示JDBC驱动必须返回插入的自增主键
connection = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
PreparedStatement preparedStatement = connection.prepareStatement(
"INSERT INTO students (id, grade, name, gender, score) VALUES (?,?,?,?)",
Statement.RETURN_GENERATED_KEYS
);
preparedStatement.setObject(1,5);
preparedStatement.setObject(2,"Jerry");
preparedStatement.setObject(3,2);
preparedStatement.setObject(4,101);
int i = preparedStatement.executeUpdate(); // 使用executeUpdate来更新
try(ResultSet rs = preparedStatement.getGeneratedKeys()){
if (rs.next()) {
System.out.println(rs.getLong(1));
}
}
3.3 更新
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
try (PreparedStatement ps = conn.prepareStatement("UPDATE students SET name=? WHERE id=?")) {
ps.setObject(1, "Bob"); // 注意:索引从1开始
ps.setObject(2, 999);
int n = ps.executeUpdate(); // 返回更新的行数
}
}
3.4 删除
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
try (PreparedStatement ps = conn.prepareStatement("DELETE FROM students WHERE id=?")) {
ps.setObject(1, 999); // 注意:索引从1开始
int n = ps.executeUpdate(); // 删除的行数
}
}
四、事务
DB事务(Transaction)由若干个SQL语句构成一个操作序列,类似Java的synchronized同步,一个SQL要么全部成功,或者全部不成功:ACID
- Atomicity:原子性
- Consistency:一致性
- Isolation:隔离性
- Durability:持久性
数据库事务可以并发执行,从效率角度,定义了不同的隔离级别
- 脏读:A事务读到B事务更新但未提交的数据,B回滚了
- 不重复读:A事务第一次读取数据后,B事务修改了数据,A又读取数据,两次数据不一致
- 幻读:A事务查询记录,没有,然后更新这条记录,竟然成功了
在JDBC中执行事物,本质是把多条SQL包裹在一个数据库事务中执行
Connect conn = openConnection();
try{
//关闭自动提交
conn.setAutoCommit(false);
//执行多条SQL
insert();update();delete;
//提交事物
conn.commit();
}catch(SQLException e){
// 回滚事物;
conn.rollback();
}
conn.setAtuoCommit(false) 代表自动提交关闭
conn.commit() 手动提交
//可以设定隔离级别,默认REPEATABLE_READ
conn.setTrasactionIsolation(Connection.TRANSACTION_READ_COMMITTED)
五、Batch
批量操作,可通过循环来执行PreparedStatement来执行,但是性能很低。只有参数不同若干语句可以作为batch执行(批量),它有特别的优化,速度远远快于循环执行的每个SQL
public class JDBCTest5
{
public static void main( String[] args ) throws SQLException {
List<Student> students = List.of(
new Student("小红", 20L,3L,48)
,new Student("小红2", 20L,3L,49)
,new Student("小红3", 20L,3L,50)
,new Student("小红4", 20L,3L,49)
,new Student("小红5", 20L,3L,55)
);
// JDBC连接的URL, 不同数据库有不同的格式:
String JDBC_URL = "jdbc:mysql://localhost:3306/learnjdbc";
String JDBC_USER = "root";
String JDBC_PASSWORD = "123456";
Connection connection;
Statement statement = null;
ResultSet resultSet1 = null;
//获取链接
try {
connection = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
PreparedStatement preparedStatement = connection.prepareStatement(
"INSERT INTO students (grade, name, gender, score) VALUES (?,?,?,?)"
);
//对于同一个PreparedStatement反复设置参数并调用addBatch()
for(Student s : students){
preparedStatement.setObject(1,s.grade);
preparedStatement.setObject(2,s.name);
preparedStatement.setObject(3,s.gender);
preparedStatement.setObject(4,s.score);
preparedStatement.addBatch();//添加到batch
}
//执行batch
int[] executeBatch = preparedStatement.executeBatch();
for(int n:executeBatch){
System.out.println(n + " inserted.");
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
if(statement != null) statement.close();
if(resultSet1 != null) resultSet1.close();
}
}
}
六、连接池
JDBC连接是一种俺贵的资源,创建线程也是一种昂贵的操作,频繁的创建销毁JDBC,会造成大量消耗
JDBC链接池有一个标准的接口 javax.sql.DataSource , 接口在标准库中。要使用JDBC连接池,必须用它的实现类:
- HikariCP
- C3P0
- BoneCP
- Druid
使用最广泛的是HikariCP以此为例,先添加HikariCP的依赖
<!-- https://mvnrepository.com/artifact/com.zaxxer/HikariCP -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>2.7.9</version>
</dependency>
配置连接池
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setJdbcUrl("jdbc:mysql://localhost:3306/learnjdbc");
hikariConfig.setUsername("root");
hikariConfig.setPassword("123456");
hikariConfig.addDataSourceProperty("connectionTimeout","1000");//连接超时 1s
hikariConfig.addDataSourceProperty("idleTimeout","6000");//空闲超时 6s
hikariConfig.addDataSourceProperty("maximumPoolSize","10");//最大连接数
HikariDataSource hikariDataSource = new HikariDataSource(hikariConfig);
使用连接池
connection = hikariDataSource.getConnection();
...
connection.close();
get close 都不是真的创建销毁,而是从连接池中获取 放回
七、项目
7.1 第一版
-- 创建表t_fruit:
USE learnjdbc;
CREATE TABLE t_fruit (
fid BIGINT AUTO_INCREMENT NOT NULL,
fname VARCHAR(50) NOT NULL,
price INT NOT NULL,
fcount INT NOT NULL,
remark VARCHAR(50) NOT NULL,
PRIMARY KEY(fid)
) Engine=INNODB DEFAULT CHARSET=UTF8;
-- 插入初始数据:
INSERT INTO t_fruit (fname, price, fcount, remark) VALUES ('Apple', 10, 20, '苹果');
INSERT INTO t_fruit (fname, price, fcount, remark) VALUES ('Strawberry', 19, 20, '草莓');
INSERT INTO t_fruit (fname, price, fcount, remark) VALUES ('orange', 11, 20, '橙子');
目录结构
public class FruitDAOImpl implements FruitDAO {
Connection conn ;
PreparedStatement psmt ;
ResultSet rs ;
// JDBC连接的URL, 不同数据库有不同的格式:
final String DRIVER = "com.mysql.jdbc.Driver" ;
final String URL = "jdbc:mysql://localhost:3306/learnjdbc";
final String USER = "root";
final String PWD = "123456";
@Override
public List<Fruit> getFruitList() {
List<Fruit> fruitList = new ArrayList<>();
try {
//1 加载驱动
Class.forName(DRIVER);
//2 通过驱动获取连接对象
conn = DriverManager.getConnection(URL, USER, PWD);
//3 编写SQL
String sql = "select * from learnjdbc.t_fruit";
//4 创建预处理命令对象
PreparedStatement psmt = conn.prepareStatement(sql);
//5 执行查询
ResultSet rs = psmt.executeQuery(sql);
System.out.println(rs);
//6 解析rs
while (rs.next()){
String fid = rs.getString(1);
String fname = rs.getString(2);
String price = rs.getString(3);
String fcount = rs.getString(4);
String remark = rs.getString(5);
Fruit fruit = new Fruit(fid, fname, price, fcount, remark);
fruitList.add(fruit);
}
} catch (SQLException | ClassNotFoundException e) {
throw new RuntimeException(e);
} finally {
try {
if (rs != null) {
rs.close();
}
if(psmt!=null){
psmt.close();
}
if(conn!=null && !conn.isClosed()){
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
return fruitList;
}
7.2 抽取获取资源释放资源
每一个方法都是
//1 加载驱动
//2 通过驱动获取连接对象
//3 编写SQL
//4 创建预处理命令对象
//5 执行查询
//finaly close
可以把1 2 合成为getConn方法
private Connection getConn(){
try {
//1 加载驱动
Class.forName(DRIVER);
//2 通过驱动获取连接对象
conn = DriverManager.getConnection(URL, USER, PWD);
} catch (SQLException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
return conn;
}
可以把最终的close都提取出来
private void close(ResultSet rs,PreparedStatement psmt,Connection conn){
try {
if (rs != null) {
rs.close();
}
if(psmt!=null){
psmt.close();
}
if(conn!=null && !conn.isClosed()){
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
上面的getFruitList就变成了
@Override
public List<Fruit> getFruitList() {
List<Fruit> fruitList = new ArrayList<>();
try {
conn = getConn();
//3 编写SQL
String sql = "select * from learnjdbc.t_fruit";
//4 创建预处理命令对象
PreparedStatement psmt = conn.prepareStatement(sql);
//5 执行查询
ResultSet rs = psmt.executeQuery(sql);
System.out.println(rs);
//6 解析rs
while (rs.next()){
String fid = rs.getString(1);
String fname = rs.getString(2);
String price = rs.getString(3);
String fcount = rs.getString(4);
String remark = rs.getString(5);
Fruit fruit = new Fruit(fid, fname, price, fcount, remark);
fruitList.add(fruit);
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
close(rs,psmt,conn);
}
return fruitList;
}
7.3 抽取BaseDAO中通用方法
常量抽取
FruitDAOImpl 存在 DRIVER URL USER PWD 等,这些可以向上抽出来,和其它impl一起使用
public abstract class BaseDAO {
// JDBC连接的URL, 不同数据库有不同的格式:
protected final String DRIVER = "com.mysql.jdbc.Driver" ;
protected final String URL = "jdbc:mysql://localhost:3306/learnjdbc";
protected final String USER = "root";
protected final String PWD = "123456";
}
BaseDAO 为 abstract 的,Impl使用直接extend BaseDAO 即可
资源方法抽取
之前抽取的getConn() & close() 也可以在这里
protected Connection conn ;
protected PreparedStatement psmt ;
protected ResultSet rs ;
protected Connection getConn(){
try {
//1.加载驱动
Class.forName(DRIVER);
//2.通过驱动管理器获取连接对象
return DriverManager.getConnection(URL, USER, PWD);
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
}
return null ;
}
protected void close(ResultSet rs , PreparedStatement psmt , Connection conn){
try {
if (rs != null) {
rs.close();
}
if(psmt!=null){
psmt.close();
}
if(conn!=null && !conn.isClosed()){
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
执行方法抽取
//1 加载驱动
//2 通过驱动获取连接对象
//3 编写SQL
//4 创建预处理命令对象
//5 执行查询
//finaly close
每一个任务也就SQL不一样 ,其它都已经
再次进行抽取
//执行更新,返回影响行数
protected int executeUpdate(String sql , Object... params){
try {
conn = getConn();
psmt = conn.prepareStatement(sql);
if(params!=null && params.length>0){
for (int i = 0; i < params.length; i++) {
psmt.setObject(i+1,params[i]);
}
}
return psmt.executeUpdate() ;
} catch (SQLException e) {
e.printStackTrace();
}finally {
close(rs,psmt,conn);
}
return 0;
}
由于传入的参数个数是不一样的,所以用 不定参数 Object… params
Impl重写
@Override
public boolean addFruit(Fruit fruit) {
String sql = "insert into t_fruit values(0,?,?,?,?)";
return super.executeUpdate(sql,fruit.getFname(),fruit.getPrice(),fruit.getFcount(),fruit.getRemark())>0;
}
7.4 抽取通用的查询方法-获取entityClass
public List getFruitList() {
对于其它Class ,模式都是一样的,所以可以抽取出来executeUpdate()
//给预处理命令对象设置参数
private void setParams(PreparedStatement psmt , Object... params) throws SQLException {
if(params!=null && params.length>0){
for (int i = 0; i < params.length; i++) {
psmt.setObject(i+1,params[i]);
}
}
}
//执行查询,返回List
protected List<T> executeQuery(String sql , Object... params){
List<T> list = new ArrayList<>();
try {
conn = getConn() ;
psmt = conn.prepareStatement(sql);
setParams(psmt,params);
rs = psmt.executeQuery();
//通过rs可以获取结果集的元数据
//元数据:描述结果集数据的数据 , 简单讲,就是这个结果集有哪些列,什么类型等等
ResultSetMetaData rsmd = rs.getMetaData();
//获取结果集的列数
int columnCount = rsmd.getColumnCount();
//6.解析rs
while(rs.next()){
T entity = (T)entityClass.newInstance();
for(int i = 0 ; i<columnCount;i++){
String columnName = rsmd.getColumnName(i+1); //fid fname price
Object columnValue = rs.getObject(i+1); //33 苹果 5
setValue(entity,columnName,columnValue);
}
list.add(entity);
}
} catch (SQLException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} finally {
close(rs,psmt,conn);
}
return list ;
}
T是后续impl指定的
public class FruitDAOImpl extends BaseDAO<Fruit> implements FruitDAO {
在BaseDAO中应该获取Impl 中的T
public BaseDAO(){
//getClass() 获取Class对象,当前我们执行的是new FruitDAOImpl() , 创建的是FruitDAOImpl的实例
//那么子类构造方法内部首先会调用父类(BaseDAO)的无参构造方法
//因此此处的getClass()会被执行,但是getClass获取的是FruitDAOImpl的Class
//所以getGenericSuperclass()获取到的是BaseDAO的Class
Type genericType = getClass().getGenericSuperclass();
//ParameterizedType 参数化类型
Type[] actualTypeArguments = ((ParameterizedType) genericType).getActualTypeArguments();
//获取到的<T>中的T的真实的类型
Type actualType = actualTypeArguments[0];
try {
entityClass = Class.forName(actualType.getTypeName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
7.5 抽取通用的查询方法
之前add方法中,由于T 不能 new
现在可以通过反射来做
//通过反射技术给obj对象的property属性赋propertyValue值
private void setValue(Object obj , String property , Object propertyValue){
Class clazz = obj.getClass();
try {
//获取property这个字符串对应的属性名 , 比如 "fid" 去找 obj对象中的 fid 属性
Field field = clazz.getDeclaredField(property);
if(field!=null){
field.setAccessible(true);
field.set(obj,propertyValue);
}
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
if(rs.next()){
T entity = (T)entityClass.newInstance();
for(int i = 0 ; i<columnCount;i++){
String columnName = rsmd.getColumnName(i+1); //fid fname price
Object columnValue = rs.getObject(i+1); //33 苹果 5
setValue(entity,columnName,columnValue);
}
return entity ;
}
impl中调用
@Override
public List<Fruit> getFruitList() {
return super.executeQuery("select * from t_fruit");
}