ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Exposed DAO 实体关系实战指南:从表设计到关联查询的完整示例工程

Exposed DAO 实体关系实战指南:从表设计到关联查询的完整示例工程 ORM后端数据存储【免费下载链接】ExposedKotlin SQL Framework项目地址https://gitcode.com/gh_mirrors/ex/Exposed点击查看免费下载Exposed 的 DAO API 提供了一套面向对象的实体关系建模能力让 Kotlin 开发者可以用类型安全的方式表达数据库表之间的关联。本文以官方示例工程exposed-dao-relationships为主线完整讲解多对一、可选引用、有序引用、多对多、父子层级、复合主键引用与预加载Eager Loading七类关系的表定义、实体声明与查询写法并给出可一键构建运行的 Gradle 工程读完即可在自己的项目中落地这些关系模式。示例工程是什么exposed-dao-relationships是一个独立的 Gradle 示例应用位于仓库的 documentation-website/Writerside/snippets/exposed-dao-relationships 目录下。它的唯一目的就是展示「如何用 Exposed DAO API 在实体之间建立关系」其源码文件被官方文档主题 DAO-Relationships.topic 按行号直接引用作为文档中所有代码块的权威来源。工程采用标准的 Gradle 目录布局exposed-dao-relationships/ ├── README.md └── src/main/kotlin/org/example/ ├── App.kt # 程序入口连接数据库、建表、依次运行各示例 ├── tables/ # 表定义DSL 层 │ ├── UsersTable.kt │ ├── UserRatingsTable.kt │ ├── StarWarsFilmsTable.kt │ ├── ActorsTable.kt │ └── DirectorsTable.kt ├── entities/ # 实体定义DAO 层 │ ├── UserEntity.kt │ ├── UserRatingEntity.kt │ ├── StarWarsFilmEntity.kt │ ├── ActorEntity.kt │ └── DirectorEntity.kt └── examples/ # 各关系的实际查询示例 ├── OneToManyExamples.kt ├── ManyToManyExamples.kt ├── ParentChildExamples.kt └── EagerLoadingExamples.kt所有示例围绕一个主题域展开Star Wars系列电影、用户评分、演员与导演。得益于这种统一的示例数据每一种关系模式都能直观对照现实语义来理解。构建与运行示例工程按照 README.md 的说明构建与运行都需要先切换到snippets目录注意不是示例工程自身目录而是其上一级、包含settings.gradle.kts的 Gradle 根目录。构建整个工程./gradlew :exposed-dao-relationships:build运行应用./gradlew :exposed-dao-relationships:run运行时会执行以下流程见 App.kt连接内存数据库jdbc:h2:mem:testH2 驱动并开启嵌套事务支持Database.connect( jdbc:h2:mem:test, org.h2.Driver, databaseConfig DatabaseConfig { useNestedTransactions true } )在transaction {}块内注册StdOutSqlLogger将生成的 SQL 打印到控制台便于观察每条关系查询对应的 SQL 语句。通过SchemaUtils.create(...)依次创建所有涉及的表ActorsTable、StarWarsFilmsTable、StarWarsFilmActorsTable、UserRatingsTable、UsersTable以及父子关系示例中的DirectorsTable、StarWarsFilmsWithDirectorTable、StarWarsFilmRelationsTable。依次调用runOneToManyExample()、runManyToManyExample()、runParentChildExample()、runEagerLoadingExamples()即运行examples目录下的全部函数。只运行某一个示例也很简单按 README 所述直接修改App.kt注释掉不需要的runXxxExample()调用后重新运行即可。例如只想观察多对一关系可以只保留runOneToManyExample()。表与实体的基本形态IntIdTable IntEntity在进入关系定义之前先看示例中最基础的表与实体配对见 UsersTable.kt 与 UserEntity.ktobject UsersTable : IntIdTable() { val name varchar(name, MAX_USER_NAME_LENGTH) // MAX_USER_NAME_LENGTH 50 } class UserEntity(id: EntityIDInt) : IntEntity(id) { companion object : IntEntityClassUserEntity(UsersTable) var name by UsersTable.name }要点表继承IntIdTable()自动获得自增的Int主键列id实体继承IntEntity构造函数接收EntityIDInt。实体通过by委托将表列映射为属性var name by UsersTable.name表示可读写的普通字段。每张表/实体都配套一个companion object继承IntEntityClassT它是后续执行new、findById、all、find等查询操作的入口。多对一关系Many-to-one多对一关系指「子表的多行记录可以引用父表的同一行记录」。示例中用户UsersTable与评分UserRatingsTable就是典型的多对一一个用户可以有多个评分每个评分只属于一个用户同时每一条评分还引用一部电影。用 reference() 创建引用列在子表UserRatingsTable中通过reference()函数声明指向父表的列见 UserRatingsTable.ktobject UserRatingsTable : IntIdTable() { val value long(value) val film reference(film, StarWarsFilmsTable) val user reference(user, UsersTable) }reference(columnName, targetTable)会同时完成两件事在UserRatingsTable上创建一个外键列列类型与目标表主键一致这里是Int生成数据库层面的外键约束保证引用的完整性。在实体中使用 referencedOn表定义完成后在实体UserRatingEntity中通过referencedOn将引用列映射为强类型的实体属性见 UserRatingEntity.ktclass UserRatingEntity(id: EntityIDInt) : IntEntity(id) { companion object : IntEntityClassUserRatingEntity(UserRatingsTable) var value by UserRatingsTable.value var film by StarWarsFilmEntity referencedOn UserRatingsTable.film // 普通引用用 referencedOn var user by UserEntity referencedOn UserRatingsTable.user }注意这里的关键字是referencedOn表示「本实体的这个属性指向另一实体」是多对一方向的映射。正向访问像访问普通字段一样取值一旦建立映射获取关联对象与获取普通字段没有区别。见 OneToManyExamples.ktval filmRating UserRatingEntity.new { value MOVIE_RATING film starWarsFilm user user1 } // 返回一个 StarWarsFilmEntity 对象 val film filmRating.film创建评分实体时可以直接把starWarsFilm、user1实体对象赋给引用属性Exposed 会自动把对应的主键值写入外键列。反向访问referrersOn 一对多「给定一个用户获取他的所有评分」属于一对多方向。虽然可以用UserRatingEntity.find { UserRatingsTable.user eq user.id }查询但更优雅的做法是在父实体上声明referrersOn字段见 StarWarsFilmEntity.ktclass StarWarsFilmEntity(id: EntityIDInt) : IntEntity(id) { companion object : IntEntityClassStarWarsFilmEntity(StarWarsFilmsTable) var sequelId by StarWarsFilmsTable.sequelId var name by StarWarsFilmsTable.name var director by StarWarsFilmsTable.director val ratings by UserRatingEntity referrersOn UserRatingsTable.film // 必须用 val referrersOn var actors by ActorEntity via StarWarsFilmActorsTable }语法为val 属性 by 子实体类 referrersOn 子表引用列。此时在实体对象上访问film.ratings就会返回该电影关联的所有UserRatingEntity的SizedIterable见 OneToManyExamples.kt// 返回所有以该电影为 film 的 UserRatingEntity 对象 val filmRatings starWarsFilm.ratings代码注释强调referrersOn属性必须使用val声明。单值反向访问backReferencedOnbackReferencedOn用于一对一的简化反向场景当业务约束「每个用户只给一部电影评分」时可在用户实体上直接暴露单个评分对象而非集合见 UserEntity.ktclass UserWithSingleRatingEntity(id: EntityIDInt) : IntEntity(id) { companion object : IntEntityClassUserWithSingleRatingEntity(UsersTable) var name by UsersTable.name val rating by UserRatingEntity backReferencedOn UserRatingsTable.user // 必须用 val backReferencedOn }访问方式user1.rating // 返回一个 UserRating 对象backReferencedOn与referrersOn都要求val声明二者区别在于返回的是单个实体对象一对一语义还是实体集合一对多语义。可选引用Optional reference如果某些评分来自匿名用户没有注册账号外键值可以为空。这时表与实体需要分别使用optReference()与optionalReferencedOn见 UserRatingsTable.kt 与 UserRatingEntity.ktobject UserRatingsWithOptionalUserTable : IntIdTable() { val value long(value) val film reference(film, StarWarsFilmsTable) val user optReference(user, UsersTable) // 允许为空的外键 } class UserRatingWithOptionalUserEntity(id: EntityIDInt) : IntEntity(id) { companion object : IntEntityClassUserRatingWithOptionalUserEntity(UserRatingsWithOptionalUserTable) var value by UserRatingsWithOptionalUserTable.value var film by StarWarsFilmEntity referencedOn UserRatingsWithOptionalUserTable.film var user by UserEntity optionalReferencedOn UserRatingsWithOptionalUserTable.user }对应的实体属性类型自动变为可空user在未关联用户时返回null。optionalReferencedOn同样存在可选的反向变体optionalReferrersOn用于一对多的可空反向引用在复合主键一节还会再次遇到。有序引用Ordered reference默认情况下referrersOn返回的集合顺序由数据库决定。若希望固定排序可在声明后追加orderBy见 UserEntity.kt。单个列排序的最简形式val ratings by UserRatingEntity referrersOn UserRatingsTable.user orderBy UserRatingsTable.value多列排序时传入列 to SortOrder的列表可以混用升序ASC与降序DESCval ratings by UserRatingEntity referrersOn UserRatingsTable.user orderBy listOf( UserRatingsTable.value to SortOrder.DESC, UserRatingsTable.id to SortOrder.ASC )若不想使用中缀写法等价于把orderBy作为普通方法链式调用val ratings by UserRating.referrersOn(UserRatings.user).orderBy( UserRatings.value to SortOrder.DESC, UserRatings.id to SortOrder.ASC )多对多关系Many-to-many多对多关系需要一张中间表来桥接两侧。示例中电影与演员是多对多一部电影有多个演员一个演员也出演多部电影。定义中间表StarWarsFilmActorsTable用两条reference()分别指向电影表与演员表并以这两列组成复合主键见 ActorsTable.ktobject StarWarsFilmActorsTable : Table() { val starWarsFilm reference(starWarsFilm, StarWarsFilmsTable) val actor reference(actor, ActorsTable) override val primaryKey PrimaryKey(starWarsFilm, actor, name PK_StarWarsFilmActors_swf_act) // 主键名称可选 }中间的连接表只需继承普通的Table无需主键自增列。在实体中用 via() 建立桥接在StarWarsFilmEntity中通过via()声明集合属性见 StarWarsFilmEntity.ktvar actors by ActorEntity via StarWarsFilmActorsTable写法与referrersOn不同目标是另一端的实体类ActorEntity中间经过连接表。之后就可以在创建电影时直接赋值演员集合见 ManyToManyExamples.ktval actor ActorEntity.new { firstname Daisy lastname Ridley } val film StarWarsFilmEntity.new { name The Rise of Skywalker sequelId MOVIE2_SEQUEL_ID director J.J. Abrams actors SizedCollection(listOf(actor)) } val filmActors film.actors filmActors.forEach { println(it.firstname) }actors的类型是SizedIterableActorEntity写入时需要包装为SizedCollection(listOf(actor))。Exposed 会自动维护中间表赋值即插入关联记录读取即联表查询。父子关系Parent-child reference父子关系与多对多非常相似区别在于中间表的两条引用都指向同一张表用于表达层级结构如续集/前传关系。示例中StarWarsFilmRelationsTable的两列都指向StarWarsFilmsWithDirectorTable见 StarWarsFilmsTable.ktobject StarWarsFilmsWithDirectorTable : IntIdTable() { val name varchar(name, MAX_VARCHAR_LENGTH) val director reference(director, DirectorsTable) } object StarWarsFilmRelationsTable : Table() { val parentFilm reference(parent_film_id, StarWarsFilmsWithDirectorTable) val childFilm reference(child_film_id, StarWarsFilmsWithDirectorTable) override val primaryKey PrimaryKey(parentFilm, childFilm, name PK_FilmRelations) }这里parentFilm表示原版电影childFilm表示续集/前传/衍生作品中间表的两列只指向StarWarsFilmsWithDirectorTable形成自引用。实体层面用两次方向相反的via()同时声明「续集」与「前传」两个视角见 StarWarsFilmEntity.ktclass StarWarsFilmWithParentAndChildEntity(id: EntityIDInt) : IntEntity(id) { companion object : IntEntityClassStarWarsFilmWithParentAndChildEntity(StarWarsFilmsWithDirectorTable) var name by StarWarsFilmsWithDirectorTable.name var director by DirectorEntity referencedOn StarWarsFilmsWithDirectorTable.director var sequels by StarWarsFilmWithParentAndChildEntity.via( StarWarsFilmRelationsTable.parentFilm, StarWarsFilmRelationsTable.childFilm ) var prequels by StarWarsFilmWithParentAndChildEntity.via( StarWarsFilmRelationsTable.childFilm, StarWarsFilmRelationsTable.parentFilm ) }注意via()的两个参数顺序决定了方向(parentFilm, childFilm)表示「从父到子」得到sequels(childFilm, parentFilm)表示「从子到父」得到prequels。创建与查询层级数据的完整过程见 ParentChildExamples.ktval director1 DirectorEntity.new { name George Lucas genre Genre.SCI_FI } val film1 StarWarsFilmWithParentAndChildEntity.new { name Star Wars: A New Hope; director director1 } val film2 StarWarsFilmWithParentAndChildEntity.new { name Star Wars: The Empire Strikes Back; director director1 } val film3 StarWarsFilmWithParentAndChildEntity.new { name Star Wars: Return of the Jedi; director director1 } // 建立父子关系 film2.prequels SizedCollection(listOf(film1)) // Empire Strikes Back 是 A New Hope 的续集 film3.prequels SizedCollection(listOf(film2)) // Return of the Jedi 是 Empire Strikes Back 的续集 film1.sequels SizedCollection(listOf(film2, film3)) // A New Hope 有续集 Empire Strikes Back film2.sequels SizedCollection(listOf(film3)) film1.sequels.forEach { sequel - println(${sequel.name} is a sequel to ${film1.name}) } film3.prequels.forEach { prequel - println(${film3.name} has a prequel: ${prequel.name}) }复合主键引用Composite primary key reference当目标表使用复合主键时引用方式稍有不同。示例中DirectorsCompositeIdTable以「姓名 公会 ID」两列组成复合主键见 DirectorsTable.ktobject DirectorsCompositeIdTable : CompositeIdTable() { val name varchar(name, NAME_LENGTH).entityId() val guildId uuid(guild_id).autoGenerate().entityId() val genre enumerationGenre(genre) override val primaryKey PrimaryKey(name, guildId) }要点表继承CompositeIdTable主键列需要调用.entityId()标记实体继承CompositeEntity构造函数接收EntityIDCompositeID见 DirectorEntity.kt。子表侧表级外键约束 referencedOn引用复合主键表时reference()不再适用改为在子表中加入各主键对应的列并在init块中通过foreignKey(...)声明表级外键约束见 StarWarsFilmsTable.ktobject StarWarsFilmsWithCompositeRefTable : IntIdTable() { val sequelId integer(sequel_id).uniqueIndex() val name varchar(name, MAX_VARCHAR_LENGTH) val directorName varchar(director_name, MAX_VARCHAR_LENGTH) val directorGuildId uuid(director_guild_id) init { foreignKey(directorName, directorGuildId, target DirectorsCompositeIdTable.primaryKey) } }关于表级外键约束的完整说明可参考文档 Working-with-Tables.topic 中的 Foreign Key constraint 一节。实体侧同样使用referencedOn但传入的是整张表而非单个列见 StarWarsFilmEntity.ktclass StarWarsFilmWithCompositeRefEntity(id: EntityIDInt) : IntEntity(id) { companion object : IntEntityClassStarWarsFilmWithCompositeRefEntity(StarWarsFilmsWithCompositeRefTable) var sequelId by StarWarsFilmsWithCompositeRefTable.sequelId var name by StarWarsFilmsWithCompositeRefTable.name var director by DirectorCompositeIDEntity referencedOn StarWarsFilmsWithCompositeRefTable }访问与普通引用完全一致movie.director // 返回一个 Director 对象反向访问复合主键引用若想从导演侧获取其执导的所有电影使用referrersOn并传入子表见 DirectorEntity.ktclass DirectorCompositeIDEntity(id: EntityIDCompositeID) : CompositeEntity(id) { companion object : CompositeEntityClassDirectorCompositeIDEntity(DirectorsCompositeIdTable) var genre by DirectorsCompositeIdTable.genre val films by StarWarsFilmWithCompositeRefEntity referrersOn StarWarsFilmsWithCompositeRefTable }访问director.films // 返回所有引用该导演的 StarWarsFilm 对象此外optionalReferencedOn、backReferencedOn、optionalReferrersOn等中缀函数同样支持CompositeEntity只需要使用它们接受IdTable参数的重载Exposed 会自动解析与复合主键关联的外键约束。预加载Eager Loading消灭 N1 查询Exposed 的实体引用默认是懒加载的第一次访问引用属性时才发起查询。当批量遍历实体并逐个访问引用时会产生经典的「N1」问题。若提前知道需要哪些引用可以在父查询执行时一并加载把多次查询合并为一次见 EagerLoadingExamples.kt。单个实体的 load()使用load()并传入引用属性的KPropertyUserEntity.findById(1)?.load(UserEntity::ratings)load()支持引用套引用例如UserRatingTable上有film引用可以一次性加载两级UserEntity.findById(1)?.load(UserEntity::ratings, UserRatingEntity::film)集合的 with()对List、SizedIterable等实体集合使用with()为集合中所有实体预加载指定引用并返回原集合UserEntity.all().with(UserEntity::ratings)SizedIterable的方法调用需要处于事务中因此预加载后的集合不能直接在transaction块外使用除非先转换为标准集合如toList()。注意预加载结果与事务缓存预加载的引用存储在事务缓存中这意味着它们只在当前事务内有效跨事务访问会拿不到。若需要在事务外读取引用值可以在DatabaseConfig中开启keepLoadedReferencesOutOfTransaction。示例的App.kt中展示了DatabaseConfig的配置方式目前配置的是useNestedTransactions true。文本字段的预加载另一个与懒加载相关的细节部分数据库驱动出于性能与内存考虑不会立即加载大文本内容导致文本列只能在打开的transaction内取值。若希望文本内容在事务外也可用在字段定义时开启eagerLoadingobject StarWarsFilmsTable : Table() { //... val description text(name, eagerLoading true) }小结关系模式表层 API实体层 API示例文件多对一reference()referencedOnUserRatingsTable.kt、UserRatingEntity.kt一对多反向—referrersOnStarWarsFilmEntity.kt一对一单值反向—backReferencedOnUserEntity.kt可选引用optReference()optionalReferencedOnUserRatingsTable.kt有序引用—referrersOnorderByUserEntity.kt多对多中间表复合主键via()ActorsTable.kt、StarWarsFilmEntity.kt父子层级自引用中间表via()双向StarWarsFilmsTable.kt、ParentChildExamples.kt复合主键引用CompositeIdTableforeignKeyreferencedOn/referrersOn传表DirectorsTable.kt、DirectorEntity.kt预加载text(..., eagerLoadingtrue)load()/with()EagerLoadingExamples.kt掌握referencedOn、referrersOn、backReferencedOn、optionalReferencedOn、via这五类中缀函数与load/with两个预加载入口就能覆盖绝大多数实体关系建模场景。官方完整的 DAO 关系文档见 DAO-Relationships.topic其中所有代码示例均直接来源于本文剖析的这个示例工程按行号一一对应可作为深入阅读的下一步资料。赞分享ORM后端数据存储【免费下载链接】ExposedKotlin SQL Framework项目地址https://gitcode.com/gh_mirrors/ex/Exposed点击查看免费下载相关推荐generator-nm与CI/CD集成自动化测试与部署实战指南generator nm与CI/CD集成自动化测试与部署实战指南 generator nm是一款强大的Node.js模块脚手架工具能够帮助开发者快速搭建标准SQLCoder TABLE JOIN实战多表关联查询指南SQLCoder TABLE JOIN实战多表关联查询指南 一、痛点解析你还在为多表关联查询头疼吗 在数据查询领域多表关联TABLE JOIN是最常如何掌握面向对象设计中的关联关系从理论到实战的完整指南如何掌握面向对象设计中的关联关系从理论到实战的完整指南 面向对象编程OOP中的关联关系是构建灵活、可维护软件系统的核心基础。在GitHub推荐项目精选 a示例工程上一篇Gemma 3重磅发布多模态AI新纪元128K上下文与140语言支持重塑开发范式下一篇终极指南如何用 recast 构建智能代码质量检查工具创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表