尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Exposed:面向 Kotlin 的轻量级 ORM 框架全面指南

Exposed:面向 Kotlin 的轻量级 ORM 框架全面指南 ORM后端数据存储【免费下载链接】ExposedKotlin SQL Framework项目地址https://gitcode.com/gh_mirrors/ex/Exposed点击查看免费下载Exposed 是 JetBrains 官方开源的 Kotlin SQL 框架ORM它以数据库驱动为基础为 Kotlin 语言提供了 JDBC 与 R2DBC 双通道支持R2DBC 自 1.0.0-* 版本起引入并通过类型安全的 SQL 包装 DSL与轻量级 DAO API两种方式访问数据库。本文将完整梳理 Exposed 的模块组成、支持数据库、运行环境要求与依赖接入方式并逐行讲解官方 README 中完整的 DSL 与 DAO 实战示例及它们各自生成的 SQL帮助你快速上手并理解其底层执行语义。一、框架定位与设计理念Exposed 的定位是“构建在数据库连接驱动之上、为 Kotlin 语言设计的轻量级 SQL 库”。与许多重量级 ORM 不同它不追求把数据库完全抽象成对象模型而是提供两条可选的访问路径DSL APIDomain-Specific Language以类型安全的方式包装 SQL允许你像写 SQL 一样写查询但获得编译期的列名、表名与类型检查。DAO APIData Access Object一种轻量级对象关系映射方式将表行映射为 Kotlin 对象适合以面向对象思维操作数据。官方将框架的吉祥物定为墨鱼cuttlefish寓意其出色的“拟态”能力正如墨鱼能融入任何环境Exposed 可以“模仿”多种数据库引擎让应用不依赖特定数据库引擎从而在切换数据库时几乎无需改动代码——这正是框架多数据库方言支持vendor dialect设计的核心理念。二、支持的数据库当前仓库README 第 31-41 行明确列出的支持数据库如下数据库说明H2版本 2.x常用于本地开发与测试MariaDB基于 MariaDB Connector/JMySQL基于 MySQL Connector/JOracle基于 Oracle JDBC 驱动PostgreSQL基于 PostgreSQL JDBC 驱动同时支持 pgjdbc-ng 驱动Amazon Redshift仅支持 JDBCMicrosoft SQL Server基于 mssql-jdbcSQLite基于 xerial sqlite-jdbc从源码结构看每种数据库在 exposed-core 的 vendors 包 中都有对应的方言实现例如MysqlDialect.kt、PostgreSQL.kt、OracleDialect.kt、SQLServerDialect.kt、SQLiteDialect.kt、MariaDBDialect.kt、Redshift.kt、H2.kt它们负责将统一 API 翻译为各数据库特有的 SQL 语法这正是“以很少改动甚至零改动切换数据库”的能力来源。三、模块组成与依赖接入Exposed 的发布版本位于 Maven Central。完整的模块说明可参考仓库内 docs/exposed-modules.html 与官方模块指南。3.1 核心模块模块功能exposed-core提供以类型安全方式操作数据库所需的基础组件与抽象包含 DSL API。这是所有模块的地基对应源码目录 exposed-core内含Table.kt、Column.kt、Transaction.kt、AbstractQuery.kt、SchemaUtilityApi.kt等核心文件exposed-dao可选提供 DAO API。注意仅与exposed-jdbc兼容不支持exposed-r2dbcexposed-jdbc基于 Java JDBC API 的传输层实现对应 exposed-jdbcexposed-r2dbc提供响应式关系数据库连接R2DBC支持对应 exposed-r2dbc3.2 扩展模块模块功能exposed-crypt提供额外列类型用于在数据库中存储加密数据并在客户端编解码也支持单向哈希数据如密码exposed-java-time基于 Java 8 Time API 的日期时间扩展exposed-jodatime基于 Joda-Time 库的日期时间扩展exposed-jsonJSON 与 JSONB 数据类型扩展exposed-kotlin-datetime基于 kotlinx-datetime 库的日期时间扩展exposed-migration-core提供数据库 schema 迁移的通用核心功能exposed-migration-jdbc提供依赖 JDBC 驱动的 schema 迁移工具exposed-migration-r2dbc提供依赖 R2DBC 驱动的 schema 迁移工具exposed-money支持 JavaMoney API 的MonetaryAmount扩展exposed-spring-boot-starter面向 Spring Boot 3 的 starter可将 Exposed 作为 ORM 使用exposed-spring-boot4-starter面向 Spring Boot 4 的 starterspring-transaction基于 Spring Framework 6 标准事务流程构建的事务管理器spring7-transaction基于 Spring Framework 7 标准事务流程构建的事务管理器仓库根目录的 settings.gradle.kts 以多模块 Gradle 工程组织以上全部模块gradle/libs.versions.toml 维护统一的版本目录。3.3 环境要求README 明确给出了版本与 JDK 要求Kotlin 版本要求2.2.0 及以上README 写为 “Kotlin version 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。3.4 依赖配置示例以 GradleKotlin DSL为例在dependencies中加入核心模块与数据库驱动dependencies { implementation(org.jetbrains.exposed:exposed-core:1.0.0) implementation(org.jetbrains.exposed:exposed-jdbc:1.0.0) implementation(org.jetbrains.exposed:exposed-dao:1.0.0) // 如需 DAO API implementation(com.h2database:h2:2.2.224) // 以 H2 为例的 JDBC 驱动 }注意具体版本号请以 Maven Central 上当前发布版本为准README 只声明发布位置为 Maven Central未固定版本号。若使用 R2DBC 通道则应引入exposed-r2dbc与对应的 R2DBC 驱动并遵循 JDK 11 的要求。四、快速上手SQL DSL 实战以下完整示例取自 README 的 “SQL DSL” 一节演示了从建表、插入、更新、删除到查询、联表、聚合的完整 CRUD 流程可直接复制运行。4.1 定义表结构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) }要点说明Table()定义一个表表名默认由对象名解析去掉Table后缀也可在构造时显式传入。integer(id).autoIncrement()定义自增整数主键varchar(name, length)定义变长字符串列。.references(Cities.id)声明外键关系.nullable()允许该列存储NULL。通过override val primaryKey PrimaryKey(...)自定义主键DSL 示例中为Users表指定了具名主键约束PK_User_IDCities的主键则由自增id列构成。SqlExpressionBuilder.like的导入用于后续的LIKE条件构造。4.2 建立连接并开启事务fun main() { Database.connect(jdbc:h2:mem:test, driver org.h2.Driver, user root, password ) transaction { addLogger(StdOutSqlLogger) SchemaUtils.create(Cities, Users) // ... 增删改查 } }关键点Database.connect(url, driver, user, password)用于创建Database实例。从 exposed-jdbc 的 Database.kt 源码看connect提供了三种重载传入DataSource、传入getNewConnection: () - Connection、或传入url字符串此时driver默认为根据 url 自动匹配即getDriver(url)。注意该方法并不会立即建立真实连接而是记录连接细节直到事务真正需要连接时才建立。transaction { }开启一个事务作用域块内的所有操作在同一事务中执行。addLogger(StdOutSqlLogger)将生成的 SQL 输出到标准输出便于调试与学习。SchemaUtils.create(...)/SchemaUtils.drop(...)用于在数据库中创建 / 删除表其实现位于 exposed-core 的 SchemaUtilityApi.kt。4.3 插入数据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]insert { it[column] value }生成 INSERT 语句并可通过get column或[column]取回生成的主键等值依赖数据库的RETURNING或getGeneratedKeys能力。第三例展示了在插入时直接对值应用 SQL 函数stringLiteral( Prague ).trim().substring(1, 2)对应生成SUBSTRING(TRIM( Prague ), 1, 2)最终插入值为Pr。4.4 查询、更新与删除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 }selectAll().where { ... }.single()查询单条记录eq是 Exposed 的类型安全比较操作符。Users.update(where { ... }) { ... }按条件更新deleteWhere { ... }按条件删除。Users.name like %thing使用LIKE模式匹配删除name以 “thing” 结尾的记录此处删除 smth。4.5 联表查询与聚合// 手动联表 (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]}) } // 基于外键的联表 (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) } } // 函数与分组 (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) } }innerJoin是 Exposed DSL 提供的联表操作符or、and用于组合布尔条件isNull()判断空值。Users.id.count()是聚合函数COUNT与groupBy(Cities.name)配合实现分组统计。聚合函数定义在 exposed-core 的 Function.kt / FunctionBuilder.kt。行读取通过result[column]完成ResultRow提供类型安全的列值访问见 exposed-core 的 ResultRow.kt。4.6 DSL 示例生成的 SQLREADME 中给出了上述 DSL 代码在 H2 上生成的完整 SQL是理解各 API 底层语义的最佳教材节选CREATE TABLE IF NOT EXISTS CITIES (ID INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL) 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) INSERT INTO CITIES (name) VALUES (St. Petersburg) INSERT INTO CITIES (name) VALUES (Munich) INSERT INTO CITIES (name) VALUES (SUBSTRING(TRIM( Prague ), 1, 2)) SELECT CITIES.ID, CITIES.name FROM CITIES WHERE CITIES.ID 3 -- pragueName Pr INSERT INTO USERS (ID, name, CITY_ID) VALUES (andrey, Andrey, 1) INSERT INTO USERS (ID, name, CITY_ID) VALUES (sergey, Sergey, 2) INSERT INTO USERS (ID, name, CITY_ID) VALUES (eugene, Eugene, 2) INSERT INTO USERS (ID, name, CITY_ID) VALUES (alex, Alex, NULL) INSERT INTO USERS (ID, name, CITY_ID) VALUES (smth, Something, NULL) UPDATE USERS SET nameAlexey WHERE USERS.ID alex DELETE FROM USERS WHERE USERS.name LIKE %thing -- 全量查询 CITIES输出 1: St. Petersburg / 2: Munich / 3: Pr 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 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 / sergey lives nowhere / eugene lives nowhere / alex lives nowhere 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 / Nobody lives in Pr DROP TABLE IF EXISTS USERS DROP TABLE IF EXISTS CITIES可以观察到外键默认约束为ON DELETE RESTRICT ON UPDATE RESTRICTnullable()列生成NULL约束具名主键PK_User_ID被正确应用到建表语句中——所有 DSL 表达都精确映射为标准 SQL。五、DAO API 实战面向对象的数据库访问DAO API 建立在 DSL 之上将表行封装为 Kotlin 对象。以下完整示例取自 README 的 “DAO” 一节。5.1 定义表与实体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: EntityIDInt) : IntEntity(id) { companion object : IntEntityClassCity(Cities) var name by Cities.name val users by User referrersOn Users.city } class User(id: EntityIDInt) : IntEntity(id) { companion object : IntEntityClassUser(Users) var name by Users.name var city by City referencedOn Users.city var age by Users.age }要点说明IntIdTable()是带Int自增主键id的快捷表基类。从 IdTable.kt 源码可见其id列定义为integer(columnName).autoIncrement(sequenceName).entityId()且自动构成PrimaryKey(id)构造参数支持自定义列名默认id与序列名。reference(city, Cities)在Users表中创建指向Cities的外键列。.index()为列创建索引示例中生成CREATE INDEX USERS_NAME ON USERS (name)。实体类继承IntEntity(id)通过companion object : IntEntityClassCity(Cities)绑定对应表。属性通过委托绑定列var name by Cities.name读写即映射为对应列referrersOn/referencedOn分别定义一对多反向引用与多对一外键引用。5.2 CRUD 操作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) } }City.new { ... }/User.new { ... }创建并立即插入新记录块内直接为实体属性赋值。修改实体属性如alex.name Alexey后Exposed 会在事务提交时自动生成 UPDATE。City.all()返回全部实体User.find { Users.age greaterEq 18 }通过条件查询实体集合greaterEq对应。反向引用saintPetersburg.users会按需触发查询懒加载实现一对多导航。5.3 DAO 示例生成的 SQLCREATE TABLE IF NOT EXISTS CITIES (ID INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL) 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) CREATE INDEX USERS_NAME ON USERS (name) INSERT INTO CITIES (name) VALUES (St. Petersburg) INSERT INTO CITIES (name) VALUES (Munich) SELECT CITIES.ID, CITIES.name FROM CITIES -- Cities: St. Petersburg, Munich INSERT INTO USERS (name, CITY, AGE) VALUES (Andrey, 1, 5) INSERT INTO USERS (name, CITY, AGE) VALUES (Sergey, 1, 27) INSERT INTO USERS (name, CITY, AGE) VALUES (Eugene, 2, 42) INSERT INTO USERS (name, CITY, AGE) VALUES (Alexey, 2, 11) SELECT USERS.ID, USERS.name, USERS.CITY, USERS.AGE FROM USERS WHERE USERS.CITY 1 -- Users in St. Petersburg: Andrey, Sergey SELECT USERS.ID, USERS.name, USERS.CITY, USERS.AGE FROM USERS WHERE USERS.AGE 18 -- Adults: Sergey, Eugene DROP TABLE IF EXISTS USERS DROP TABLE IF EXISTS CITIES可以看到 DAO 层面的一切操作最终都被翻译为标准的 SQL 语句外键引用列CITY存储的是被引用实体的idfind的greaterEq条件生成WHERE USERS.AGE 18。这也印证了 DAO 是对 DSL 的轻量封装——两者共享同一套底层 SQL 生成机制。六、源码级补充常用 API 的底层实现以下是从当前仓库源码中可以确认的若干关键实现细节帮助理解框架内部机制表与列模型Table、Column、ColumnSet、FieldSet等核心抽象定义在 exposed-core/src/main/kotlin/org/jetbrains/exposed/v1/core/Table.kt共 2300 余行其中FieldSet.realFields会递归展开复合列CompositeColumn与复合主键CompositeIdTable的idColumns说明框架在生成 SQL 前会先对字段集做规范化。连接管理Database.connect的三类重载DataSource / 连接工厂函数 / JDBC URL位于 exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/v1/jdbc/Database.kt且文档注释明确“不立即建立连接事务需要时才连接”。事务模型transaction {}、TransactionManager等定义于 exposed-core 的 transactions 包 与 exposed-jdbc 的 transactions 包支持线程局部事务栈与协程上下文传播TransactionContextElement。方言适配各数据库方言实现集中在 exposed-core/src/main/kotlin/org/jetbrains/exposed/v1/core/vendors每个方言负责类型映射与 SQL 语法差异的翻译。语句实现INSERT / UPDATE / DELETE / MERGE / UPSERT / BATCH 等语句类集中在 exposed-core/src/main/kotlin/org/jetbrains/exposed/v1/core/statements可通过StatementInterceptor拦截与定制 SQL 生成。DAO 实体层实体基类与缓存、批量操作实现位于 exposed-dao/src/main/kotlin/org/jetbrains/exposed/v1/dao其中EntityCache.kt负责实体的身份映射与懒加载缓存。七、配套示例与学习路线仓库 samples 提供了多个可直接运行的综合示例覆盖不同技术栈springboot3-exposed-r2dbcSpring Boot 3.5、AOT、Java Virtual Threads、Exposed-R2DBC 与 MySQL R2DBC 的后端应用exposed-ktor基于 Ktor 与 Exposed 的 CRUD 后端应用exposed-ktor-r2dbc基于 Ktor、Exposed 与 PostgreSQL R2DBC 的应用exposed-spring基于 Spring Boot 3 的 CRUD 项目exposed-migration演示如何使用 Exposed 生成数据库迁移脚本exposed-gradle-plugin-sample演示 Exposed Gradle 插件及其generateMigrations任务。八、文档、贡献与支持文档完整文档、示例与教程见仓库内 docs/index.html迁移指南与破坏性变更说明分别见 docs/migration-guide.html 与 docs/breaking-changes.html。问题反馈官方使用 YouTrackEXPOSED 项目收集 issue、功能请求与问题创建或评论 issue 需要登录 YouTrack。Pull Request欢迎提交 PR并建议关联已有 issue贡献者默认以 Apache License 2.0 授权其贡献见仓库根目录 LICENSE.txt。社区支持官方讨论渠道为 Kotlin Slack 的#exposed频道可申请邀请加入。结语Exposed 以“轻量、类型安全、可移植”为核心设计目标通过 DSL 与 DAO 两种 API 覆盖了从底层 SQL 到对象映射的全部日常场景并依托完善的方言体系支持 H2、MySQL、MariaDB、PostgreSQL、Oracle、SQL Server、SQLite、Redshift 等多种数据库。本文从 README 出发结合仓库源码给出了模块选择、环境要求、依赖配置、完整可运行的 CRUD 示例及生成的 SQL可作为你快速上手 Exposed 的参考起点更深入的 DSL 查询、表类型、事务与迁移等内容可继续阅读仓库内 docs 目录下的专题文档。赞分享ORM后端数据存储【免费下载链接】ExposedKotlin SQL Framework项目地址https://gitcode.com/gh_mirrors/ex/Exposed点击查看免费下载相关推荐Kotlin SQL框架Exposed轻量级ORM的革命性设计与实现Kotlin SQL框架Exposed轻量级ORM的革命性设计与实现 你是否在寻找一种既能保持SQL控制力又能享受Kotlin类型安全的数据库访问方式ExORM后端数据存储Koin 入门指南面向 Kotlin 与 Kotlin Multiplatform 的轻量级依赖注入框架Koin 入门指南面向 Kotlin 与 Kotlin Multiplatform 的轻量级依赖注入框架 Koin 是一个专为 Kotlin 开发者设计的轻量后端LoggerPlusPlus导出功能全攻略CSV、JSON、HAR与Elasticsearch无缝集成LoggerPlusPlus导出功能全攻略CSV、JSON、HAR与Elasticsearch无缝集成 LoggerPlusPlus作为Burp Suite的上一篇魔兽争霸3卡顿优化与帧率解锁终极指南WarcraftHelper 三步实战下一篇NCM转MP3一键搞定NCMconverter免费批量转换完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表