Spring JdbcTemplate詳解,這都看不懂就安心去當個鹹魚吧!

無敵天驕發表於2021-04-20

JDBC 基礎

Java程式使用JDBC介面訪問關聯式資料庫的時候,需要以下幾步:

  • 建立全域性DataSource例項,表示資料庫連線池;

  • 在需要讀寫資料庫的方法內部,按如下步驟訪問資料庫:

  • 從全域性DataSource例項獲取Connection例項;

  • 透過Connection例項建立PreparedStatement例項;

  • 執行SQL語句,如果是查詢,則透過ResultSet讀取結果集,如果是修改,則獲得int結果。

正確編寫JDBC程式碼的關鍵是使用 try ... finally釋放資源,涉及到事務的程式碼需要正確提交或回滾事務。

在Spring使用JDBC,首先我們透過IoC容器建立並管理一個 DataSource例項,然後,Spring提供了一個 JdbcTemplate,可以方便地讓我們操作JDBC,因此,通常情況下,我們會例項化一個 JdbcTemplate。顧名思義,這個類主要使用了 Template模式

編寫示例程式碼或者測試程式碼時,我們強烈推薦使用 HSQLDB這個資料庫,它是一個用Java編寫的關聯式資料庫,可以以記憶體模式或者檔案模式執行,本身只有一個jar包,非常適合演示程式碼或者測試程式碼,本文使用MySql

我們以實際工程為例,先建立Maven工程 spring-data-jdbc,然後引入以下依賴:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.0.RELEASE</version>
    </dependency>
    <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.15</version>
        </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.2.0.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>javax.annotation</groupId>
        <artifactId>javax.annotation-api</artifactId>
        <version>1.3.2</version>
    </dependency>
    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
        <version>3.4.2</version>
    </dependency>
    <dependency>
        <groupId>org.hsqldb</groupId>
        <artifactId>hsqldb</artifactId>
        <version>2.5.0</version>
    </dependency>
</dependencies>

AppConfig中,我們需要建立以下幾個必須的Bean:

