套用 Spring Data JDBC


在〈簡化 JDBC 與 Mail〉之後,看看其中的 AccountDAOMessageDAO 實作,幾乎就只剩下 SQL 語句了,這不禁人想到〈簡介 Spring Data JDBC〉,也只是下下 SQL 就了事了。

那就在你的 build.gradle 加上 Spring Data JDBC 相依吧!

implementation('org.springframework.boot:spring-boot-starter-data-jdbc')

在原本的 AccountDAO 實作中,createAccount 方法下了兩條 SQL 語句,分別在兩個表格新增資料,這是因為一開始,本預計使用者可能會擁有多個角色,為了一開始不要讓事情變得複雜,將新增使用者與角色都隱藏在 createAccount 之中。

其實這應該是兩個任務,基本上應該另外定義個 AccountRoleDAO 來處理使用者角色對應的問題,不過,gossip 應用程式實際上只用到一個 ROLE_MEMBER 角色,乾脆就把角色寫在與帳號表格好了,如此,AccountDAO 就可以只寫為:

package cc.openhome.model;

import java.util.Optional;

import org.springframework.data.jdbc.repository.query.Modifying;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;

public interface AccountDAO extends CrudRepository<Account, Integer> {
    @Query("SELECT * FROM account WHERE name = :name")
    Optional<Account> accountByUsername(@Param("name") String name);

    @Query("SELECT * FROM account WHERE email = :email")
    Optional<Account> accountByEmail(@Param("email") String email);

    @Modifying
    @Query("UPDATE account SET enabled = 1 WHERE name = :name")
    void activateAccount(@Param("name") String name);

    @Modifying
    @Query("UPDATE account SET password = :password WHERE name = :name")  
    void updatePassword(@Param("name") String name, @Param("password") String password);
}

MessageDAO 就單純多了:

package cc.openhome.model;

import java.util.List;

import org.springframework.data.jdbc.repository.query.Modifying;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;

public interface MessageDAO extends CrudRepository<Message, Integer> {
    @Query("SELECT * FROM message m WHERE m.username = :username")
    List<Message> messagesBy(@Param("username") String username);

    @Modifying
    @Query("DELETE FROM message WHERE username = :username AND millis = :millis")   
    void deleteMessageBy(@Param("username") String username, @Param("millis") String millis);

    @Query("SELECT * FROM message ORDER BY millis DESC LIMIT :n")
    List<Message> newestMessages(@Param("n") int n);
}

原本 AccountDAOMessageDAO 各定義了儲存帳號與訊息的方法,現在因為繼承了 CrudRepository,而它本身有個 save 方法,因此就可以刪掉 AccountDAOMessageDAO 中各自儲存帳號與訊息的方法。

表格本身則重新調整如下:

CREATE TABLE account (
    id INT NOT NULL AUTO_INCREMENT,
    name VARCHAR(15) NOT NULL,
    email VARCHAR(128) NOT NULL,
    password VARCHAR(64) NOT NULL,
    enabled TINYINT NOT NULL,
    role VARCHAR(15) NOT NULL,
    PRIMARY KEY (id)
);
CREATE TABLE message (
    id INT NOT NULL AUTO_INCREMENT,
    username VARCHAR(15) NOT NULL,
    millis BIGINT NOT NULL,
    blabla VARCHAR(512) NOT NULL,
    PRIMARY KEY (id)
);

Account 部份,標示 @Id 並加上對應的欄位:

package cc.openhome.model;

import org.springframework.data.annotation.Id;

public class Account {
    @Id
    private Integer id;
    private String name;
    private String email;
    private String password;
    private Integer enabled;
    private String role;

    public Account() {
    }

    public Account(String name, String email, String password, Integer enabled, String role) {
        this.name = name;
        this.email = email;
        this.password = password;
        this.enabled = enabled;
        this.role = role;
    }    

    public Account(String name, String email, String password) {
        this(name, email, password, 0, "ROLE_MEMBER");
    }

    ...略
}

Message 也是:

package cc.openhome.model;

import java.time.*;

import org.springframework.data.annotation.Id;

public class Message {
    @Id
    private Integer id;
    private String username;
    private Long millis;
    private String blabla;

    ...略
}

因為調整了表格,在 Spring Security 的 JDBC 驗證部份,SQL 要做點修改:

auth.jdbcAuthentication()
    .passwordEncoder(passwordEncoder)
    .dataSource(dataSource)
    .usersByUsernameQuery("select name, password, enabled from account where name=?")
    .authoritiesByUsernameQuery("select name, role from account where name=?");

因為調整了 AccountDAOMessageDAO 等協定,UserService 勢必也要做點相應的變化,主要就是與新增操作有關的方法:

package cc.openhome.model;

...歄

@Service
public class UserService {
    ...略

    private Account createUser(String username, String email, String password) {
        Account acct = new Account(username, email, passwordEncoder.encode(password), 0, "ROLE_MEMBER");
        accountDAO.save(acct);
        return acct;
    }

    ...略

    public void addMessage(String username, String blabla) {
        messageDAO.save(new Message(
                username, Instant.now().toEpochMilli(), blabla));
    }    

    ...略
}

你可以在 gossip 找到以上的範例專案。