三分鐘數據持久化:Spring Boot, JPA 與 SQLite 的完美融合

三分鐘,迎接一個更加高效和簡便的開發體驗。

在快節奏的軟件開發領域,每一個簡化工作流程的機會都不容錯過。想要一個無需繁瑣配置、能夠迅速啓動的數據持久化方案嗎?這篇文章將是你的首選攻略。在這裏,我們將向你展示如何將 Spring Boot 的便捷性、JPA 的強大查詢能力和 SQLite 的輕量級特性結合在一起,實現快速而又優雅的數據管理。

爲什麼選擇 SQLite

SQLite 是一個用 C 語言編寫的開源、輕量級、快速、獨立且高可靠性的 SQL 數據庫引擎,它提供了功能齊全的數據庫解決方案。對於大多數的應用,SQLite 都可以滿足。使用 SQLite 可以零配置啓動,對於小型應用或者快速原型設計是一個非常大的優勢。

使用 SQLite 具有下面幾個優點:

  1. 輕量級:SQLite很小巧,不需要獨立服務器,便於集成到應用中。
  2. 零配置:啓用 SQLite 無需複雜配置,只需指定一個文件路徑存放 DB 文件,簡化了數據庫的設置流程。
  3. 便於移植:數據庫是單一文件,方便備份和在不同環境間遷移。
  4. 跨平臺:SQLite 支持各種操作系統,容易實現應用的跨平臺運行。
  5. 性能良好:對於小型應用,SQLite 提供足夠的讀寫性能。
  6. 遵循ACID:SQLite事務符合ACID原則,數據操作可靠。
  7. 社區支持:雖然簡單,但擁有強大的社區和廣泛的文檔資源。