@Configuration@ComponentScan@PropertySource("jdbc.properties")public class AppConfig {
    @Value("${jdbc.url}")
    String jdbcUrl;
    @Value("${jdbc.username}")
    String jdbcUsername;
    @Value("${jdbc.password}")
    String jdbcPassword;
    @Bean
    DataSource createDataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(jdbcUrl);
        config.setUsername(jdbcUsername);
        config.setPassword(jdbcPassword);
        config.addDataSourceProperty("autoCommit", "true");
        config.addDataSourceProperty("connectionTimeout", "5");
        config.addDataSourceProperty("idleTimeout", "60");
        return new HikariDataSource(config);
    }
    @Bean
    JdbcTemplate createJdbcTemplate(@Autowired DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

在上述配置中:

  • 透過@PropertySource(" jdbc.properties")讀取資料庫配置檔案;

  • 透過@Value("${jdbc.url}")注入配置檔案的相關配置;

  • 建立一個DataSource例項,它的實際型別是HikariDataSource,建立時需要用到注入的配置;

  • 建立一個JdbcTemplate例項,它需要注入DataSource,這是透過方法引數完成注入的。

最後,針對HSQLDB寫一個配置檔案 jdbc.properties

# 資料庫檔名為testdb:jdbc.url=jdbc:hsqldb:file:testdb# Hsqldb預設的使用者名稱是sa,口令是空字串:jdbc.username=sa
jdbc.password=# mysql配置# 資料庫檔名為testdb:jdbc.driverClass=com.mysql.jdbc.Driverjdbc.url=jdbc:mysql://localhost:3306/springjdbc?characterEncoding=utf-8&serverTimezone=GMT%2B8&useSSL=falsejdbc.user=root
jdbc.password=1234

可以透過HSQLDB自帶的工具來初始化資料庫表,這裡我們寫一個Bean,在Spring容器啟動時自動建立一個users表:

@Componentpublic class DatabaseInitializer {
    @Autowired
    JdbcTemplate jdbcTemplate;
    @PostConstruct
    public void init() {
        jdbcTemplate.update("CREATE TABLE IF NOT EXISTS users (" //
                + "id BIGINT IDENTITY NOT NULL PRIMARY KEY, " //
                + "email VARCHAR(100) NOT NULL, " //
                + "password VARCHAR(100) NOT NULL, " //
                + "name VARCHAR(100) NOT NULL, " //
                + "UNIQUE (email))");
    }
}

現在,所有準備工作都已完畢。我們只需要在需要訪問資料庫的Bean中,注入 JdbcTemplate即可:

@Componentpublic class UserService {
    @Autowired
    JdbcTemplate jdbcTemplate;
    ...
}

JdbcTemplate 用法

Spring提供的 JdbcTemplate採用 Template模式,提供了一系列以回撥為特點的工具方法,目的是避免繁瑣的 try...catch語句。

我們以具體的示例來說明 JdbcTemplate的用法。

首先我們看 T execute(ConnectionCallback<T> action)方法,它提供了Jdbc的 Connection供我們使用:

public User getUserById(long id) {
    // 注意傳入的是ConnectionCallback:
    return jdbcTemplate.execute((Connection conn) -> {
        // 可以直接使用conn例項,不要釋放它,回撥結束後JdbcTemplate自動釋放:
        // 在內部手動建立的PreparedStatement、ResultSet必須用try(...)釋放:
        try (var ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?")) {
            ps.setObject(1, id);
            try (var rs = ps.executeQuery()) {
                if (rs.next()) {
                    return new User( // new User object:
                            rs.getLong("id"), // id
                            rs.getString("email"), // email
                            rs.getString("password"), // password
                            rs.getString("name")); // name
                }
                throw new RuntimeException("user not found by id.");
            }
        }
    });}`
也就是說,上述回撥方法允許獲取Connection,然後做任何基於Connection的操作。
我們再看T execute(String sql, PreparedStatementCallback<'T'> action)的用法:public User getUserByName(String name) {
    // 需要傳入SQL語句,以及PreparedStatementCallback:
    return jdbcTemplate.execute("SELECT * FROM users WHERE name = ?", (PreparedStatement ps) -> {
        // PreparedStatement例項已經由JdbcTemplate建立,並在回撥後自動釋放:
        ps.setObject(1, name);
        try (var rs = ps.executeQuery()) {
            if (rs.next()) {
                return new User( // new User object:
                        rs.getLong("id"), // id
                        rs.getString("email"), // email
                        rs.getString("password"), // password
                        rs.getString("name")); // name
            }
            throw new RuntimeException("user not found by id.");
        }
    });
}

最後,我們看 T queryForObject(String sql, Object[] args, RowMapper<'T'> rowMapper)方法

public User getUserByEmail(String email) {
    // 傳入SQL,引數和RowMapper例項:
    return jdbcTemplate.queryForObject("SELECT * FROM users WHERE email = ?", new Object[] { email },
            (ResultSet rs, int rowNum) -> {
                // 將ResultSet的當前行對映為一個JavaBean:
                return new User( // new User object:
                        rs.getLong("id"), // id
                        rs.getString("email"), // email
                        rs.getString("password"), // password
                        rs.getString("name")); // name
            });
}

RowMapper定義:

@FunctionalInterfacepublic interface RowMapper<T> {
    @Nullable
    T mapRow(ResultSet var1, int var2) throws SQLException;
}

方法 getUserByEmail中(ResultSet rs, int rowNum) -> {}語句實現一個RowMapper繼承類的物件

queryForObject()方法中,傳入SQL以及SQL引數後, JdbcTemplate會自動建立 PreparedStatement,自動執行查詢並返回 ResultSet,我們提供的 RowMapper需要做的事情就是把 ResultSet的當前行對映成一個 JavaBean並返回。整個過程中,使用 ConnectionPreparedStatementResultSet都不需要我們手動管理。

RowMapper不一定返回 JavaBean,實際上它可以返回任何Java物件。例如,使用 SELECT COUNT(*)查詢時,可以返回Long:

public long getUsers() {
    return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM users", null, (ResultSet rs, int rowNum) -> {
        // SELECT COUNT(*)查詢只有一列,取第一列資料:
        return rs.getLong(1);
    });
}

如果我們期望返回多行記錄,而不是一行,可以用query()方法:

public List<User> getUsers(int pageIndex) {
    int limit = 100;
    int offset = limit * (pageIndex - 1);
    return jdbcTemplate.query("SELECT * FROM users LIMIT ? OFFSET ?", new Object[] { limit, offset },
    new BeanPropertyRowMapper<>(User.class));
}

上述 query()方法傳入的引數仍然是SQL、SQL引數以及RowMapper例項。這裡我們直接使用Spring提供的 BeanPropertyRowMapper。如果資料庫表的結構恰好和JavaBean的屬性名稱一致,那麼 BeanPropertyRowMapper就可以直接把一行記錄按列名轉換為JavaBean。

如果我們執行的不是查詢,而是插入、更新和刪除操作,那麼需要使用update()方法:

public void updateUser(User user) {
    // 傳入SQL,SQL引數,返回更新的行數:
    if (1 != jdbcTemplate.update("UPDATE user SET name = ? WHERE id=?", user.getName(), user.getId())) {
        throw new RuntimeException("User not found by id");
    }
}

只有一種INSERT操作比較特殊,那就是如果某一列是自增列(例如自增主鍵),通常,我們需要獲取插入後的自增值。 JdbcTemplate提供了一個 KeyHolder來簡化這一操作:

public User register(String email, String password, String name) {
    // 建立一個KeyHolder:
    KeyHolder holder = new GeneratedKeyHolder();
    if (1 != jdbcTemplate.update(
        // 引數1:PreparedStatementCreator
        (conn) -> {
            // 建立PreparedStatement時,必須指定RETURN_GENERATED_KEYS:
            var ps = conn.prepareStatement("INSERT INTO users(email,password,name) VALUES(?,?,?)",
                    Statement.RETURN_GENERATED_KEYS);
            ps.setObject(1, email);
            ps.setObject(2, password);
            ps.setObject(3, name);
            return ps;
        },
        // 引數2:KeyHolder
        holder)
    ) {
        throw new RuntimeException("Insert failed.");
    }
    // 從KeyHolder中獲取返回的自增值:
    return new User(holder.getKey().longValue(), email, password, name);
}

JdbcTemplate還有許多過載方法,這裡我們不一一介紹。需要強調的是, JdbcTemplate只是對JDBC操作的一個簡單封裝,它的目的是儘量減少手動編寫 try(resource) {...}的程式碼,對於查詢,主要透過 RowMapper實現了JDBC結果集到Java物件的轉換。

我們總結一下 JdbcTemplate的用法,那就是:

  • 針對簡單查詢,優選query()和queryForObject(),因為只需提供SQL語句、引數和RowMapper;

  • 針對更新操作,優選update(),因為只需提供SQL語句和引數;

  • 任何複雜的操作,最終也可以透過execute(ConnectionCallback)實現,因為拿到Connection就可以做任何JDBC操作。

實際上我們使用最多的仍然是各種查詢。如果在設計表結構的時候,能夠和JavaBean的屬性一一對應,那麼直接使用 BeanPropertyRowMapper就很方便。如果表結構和JavaBean不一致怎麼辦?那就需要稍微改寫一下查詢,使結果集的結構和JavaBean保持一致。

例如,表的列名是 office_address,而JavaBean屬性是 workAddress,就需要指定別名,改寫查詢如下:

SELECT id, email, office_address AS workAddress, name FROM users WHERE email = ?

RowMapper 的基本使用

使用過原生JDBC的朋友應該知道,從資料庫查詢出來的記錄全都被儲存在 ResultSet結果集中,我們需要將結果集中的資料一條條地獲取並設定到具體的實體類上,如此,該實體類才能在接下來的程式中使用。然而問題是,每次都要這麼操作實在是太麻煩了,Spring就不應該提供什麼功能來替我們做這些事情嗎?答案當然是有的,那就是本文的主角—— RowMapper

Spring JDBC中目前有兩個主要的 RowMapper實現,使用它們應該能解決大部分的場景了。

SingleColumnRowMapper

透過名字我們就能大概瞭解,在查詢返回單列資料的時候,就該使用這個RowMapper,下面我們來看看具體的程式碼:

@Override
    public String getStudentNameById(String id) {
        String sql = "select name from test_student where id = ?";
        return this.jdbcTemplate.queryForObject(sql, new Object[]{id},
                new SingleColumnRowMapper<>(String.class));
    }
    @Override
    public List<String> getStudentNamesByGrade(Integer grade) {
        String sql = "select name from test_student where grade = ?";
        return this.jdbcTemplate.query(sql, new Object[]{grade},
                new SingleColumnRowMapper<>(String.class));
    }

測試:

@Test
    public void getStudentNameById(){
        String name = studentService.getStudentNameById("3");
        assertEquals("李清照", name);
    }
    @Test
    public void getStudentNamesByGrade(){
        List<String> names = studentService.getStudentNamesByGrade(1);
        assertTrue(2 == names.size());
    }

BeanPropertyRowMapper

當查詢資料庫返回的是多列資料,且你需要將這些多列資料對映到某個具體的實體類上,那麼就該使用這個RowMapper,下面是具體的使用程式碼:

@Override
public Student getStudentByName2(String name) {
    String sql = "select name, gender from test_student where name = ?";
    return this.jdbcTemplate.queryForObject(sql, new Object[]{name},
    new BeanPropertyRowMapper<>(Student.class));
}
@Override
public List<Student> getStudentsByName2(String name) {
    String sql = "select name, gender from test_student where name = ?";
    return this.jdbcTemplate.query(sql, new Object[]{name},
    new BeanPropertyRowMapper<>(Student.class));
}

這種使用方式有一個前提,那就是資料庫SQL查出來的資料其列名與實體類中的屬性名是一致的,當然個數和順序可以不一致。比如資料庫SQL查出來的姓名列叫name,那麼對應的實體類中的姓名也必須叫name,而不能叫studentName或者其它。

定義自己的RowMapper

當然,如果你SQL查詢出來的資料列名就是和實體類的屬性名不一樣,或者想按照自己的規則來裝配實體類,那麼就可以定義並使用自己的Row Mapper。

public class StudentRowMapper implements RowMapper<Student> {
    @Override
    public Student mapRow(ResultSet rs, int i) throws SQLException {
        Student student = new Student();
        student.setName(rs.getString("name"));
        student.setGender(rs.getString("gender"));
        student.setEmail(rs.getString("email"));
        return student;
    }
}
@Override
public Student getStudentByName3(String name) {
    String sql = "select name, gender, email from test_student where name = ?";
    return this.jdbcTemplate.queryForObject(sql, new Object[]{name}, new StudentRowMapper());
}
@Override
public List<Student> getStudentsByName3(String name) {
    String sql = "select name, gender, email from test_student where name = ?";
    return this.jdbcTemplate.query(sql, new Object[]{name}, new StudentRowMapper());
}

小結

  • Spring提供了 JdbcTemplate來簡化JDBC操作;

  • 使用 JdbcTemplate時,根據需要優先選擇高階方法;

  • 任何JDBC操作都可以使用保底的 execute(ConnectionCallback)方法


喜歡這篇文章的朋友們可以關注個人簡介中的公眾號

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69964492/viewspace-2768938/,如需轉載,請註明出處,否則將追究法律責任。

相關文章