欢迎使用 Exposed,一个面向 Kotlin 的 ORM 框架。
Exposed 是一个基于 Kotlin 编程语言数据库连接驱动的轻量级 SQL 库, 支持 JDBC 和 R2DBC(自 1.0.0-* 版本起)驱动。 它提供两种数据库访问方式:类型安全的 SQL 包装领域特定语言(DSL)以及轻量级的数据访问对象(DAO)API。
我们的官方吉祥物是乌贼,它以出色的拟态能力而闻名,能够无缝融入任何环境。 与我们的吉祥物类似,Exposed 可用于模拟多种数据库引擎,这有助于您构建不依赖于任何特定数据库引擎的应用程序,并且只需很少或无需更改即可在它们之间切换。
支持的数据库
- H2(2.x 版本)
(此外,还支持使用 pgjdbc-ng JDBC 驱动的 PostgreSQL)
依赖项
Exposed 的发布版本可在 Maven Central 仓库 中获取。 有关如何配置此仓库以及如何将 Exposed 依赖项添加到现有 Gradle/Maven 项目的详细信息, 请参阅完整的 模块指南。
Exposed 模块
Exposed 由以下核心模块组成:
| 模块 | 功能 |
|---|---|
exposed-core | 提供以类型安全方式操作数据库所需的基础组件和抽象,并包含领域特定语言(DSL)API |
exposed-dao | (可选)允许您使用数据访问对象(DAO)API。 它仅与 exposed-jdbc 兼容,不适用于 exposed-r2dbc。 |
exposed-jdbc | 提供基于 Java JDBC API 的传输层实现,以支持 Java 数据库连接(JDBC) |
exposed-r2dbc | 提供对响应式关系数据库连接(R2DBC)的支持 |
以及以下扩展模块:
| Module | Function |
|---|---|
exposed-crypt | 提供额外的列类型,用于在数据库中存储加密数据,并在客户端对其进行编码/解码 |
exposed-java-time | 基于 Java 8 Time API 的日期时间扩展 |
exposed-jodatime | 基于 Joda-Time 库的日期时间扩展 |
exposed-json | JSON 和 JSONB 数据类型扩展 |
exposed-kotlin-datetime | 基于 kotlinx-datetime 库的日期时间扩展 |
exposed-migration-core | 为数据库架构迁移提供核心通用功能 |
exposed-migration-jdbc | 提供用于支持数据库架构迁移的工具,依赖于 JDBC 驱动程序 |
exposed-migration-r2dbc | 提供用于支持数据库架构迁移的工具,依赖于 R2DBC 驱动程序 |
exposed-money | 用于支持 MonetaryAmount 的扩展,基于 JavaMoney API |
exposed-spring-boot-starter | 用于在 Spring Boot 3 中将 Exposed 用作 ORM 的启动器 |
exposed-spring-boot4-starter | 用于在 Spring Boot 4 中将 Exposed 用作 ORM 的启动器 |
spring-transaction | 基于 Spring Framework 6 标准事务工作流构建的事务管理器 |
spring7-transaction | 基于 Spring Framework 7 标准事务工作流构建的事务管理器 |
要求
Kotlin 版本 2.2.+
以下模块需要 JDK 17 或更高版本:
spring-transaction- 依赖于 Spring Framework 6spring7-transaction- 依赖于 Spring Framework 7exposed-spring-boot-starter- 依赖于 Spring Boot 3exposed-spring-boot4-starter- 依赖于 Spring Boot 4exposed-crypt- 依赖于 Spring Security 7
以下模块需要 JDK 11 或更高版本:
exposed-r2dbcexposed-migration-r2dbc
所有其他模块的最低要求为 JDK 8。
使用 Exposed 的示例
请遵循 DSL 入门教程 快速开始,或查看 示例 以了解更深入的项目。
文档
有关完整的文档、示例和教程,请参阅以下链接:
贡献
报告问题
我们鼓励您以任何形式提供反馈,例如功能请求、错误报告、文档更新和问题。
请使用 我们的问题跟踪器 报告任何问题或记录新请求。
虽然问题对公众可见,但创建新问题或评论现有问题都需要登录 YouTrack。
提交拉取请求
我们积极欢迎您的拉取请求,并鼓励您将您的工作链接到 现有问题。
有关更多详细信息,请参阅完整的 贡献指南。
通过为 Exposed 项目做出贡献,您同意您的贡献将根据 Apache License, Version 2.0 进行许可。
支持
有问题或想参与讨论?请加入我们在 Kotlin Slack 上的 #exposed 频道。 如果你还不是成员,可以申请邀请。
示例
SQL DSL
import org.jetbrains.exposed.v1.core.*
import org.jetbrains.exposed.v1.core.SqlExpressionBuilder.like
import org.jetbrains.exposed.v1.jdbc.*
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
object Cities : Table() {
val id = integer("id").autoIncrement()
val name = varchar("name", 50)
override val primaryKey = PrimaryKey(id)
}
object Users : Table() {
val id = varchar("id", 10)
val name = varchar("name", length = 50)
val cityId = integer("city_id").references(Cities.id).nullable()
override val primaryKey = PrimaryKey(id, name = "PK_User_ID")
}
fun main() {
Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver", user = "root", password = "")
transaction {
addLogger(StdOutSqlLogger)
SchemaUtils.create(Cities, Users)
val saintPetersburgId = Cities.insert {
it[name] = "St. Petersburg"
} get Cities.id
val munichId = Cities.insert {
it[name] = "Munich"
} get Cities.id
val pragueId = Cities.insert {
it.update(name, stringLiteral(" Prague ").trim().substring(1, 2))
}[Cities.id]
val pragueName = Cities
.selectAll()
.where { Cities.id eq pragueId }
.single()[Cities.name]
println("pragueName = $pragueName")
Users.insert {
it[id] = "andrey"
it[name] = "Andrey"
it[cityId] = saintPetersburgId
}
Users.insert {
it[id] = "sergey"
it[name] = "Sergey"
it[cityId] = munichId
}
Users.insert {
it[id] = "eugene"
it[name] = "Eugene"
it[cityId] = munichId
}
Users.insert {
it[id] = "alex"
it[name] = "Alex"
it[cityId] = null
}
Users.insert {
it[id] = "smth"
it[name] = "Something"
it[cityId] = null
}
Users.update(where = { Users.id eq "alex" }) {
it[name] = "Alexey"
}
Users.deleteWhere { Users.name like "%thing" }
println("All cities:")
Cities
.selectAll()
.forEach { result ->
println("${result[Cities.id]}: ${result[Cities.name]}")
}
println("Manual join:")
(Users innerJoin Cities)
.select(Users.name, Cities.name)
.where {
(Users.id.eq("andrey") or Users.name.eq("Sergey")) and
Users.id.eq("sergey") and Users.cityId.eq(Cities.id)
}.forEach { result ->
println("${result[Users.name]} lives in ${result[Cities.name]}")
}
println("Join with foreign key:")
(Users innerJoin Cities)
.select(Users.name, Users.cityId, Cities.name)
.where { Cities.name.eq("St. Petersburg") or Users.cityId.isNull() }
.forEach { result ->
if (result[Users.cityId] != null) {
println("${result[Users.name]} lives in ${result[Cities.name]}")
} else {
println("${result[Users.name]} lives nowhere")
}
}
println("Functions and group by:")
(Cities innerJoin Users)
.select(Cities.name, Users.id.count())
.groupBy(Cities.name)
.forEach { result ->
val cityName = result[Cities.name]
val userCount = result[Users.id.count()]
if (userCount > 0) {
println("$userCount user(s) live(s) in $cityName")
} else {
println("Nobody lives in $cityName")
}
}
SchemaUtils.drop(Users, Cities)
}
}
生成的 SQL:
SQL: CREATE TABLE IF NOT EXISTS CITIES (ID INT AUTO_INCREMENT PRIMARY KEY, "name" VARCHAR(50) NOT NULL)
SQL: CREATE TABLE IF NOT EXISTS USERS (ID VARCHAR(10), "name" VARCHAR(50) NOT NULL, CITY_ID INT NULL, CONSTRAINT PK_User_ID PRIMARY KEY (ID), CONSTRAINT FK_USERS_CITY_ID__ID FOREIGN KEY (CITY_ID) REFERENCES CITIES(ID) ON DELETE RESTRICT ON UPDATE RESTRICT)
SQL: INSERT INTO CITIES ("name") VALUES ('St. Petersburg')
SQL: INSERT INTO CITIES ("name") VALUES ('Munich')
SQL: INSERT INTO CITIES ("name") VALUES (SUBSTRING(TRIM(' Prague '), 1, 2))
SQL: SELECT CITIES.ID, CITIES."name" FROM CITIES WHERE CITIES.ID = 3
pragueName = Pr
SQL: INSERT INTO USERS (ID, "name", CITY_ID) VALUES ('andrey', 'Andrey', 1)
SQL: INSERT INTO USERS (ID, "name", CITY_ID) VALUES ('sergey', 'Sergey', 2)
SQL: INSERT INTO USERS (ID, "name", CITY_ID) VALUES ('eugene', 'Eugene', 2)
SQL: INSERT INTO USERS (ID, "name", CITY_ID) VALUES ('alex', 'Alex', NULL)
SQL: INSERT INTO USERS (ID, "name", CITY_ID) VALUES ('smth', 'Something', NULL)
SQL: UPDATE USERS SET "name"='Alexey' WHERE USERS.ID = 'alex'
SQL: DELETE FROM USERS WHERE USERS."name" LIKE '%thing'
All cities:
SQL: SELECT CITIES.ID, CITIES."name" FROM CITIES
1: St. Petersburg
2: Munich
3: Pr
Manual join:
SQL: SELECT USERS."name", CITIES."name" FROM USERS INNER JOIN CITIES ON CITIES.ID = USERS.CITY_ID WHERE ((USERS.ID = 'andrey') OR (USERS."name" = 'Sergey')) AND (USERS.ID = 'sergey') AND (USERS.CITY_ID = CITIES.ID)
Sergey lives in Munich
Join with foreign key:
SQL: SELECT USERS."name", USERS.CITY_ID, CITIES."name" FROM USERS INNER JOIN CITIES ON CITIES.ID = USERS.CITY_ID WHERE (CITIES."name" = 'St. Petersburg') OR (USERS.CITY_ID IS NULL)
Andrey lives in St. Petersburg
Functions and group by:
SQL: SELECT CITIES."name", COUNT(USERS.ID) FROM CITIES INNER JOIN USERS ON CITIES.ID = USERS.CITY_ID GROUP BY CITIES."name"
2 user(s) live(s) in Munich
1 user(s) live(s) in St. Petersburg
SQL: DROP TABLE IF EXISTS USERS
SQL: DROP TABLE IF EXISTS CITIES
数据访问对象
import org.jetbrains.exposed.v1.core.StdOutSqlLogger
import org.jetbrains.exposed.v1.core.dao.id.*
import org.jetbrains.exposed.v1.dao.*
import org.jetbrains.exposed.v1.jdbc.*
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
object Cities: IntIdTable() {
val name = varchar("name", 50)
}
object Users : IntIdTable() {
val name = varchar("name", length = 50).index()
val city = reference("city", Cities)
val age = integer("age")
}
class City(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<City>(Cities)
var name by Cities.name
val users by User referrersOn Users.city
}
class User(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<User>(Users)
var name by Users.name
var city by City referencedOn Users.city
var age by Users.age
}
fun main() {
Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver", user = "root", password = "")
transaction {
addLogger(StdOutSqlLogger)
val saintPetersburg = City.new {
name = "St. Petersburg"
}
val munich = City.new {
name = "Munich"
}
User.new {
name = "Andrey"
city = saintPetersburg
age = 5
}
User.new {
name = "Sergey"
city = saintPetersburg
age = 27
}
User.new {
name = "Eugene"
city = munich
age = 42
}
val alex = User.new {
name = "alex"
city = munich
age = 11
}
alex.name = "Alexey"
println("Cities: ${City.all().joinToString { it.name }}")
println("Users in ${saintPetersburg.name}: ${saintPetersburg.users.joinToString { it.name }}")
println("Adults: ${User.find { Users.age greaterEq 18 }.joinToString { it.name }}")
SchemaUtils.drop(Users, Cities)
}
}
生成的 SQL:
SQL: CREATE TABLE IF NOT EXISTS CITIES (ID INT AUTO_INCREMENT PRIMARY KEY, "name" VARCHAR(50) NOT NULL)
SQL: CREATE TABLE IF NOT EXISTS USERS (ID INT AUTO_INCREMENT PRIMARY KEY, "name" VARCHAR(50) NOT NULL, CITY INT NOT NULL, AGE INT NOT NULL, CONSTRAINT FK_USERS_CITY__ID FOREIGN KEY (CITY) REFERENCES CITIES(ID) ON DELETE RESTRICT ON UPDATE RESTRICT)
SQL: CREATE INDEX USERS_NAME ON USERS ("name")
SQL: INSERT INTO CITIES ("name") VALUES ('St. Petersburg')
SQL: INSERT INTO CITIES ("name") VALUES ('Munich')
SQL: SELECT CITIES.ID, CITIES."name" FROM CITIES
Cities: St. Petersburg, Munich
SQL: INSERT INTO USERS ("name", CITY, AGE) VALUES ('Andrey', 1, 5)
SQL: INSERT INTO USERS ("name", CITY, AGE) VALUES ('Sergey', 1, 27)
SQL: INSERT INTO USERS ("name", CITY, AGE) VALUES ('Eugene', 2, 42)
SQL: INSERT INTO USERS ("name", CITY, AGE) VALUES ('Alexey', 2, 11)
SQL: SELECT USERS.ID, USERS."name", USERS.CITY, USERS.AGE FROM USERS WHERE USERS.CITY = 1
Users in St. Petersburg: Andrey, Sergey
SQL: SELECT USERS.ID, USERS."name", USERS.CITY, USERS.AGE FROM USERS WHERE USERS.AGE >= 18
Adults: Sergey, Eugene
SQL: DROP TABLE IF EXISTS USERS
SQL: DROP TABLE IF EXISTS CITIES