之前寫過一篇 SQLite 入門教程 (https://www.wdbyte.com/db/sqlite/),感情的同學可以參考。

爲什麼 選擇 JPA

Spring Data JPA 是Spring Data項目的一部分,旨在簡化基於JPA(Java Persistence API)的數據訪問層(Repository層)的實現。JPA是一種 ORM(對象關係映射)規範,它允許開發者以面向對象的方式來操作數據庫,

通常應用程序實現數據訪問層可能非常麻煩,必須編寫太多的樣板代碼才能實現簡單的查詢,更不用說分頁等其他操作,而 Spring Data JPA 可以讓開發者非常容易地實現對數據庫的各種操作,顯著減少實際需要的工作量。

詳細介紹 JPA 並不是本文目的,關於 JPA 的更多內容可以訪問:

  1. Spring Data JPA 官網:https://spring.io/projects/spring-data-jpa
  2. Spring Boot 使用 Spring Data JPA

創建 Spring Boot 項目

用於後續演示,首先創建一個簡單的 Spring Boot 項目。你可以自由創建,或者使用 Spring 官網提供的快速創建工具:https://start.spring.io/

注意,文章示例項目使用 Java 21 進行演示。

爲了方便開發,創建一個基礎的 Spring Boot 項目後,添加以下依賴。

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- 從 Hibernate 6 開始,支持 SQLite 方言。-->
<!-- https://mvnrepository.com/artifact/org.hibernate.orm/hibernate-community-dialects -->
<dependency>
	<groupId>org.hibernate.orm</groupId>
	<artifactId>hibernate-community-dialects</artifactId>
	<version>6.4.3.Final</version>
</dependency>
<!-- sqlite jdbc 驅動 -->
<dependency>
	<groupId>org.xerial</groupId>
	<artifactId>sqlite-jdbc</artifactId>
	<version>3.45.1.0</version>
</dependency>
<!-- Lombok 簡化 get set tostring log .. -->
<dependency>
	<groupId>org.projectlombok</groupId>
	<artifactId>lombok</artifactId>
	<optional>true</optional>
</dependency>
<!-- apache java 通用工具庫 -->
<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-lang3</artifactId>
	<version>3.13.0</version>
</dependency>
<!-- 編碼通用工具庫 -->
<dependency>
	<groupId>commons-codec</groupId>
	<artifactId>commons-codec</artifactId>
	<version>1.16.0</version>
</dependency>

配置 SQLite & JPA

在 Spring Boot 中,對 SQLite 的配置非常簡單,只需要指定一個位置存放 SQLite 數據庫文件。SQLite 無服務端,因此可以直接啓動。

spring.datasource.url=jdbc:sqlite:springboot-sqlite-jpa.db
spring.datasource.driver-class-name=org.sqlite.JDBC
# JPA Properties
spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect
# create 每次都重新創建表,update,表若存在則不重建
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

配置實體映射

在使用 JPA 開發時,就是使用 jakarta.persistence 包中的註解配置 Java 實體類和表的映射關係,比如使用 @Table 指定表名,使用 @Column 配置字段信息。

import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Entity
@Getter
@Setter
@ToString
@Table(name = "website_user")
public class WebsiteUser {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(name = "username", nullable = false, unique = true, length = 64)
    private String username;

    @Column(name = "password", nullable = false, length = 255)
    private String password;

    @Column(name = "salt", nullable = false, length = 16)
    private String salt;

    @Column(name = "status", nullable = false, length = 16, columnDefinition = "VARCHAR(16) DEFAULT 'active'")
    private String status;

    @Column(name = "created_at", nullable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
    private LocalDateTime createdAt;
  
    @Column(name = "updated_at", nullable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
    private LocalDateTime updatedAt;
}

編寫 JPA 查詢方法

Spring Data JPA 提供了多種便捷的方法來實現對數據庫的查詢操作,使得能夠以非常簡潔的方式編寫對數據庫的訪問和查詢邏輯。比如 Spring Data JPA 允許通過在接口中定義遵循一定命名方法的方式來創建數據庫查詢。如findByName 將生成一個根據 name 查詢指定實體的 SQL。

代碼示例:

@Repository
public interface WebsiteUserRepository extends CrudRepository<WebsiteUser, Long> {

    /**
     * 根據 username 查詢數據
     * @param name
     * @return
     */
    WebsiteUser findByUsername(String name);
}

代碼示例中,繼承的 CrudRepository 接口中包含了常見的 CURD 操作方法。自定義的 findByUsername 方法可以根據 WebsiteUser 中的 Username 進行查詢。

編寫 Controller

編寫三個 API 用來演示 Spring Boot 結合 SQLite 以及 JPA 是否成功。

初始化方法 init()

  • 映射到 "/sqlite/init" 的 GET請求。

  • 創建了10個 WebsiteUser 實體,爲每個用戶生成隨機的用戶名和鹽值,並用MD5加密其密碼("123456" + 鹽)。

  • 用戶信息包括用戶名、加鹽後的密碼、創建和更新的時間戳,以及用戶狀態。

  • 用戶信息被保存到數據庫中,並記錄日誌。

查找用戶方法 findByUsername(String username)

  • 映射到 "/sqlite/find" 的GET請求。
  • 通過用戶名查詢用戶。如果找到,返回用戶的字符串表示;否則返回 null

登錄方法 findByUsername(String username, String password)

  • 映射到 "/sqlite/login" 的GET請求。
  • 驗證傳入的用戶名和密碼。首先通過用戶名查詢用戶,然後將傳入的密碼與鹽值結合,並與數據庫中存儲的加鹽密碼進行MD5加密比對。
  • 如果密碼匹配,則認證成功,返回 "login succeeded";否則,返回 "login failed"。

代碼示例:

import java.time.LocalDateTime;
import com.wdbyte.springsqlite.model.WebsiteUser;
import com.wdbyte.springsqlite.repository.WebsiteUserRepository;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * @author https://www.wdbyte.com
 */
@Slf4j
@RestController
public class SqliteController {

    @Autowired
    private WebsiteUserRepository userRepository;

    @GetMapping("/sqlite/init")
    public String init() {
        for (int i = 0; i < 10; i++) {
            WebsiteUser websiteUser = new WebsiteUser();
            // 隨機4個字母
            websiteUser.setUsername(RandomStringUtils.randomAlphabetic(4));
            // 隨機16個字符用於密碼加鹽加密
            websiteUser.setSalt(RandomStringUtils.randomAlphanumeric(16));
            String password = "123456";
            // 密碼存儲 = md5(密碼+鹽)
            password = password + websiteUser.getSalt();
            websiteUser.setPassword(DigestUtils.md5Hex(password));
            websiteUser.setCreatedAt(LocalDateTime.now());
            websiteUser.setUpdatedAt(LocalDateTime.now());
            websiteUser.setStatus("active");
            WebsiteUser saved = userRepository.save(websiteUser);
            log.info("init user {}", saved.getUsername());
        }
        return "init success";
    }

    @GetMapping("/sqlite/find")
    public String findByUsername(String username) {
        WebsiteUser websiteUser = userRepository.findByUsername(username);
        if (websiteUser == null) {
            return null;
        }
        return websiteUser.toString();
    }

    @GetMapping("/sqlite/login")
    public String findByUsername(String username, String password) {
        WebsiteUser websiteUser = userRepository.findByUsername(username);
        if (websiteUser == null) {
            return "login failed";
        }
        password = password + websiteUser.getSalt();
        if (StringUtils.equals(DigestUtils.md5Hex(password), websiteUser.getPassword())) {
            return "login succeeded";
        } else {
            return "login failed";
        }
    }
}

至此,項目編寫完成,完整目錄結構如下:

├── pom.xml
└── src
    ├── main
        ├── java
        │   └── com
        │       └── wdbyte
        │           └── springsqlite
        │               ├── SpringBootSqliteApp.java
        │               ├── controller
        │               │   └── SqliteController.java
        │               ├── model
        │               │   └── WebsiteUser.java
        │               └── repository
        │                   └── WebsiteUserRepository.java
        └── resources
            ├── application.properties
            ├── static
            └── templates

啓動測試

Spring Boot 啓動時由於庫表不存在,自動創建庫表:

Hibernate: create table website_user (id integer, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP not null, password varchar(255) not null, salt varchar(16) not null, status VARCHAR(16) DEFAULT 'active' not null, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP not null, username varchar(64) not null unique, primary key (id))
Hibernate: alter table website_user drop constraint UK_61p1pfkd4ht22uhlib72oj301
2024-02-27T20:00:21.279+08:00  INFO 70956 --- [main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2024-02-27T20:00:21.578+08:00  WARN 70956 --- [main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2024-02-27T20:00:21.931+08:00  INFO 70956 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path ''
2024-02-27T20:00:21.938+08:00  INFO 70956 --- [main] c.w.springsqlite.SpringBootSqliteApp     : Started SpringBootSqliteApp in 3.944 seconds (process running for 5.061)

請求初始化接口

$ curl http://127.0.0.1:8080/sqlite/init
init success

可以看到輸出日誌成功寫入了 10條數據,且輸出了 username 值。

2024-02-27T20:01:04.120+08:00 ...SqliteController : init user HUyz
Hibernate: insert into website_user (created_at,password,salt,status,updated_at,username) values (?,?,?,?,?,?)
Hibernate: select last_insert_rowid()
2024-02-27T20:01:04.123+08:00 ...SqliteController : init user ifQU
Hibernate: insert into website_user (created_at,password,salt,status,updated_at,username) values (?,?,?,?,?,?)
Hibernate: select last_insert_rowid()
2024-02-27T20:01:04.126+08:00 ...SqliteController : init user GBPK
Hibernate: insert into website_user (created_at,password,salt,status,updated_at,username) values (?,?,?,?,?,?)
Hibernate: select last_insert_rowid()
2024-02-27T20:01:04.129+08:00 ...SqliteController : init user rytE
Hibernate: insert into website_user (created_at,password,salt,status,updated_at,username) values (?,?,?,?,?,?)
Hibernate: select last_insert_rowid()
2024-02-27T20:01:04.132+08:00 ...SqliteController : init user iATH
Hibernate: insert into website_user (created_at,password,salt,status,updated_at,username) values (?,?,?,?,?,?)
Hibernate: select last_insert_rowid()
2024-02-27T20:01:04.134+08:00 ...SqliteController : init user ZQRW
Hibernate: insert into website_user (created_at,password,salt,status,updated_at,username) values (?,?,?,?,?,?)
Hibernate: select last_insert_rowid()
2024-02-27T20:01:04.137+08:00 ...SqliteController : init user cIPM
Hibernate: insert into website_user (created_at,password,salt,status,updated_at,username) values (?,?,?,?,?,?)
Hibernate: select last_insert_rowid()
2024-02-27T20:01:04.140+08:00 ...SqliteController : init user MemS
Hibernate: insert into website_user (created_at,password,salt,status,updated_at,username) values (?,?,?,?,?,?)
Hibernate: select last_insert_rowid()
2024-02-27T20:01:04.143+08:00 ...SqliteController : init user GEeX
Hibernate: insert into website_user (created_at,password,salt,status,updated_at,username) values (?,?,?,?,?,?)
Hibernate: select last_insert_rowid()
2024-02-27T20:01:04.146+08:00 ...SqliteController : init user ZQrT

請求查詢用戶接口

$ curl http://127.0.0.1:8080/sqlite/find\?username\=ZQrT
WebsiteUser(id=10, username=ZQrT, password=538ea3b5fbacd1f9354a1f367b36135a, salt=RxaivBHlyJCxtOEv, status=active, createdAt=2024-02-27T20:01:04.144, updatedAt=2024-02-27T20:01:04.144)

查詢成功,回顯了查詢到的用戶信息。

請求登錄接口

在初始化數據時,密碼統一配置爲 123456,下面的測試可以看到使用正確的密碼可以通過校驗。

$ curl http://127.0.0.1:8080/sqlite/login\?username\=ZQrT\&password\=123456
login succeeded
$ curl http://127.0.0.1:8080/sqlite/login\?username\=ZQrT\&password\=12345
login failed

SQLite 3 數據審查

使用 Sqlite3 命令行工具查看 SQLite 數據庫內容。

$ ./sqlite3 springboot-sqlite-jpa.db
SQLite version 3.42.0 2023-05-16 12:36:15
Enter ".help" for usage hints.
sqlite> .tables
website_user

sqlite> .mode table
sqlite> select * from website_user;
+----+---------------+----------------------------------+------------------+--------+---------------+----------+
| id |  created_at   |             password             |       salt       | status |  updated_at   | username |
+----+---------------+----------------------------------+------------------+--------+---------------+----------+
| 1  | 1709035264074 | 4b2b68c0df77669540fc0a487d753400 | 1njFP8ykWmlu01Z8 | active | 1709035264074 | HUyz     |
| 2  | 1709035264120 | 7e6444d57f753cfa6c1592a17e68e66e | 9X3El5jQaMhrROSf | active | 1709035264120 | ifQU     |
| 3  | 1709035264124 | 1d24c4ddb351eb56f665adb13708f981 | Jn9IrT6MYqVqzpu8 | active | 1709035264124 | GBPK     |
| 4  | 1709035264126 | 960747cc48aeed71e8ff714deae42e87 | wq8pb1G9pIalGHwP | active | 1709035264126 | rytE     |
| 5  | 1709035264129 | cf1037b95a997a1b1b9d9aa598b9f96b | An0hwV2n9cN4wpOy | active | 1709035264129 | iATH     |
| 6  | 1709035264132 | b68d42108e5046bd25b74cda947e0ffc | EozfDAkpn5Yx4yin | active | 1709035264132 | ZQRW     |
| 7  | 1709035264134 | 78d4841af9a12603204f077b9bf30dcc | 2FRNQ2zWksJHOyX9 | active | 1709035264135 | cIPM     |
| 8  | 1709035264137 | 60b8051ca3379c569a3fb41ed5ff05aa | KpT3IGwWmhlWIUq7 | active | 1709035264137 | MemS     |
| 9  | 1709035264140 | 0ca0a2dce442315c11f5488c0127f905 | RhGOYnNEMYbnWoat | active | 1709035264140 | GEeX     |
| 10 | 1709035264144 | 538ea3b5fbacd1f9354a1f367b36135a | RxaivBHlyJCxtOEv | active | 1709035264144 | ZQrT     |
+----+---------------+----------------------------------+------------------+--------+---------------+----------+
sqlite> 

一如既往,文章中代碼存放在 Github.com/niumoo/javaNotes.

參考

本文 Github.com/niumoo/JavaNotes倉庫已經收錄。
本文原發於網站:Spring Boot 整合 SQLite 和 JPA
本文原發於公衆號:三分鐘數據持久化:Spring Boot, JPA 與 SQLite 的完美融合

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章