三分鐘數(shù)據(jù)持久化:Spring Boot, JPA 與 SQLite 的完美融合
在快節(jié)奏的軟件開發(fā)領(lǐng)域,每一個(gè)簡化工作流程的機(jī)會(huì)都不容錯(cuò)過。想要一個(gè)無需繁瑣配置、能夠迅速啟動(dòng)的數(shù)據(jù)持久化方案嗎?這篇文章將是你的首選攻略。在這里,我們將向你展示如何將 Spring Boot 的便捷性、JPA 的強(qiáng)大查詢能力和 SQLite 的輕量級(jí)特性結(jié)合在一起,實(shí)現(xiàn)快速而又優(yōu)雅的數(shù)據(jù)管理。
為什么選擇 SQLite
SQLite 是一個(gè)用 C 語言編寫的開源、輕量級(jí)、快速、獨(dú)立且高可靠性的 SQL 數(shù)據(jù)庫引擎,它提供了功能齊全的數(shù)據(jù)庫解決方案。對于大多數(shù)的應(yīng)用,SQLite 都可以滿足。使用 SQLite 可以零配置啟動(dòng),對于小型應(yīng)用或者快速原型設(shè)計(jì)是一個(gè)非常大的優(yōu)勢。
使用 SQLite 具有下面幾個(gè)優(yōu)點(diǎn):
- 1. 輕量級(jí):SQLite很小巧,不需要獨(dú)立服務(wù)器,便于集成到應(yīng)用中。
- 2. 零配置:啟用 SQLite 無需復(fù)雜配置,只需指定一個(gè)文件路徑存放 DB 文件,簡化了數(shù)據(jù)庫的設(shè)置流程。
- 3. 便于移植:數(shù)據(jù)庫是單一文件,方便備份和在不同環(huán)境間遷移。
- 4. 跨平臺(tái):SQLite 支持各種操作系統(tǒng),容易實(shí)現(xiàn)應(yīng)用的跨平臺(tái)運(yùn)行。
- 5. 性能良好:對于小型應(yīng)用,SQLite 提供足夠的讀寫性能。
- 6. 遵循ACID:SQLite事務(wù)符合ACID原則,數(shù)據(jù)操作可靠。
- 7. 社區(qū)支持:雖然簡單,但擁有強(qiáng)大的社區(qū)和廣泛的文檔資源。
之前寫過一篇 SQLite 入門教程 (https://www.wdbyte.com/db/sqlite/)[1],感情的同學(xué)可以參考。
為什么 選擇 JPA
Spring Data JPA 是Spring Data項(xiàng)目的一部分,旨在簡化基于JPA(Java Persistence API)的數(shù)據(jù)訪問層(Repository層)的實(shí)現(xiàn)。JPA是一種 ORM(對象關(guān)系映射)規(guī)范,它允許開發(fā)者以面向?qū)ο蟮姆绞絹聿僮鲾?shù)據(jù)庫,
通常應(yīng)用程序?qū)崿F(xiàn)數(shù)據(jù)訪問層可能非常麻煩,必須編寫太多的樣板代碼才能實(shí)現(xiàn)簡單的查詢,更不用說分頁等其他操作,而 Spring Data JPA 可以讓開發(fā)者非常容易地實(shí)現(xiàn)對數(shù)據(jù)庫的各種操作,顯著減少實(shí)際需要的工作量。
詳細(xì)介紹 JPA 并不是本文目的,關(guān)于 JPA 的更多內(nèi)容可以訪問:
- 1. Spring Data JPA 官網(wǎng):https://spring.io/projects/spring-data-jpa[2]。
- 2. Spring Boot 使用 Spring Data JPA[3]
創(chuàng)建 Spring Boot 項(xiàng)目
用于后續(xù)演示,首先創(chuàng)建一個(gè)簡單的 Spring Boot 項(xiàng)目。你可以自由創(chuàng)建,或者使用 Spring 官網(wǎng)提供的快速創(chuàng)建工具:https://start.spring.io/[4]
注意,文章示例項(xiàng)目使用 Java 21 進(jìn)行演示。
為了方便開發(fā),創(chuàng)建一個(gè)基礎(chǔ)的 Spring Boot 項(xiàng)目后,添加以下依賴。
<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 驅(qū)動(dòng) -->
<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 的配置非常簡單,只需要指定一個(gè)位置存放 SQLite 數(shù)據(jù)庫文件。SQLite 無服務(wù)端,因此可以直接啟動(dòng)。
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 每次都重新創(chuàng)建表,update,表若存在則不重建
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
配置實(shí)體映射
在使用 JPA 開發(fā)時(shí),就是使用 jakarta.persistence 包中的注解配置 Java 實(shí)體類和表的映射關(guān)系,比如使用 @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 提供了多種便捷的方法來實(shí)現(xiàn)對數(shù)據(jù)庫的查詢操作,使得能夠以非常簡潔的方式編寫對數(shù)據(jù)庫的訪問和查詢邏輯。比如 Spring Data JPA 允許通過在接口中定義遵循一定命名方法的方式來創(chuàng)建數(shù)據(jù)庫查詢。如findByName 將生成一個(gè)根據(jù) name 查詢指定實(shí)體的 SQL。
代碼示例:
@Repository
public interface WebsiteUserRepository extends CrudRepository<WebsiteUser, Long> {
/**
* 根據(jù) username 查詢數(shù)據(jù)
* @param name
* @return
*/
WebsiteUser findByUsername(String name);
}
代碼示例中,繼承的 CrudRepository 接口中包含了常見的 CURD 操作方法。自定義的 findByUsername 方法可以根據(jù) WebsiteUser 中的 Username 進(jìn)行查詢。
編寫 Controller
編寫三個(gè) API 用來演示 Spring Boot 結(jié)合 SQLite 以及 JPA 是否成功。
初始化方法 init():
- ? 映射到 "/sqlite/init" 的 GET請求。
- ? 創(chuàng)建了10個(gè) WebsiteUser 實(shí)體,為每個(gè)用戶生成隨機(jī)的用戶名和鹽值,并用MD5加密其密碼("123456" + 鹽)。
- ? 用戶信息包括用戶名、加鹽后的密碼、創(chuàng)建和更新的時(shí)間戳,以及用戶狀態(tài)。
- ? 用戶信息被保存到數(shù)據(jù)庫中,并記錄日志。
查找用戶方法 findByUsername(String username):
- ? 映射到 "/sqlite/find" 的GET請求。
- ? 通過用戶名查詢用戶。如果找到,返回用戶的字符串表示;否則返回 null。
登錄方法 findByUsername(String username, String password):
- ? 映射到 "/sqlite/login" 的GET請求。
- ? 驗(yàn)證傳入的用戶名和密碼。首先通過用戶名查詢用戶,然后將傳入的密碼與鹽值結(jié)合,并與數(shù)據(jù)庫中存儲(chǔ)的加鹽密碼進(jìn)行MD5加密比對。
- ? 如果密碼匹配,則認(rèn)證成功,返回 "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();
// 隨機(jī)4個(gè)字母
websiteUser.setUsername(RandomStringUtils.randomAlphabetic(4));
// 隨機(jī)16個(gè)字符用于密碼加鹽加密
websiteUser.setSalt(RandomStringUtils.randomAlphanumeric(16));
String password = "123456";
// 密碼存儲(chǔ) = 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";
}
}
}
至此,項(xiàng)目編寫完成,完整目錄結(jié)構(gòu)如下:
├── pom.xml
└── src
├── main
├── java
│ └── com
│ └── wdbyte
│ └── springsqlite
│ ├── SpringBootSqliteApp.java
│ ├── controller
│ │ └── SqliteController.java
│ ├── model
│ │ └── WebsiteUser.java
│ └── repository
│ └── WebsiteUserRepository.java
└── resources
├── application.properties
├── static
└── templates
啟動(dòng)測試
Spring Boot 啟動(dòng)時(shí)由于庫表不存在,自動(dòng)創(chuàng)建庫表:
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條數(shù)據(jù),且輸出了 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)
查詢成功,回顯了查詢到的用戶信息。
請求登錄接口
在初始化數(shù)據(jù)時(shí),密碼統(tǒng)一配置為 123456,下面的測試可以看到使用正確的密碼可以通過校驗(yàn)。
$ 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 數(shù)據(jù)審查
使用 Sqlite3 命令行工具查看 SQLite 數(shù)據(jù)庫內(nèi)容。
$ ./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[5].
參考
? https://docs.spring.io/spring-data/jpa/reference/jpa.html
引用鏈接
[1] SQLite 入門教程 (https://www.wdbyte.com/db/sqlite/): https://www.wdbyte.com/db/sqlite/
[2] Spring Data JPA 官網(wǎng):https://spring.io/projects/spring-data-jpa: https://spring.io/projects/spring-data-jpa
[3] Spring Boot 使用 Spring Data JPA: https://www.wdbyte.com/2019/03/springboot/springboot-10-data-jpa
[4] https://start.spring.io/: https://start.spring.io/#!type=maven-project&language=java&platformVersion=3.2.3&packaging=jar&jvmVersion=21&groupId=com.example&artifactId=springboot-sqlite-jpa&name=springboot-sqlite-jpa&description=Demo project for Spring Boot&packageName=com.example.springboot-sqlite-jpa&dependencies=web,lombok,data-jpa
[5] Github.com/niumoo/javaNotes: https://github.com/niumoo/JavaNotes/tree/master/springboot/springboot-sqlite-jpa