使用 Jooq 创建通用存储库
Create a generic repository with Jooq
我们正在使用 JOOQ 映射现有数据库。此数据库中的每个 table(第 1 条)共享一组相似的列:
resource_id: Int (FK)
id: Int (PK)
identifier: UUID
date_created: DateTime
date_modified: DateTime
数据库中有一个通用规则,适用于所有看起来像这样的操作:
select *
from table t
join resource r on r.id = t.resource_id
where r.is_archived = false
使用 JOOQs 生成功能,我可以成功创建一个实现基本查询操作的存储库 class:
@Repository
class UserRepository(val dsl:DSLContext): IRepository {
val record = Tables.AUTHENTICATION_USER!! //generated by jooq
val resource = Tables.RESOURCE!! //generated by jooq
val pojo = AuthenticationUser::class.java //generated by jooq
// this is the general query logic that should be applied to all the things
override fun selectQuery(): SelectConditionStep<Record> {
return dsl
.select(record.asterisk())
.from(record)
.join(resource).on(resource.ID.eq(record.RESOURCE_ID))
.where(resource.IS_ARCHIVED.eq(false))
}
override fun asyncCount(): Mono<Int> = Mono.fromCallable {
selectQuery()
.count()
}
fun asyncGetPage(from: Int,
size: Int,
orderBy: SortField<*> = record.DATE_CREATED.asc(),
) = Mono.fromCallable {
selectQuery()
.orderBy(orderBy)
.limit(size)
.offset(from)
.fetchInto(pojo)
.let { Collections.unmodifiableList(it) }
}
现在,这一切都很好,但我无法将其转换为可以
的通用实现
- 接受
table
类型 T
- 可以识别 table 上的那 5 个常见 fields/attributes,因此我们可以使我们的查询通用。
我可以让它工作:
class GeneralRepository<T: Table<R>, R: Record>(val table: T, val record: R, val dsl:DSLContext) {
fun asyncGetAll(): Mono<List<R>> = Mono.fromCallable {
dsl
.select()
.from(table)
.fetchInto(record::class.java)
}
}
// manually construct it ignoring Springs DI for development
val resourceRepository = GeneralRepository(
Tables.RESOURCE,
ResourceRecord(),
JooqConfiguration().dsl(),
)
当我尝试对 table T
做任何事情时,问题就来了。由于 T
是通用的 table 类型,编译器无法知道字段 id
、identifier
、resource_id
、date_created
等等,这意味着像这样的查询无法编译:
fun asyncGetAllNonArchived() =
dsl
.select()
.from(table)
.where(table.DATE_CREATED > ...)
问题:
有没有办法告诉编译器或以公共字段可用的方式声明泛型。
编辑:感谢下面的 Lukas Eder 的建议。调整该解决方案,我们能够创建一个通用存储库。
首先,将生成器策略更改为 Kotlin 让 Auditable
接口正确运行 :)
其次,我们找不到使用通用 class 的方法来根据 Record
类型实现 Table
类型,因此您必须非常轻微地扩展签名。
interface Auditable {
fun RESOURCE_ID(): TableField<Record, Int> =
(this as Table<Record>).field("resource_id") as TableField<Record, Int>
fun ID(): TableField<Record, Int> =
(this as Table<Record>).field("id") as TableField<Record, Int>
fun IDENTIFIER(): TableField<Record, UUID> =
(this as Table<Record>).field("identifier") as TableField<Record, UUID>
fun DATE_CREATED(): TableField<Record, LocalDateTime> =
(this as Table<Record>).field("date_created") as TableField<Record, LocalDateTime>
fun DATE_MODIFIED(): TableField<Record, LocalDateTime> =
(this as Table<Record>).field("date_modified") as TableField<Record, LocalDateTime>
}
open class GeneralRepository<R, T>(val table: T) where R: Record, T: Table<R>, T: Auditable {
val resource = Resource.RESOURCE
fun selectQuery() =
dsl
.select(table.asterisk())
.from(table)
.join(resource).on(resource.ID.eq(table.RESOURCE_ID()))
.where(resource.IS_ARCHIVED.eq(false))
然后您可以轻松实现通用扩展:
class UserRepo: GeneralRepository<UserRecord, User>(table = User.USER) {
fun getEmail(email: String) = Mono.fromCallable {
selectQuery().and(table.EMAIL.eq(email))
}
}
您可以生成一个接口来提供对这些列的访问:
interface Auditable {
fun RESOURCE_ID(): TableField<Record, Int?> =
(this as Table<Record>).field("resource_id") as TableField<Record, Int?>
fun ID(): TableField<Record, Int?> =
(this as Table<Record>).field("id") as TableField<Record, Int?>
fun identifier(): TableField<Record, UUID?> =
(this as Table<Record>).field("identifier") as TableField<Record, UUID?>
fun DATE_CREATED(): TableField<Record, LocalDateTime?> =
(this as Table<Record>).field("date_created") as TableField<Record, LocalDateTime?>
fun DATE_MODIFIED(): TableField<Record, LocalDateTime?> =
(this as Table<Record>).field("date_modified") as TableField<Record, LocalDateTime?>
}
然后使用 generator strategy:
将接口附加到所有生成的表
<strategy>
<matchers>
<tables>
<table>
<expression>T_.*</expression>
<tableImplements>com.example.Auditable</tableImplements>
</table>
</tables>
</matchers>
</strategy>
目前无法在界面中声明属性,因为无法检测是否需要 override
修饰符,请参阅 https://github.com/jOOQ/jOOQ/issues/10776。
但现在这可能已经足够好了。然后,您可以像这样创建通用条件:
fun <T> dateCreatedCondition(table: T, dt: LocalDateTime): Condition
where T : Table<*>, T : Auditable = table.DATE_CREATED().gt(dt)
我们正在使用 JOOQ 映射现有数据库。此数据库中的每个 table(第 1 条)共享一组相似的列:
resource_id: Int (FK)
id: Int (PK)
identifier: UUID
date_created: DateTime
date_modified: DateTime
数据库中有一个通用规则,适用于所有看起来像这样的操作:
select *
from table t
join resource r on r.id = t.resource_id
where r.is_archived = false
使用 JOOQs 生成功能,我可以成功创建一个实现基本查询操作的存储库 class:
@Repository
class UserRepository(val dsl:DSLContext): IRepository {
val record = Tables.AUTHENTICATION_USER!! //generated by jooq
val resource = Tables.RESOURCE!! //generated by jooq
val pojo = AuthenticationUser::class.java //generated by jooq
// this is the general query logic that should be applied to all the things
override fun selectQuery(): SelectConditionStep<Record> {
return dsl
.select(record.asterisk())
.from(record)
.join(resource).on(resource.ID.eq(record.RESOURCE_ID))
.where(resource.IS_ARCHIVED.eq(false))
}
override fun asyncCount(): Mono<Int> = Mono.fromCallable {
selectQuery()
.count()
}
fun asyncGetPage(from: Int,
size: Int,
orderBy: SortField<*> = record.DATE_CREATED.asc(),
) = Mono.fromCallable {
selectQuery()
.orderBy(orderBy)
.limit(size)
.offset(from)
.fetchInto(pojo)
.let { Collections.unmodifiableList(it) }
}
现在,这一切都很好,但我无法将其转换为可以
的通用实现- 接受
table
类型T
- 可以识别 table 上的那 5 个常见 fields/attributes,因此我们可以使我们的查询通用。
我可以让它工作:
class GeneralRepository<T: Table<R>, R: Record>(val table: T, val record: R, val dsl:DSLContext) {
fun asyncGetAll(): Mono<List<R>> = Mono.fromCallable {
dsl
.select()
.from(table)
.fetchInto(record::class.java)
}
}
// manually construct it ignoring Springs DI for development
val resourceRepository = GeneralRepository(
Tables.RESOURCE,
ResourceRecord(),
JooqConfiguration().dsl(),
)
当我尝试对 table T
做任何事情时,问题就来了。由于 T
是通用的 table 类型,编译器无法知道字段 id
、identifier
、resource_id
、date_created
等等,这意味着像这样的查询无法编译:
fun asyncGetAllNonArchived() =
dsl
.select()
.from(table)
.where(table.DATE_CREATED > ...)
问题: 有没有办法告诉编译器或以公共字段可用的方式声明泛型。
编辑:感谢下面的 Lukas Eder 的建议。调整该解决方案,我们能够创建一个通用存储库。
首先,将生成器策略更改为 Kotlin 让 Auditable
接口正确运行 :)
其次,我们找不到使用通用 class 的方法来根据 Record
类型实现 Table
类型,因此您必须非常轻微地扩展签名。
interface Auditable {
fun RESOURCE_ID(): TableField<Record, Int> =
(this as Table<Record>).field("resource_id") as TableField<Record, Int>
fun ID(): TableField<Record, Int> =
(this as Table<Record>).field("id") as TableField<Record, Int>
fun IDENTIFIER(): TableField<Record, UUID> =
(this as Table<Record>).field("identifier") as TableField<Record, UUID>
fun DATE_CREATED(): TableField<Record, LocalDateTime> =
(this as Table<Record>).field("date_created") as TableField<Record, LocalDateTime>
fun DATE_MODIFIED(): TableField<Record, LocalDateTime> =
(this as Table<Record>).field("date_modified") as TableField<Record, LocalDateTime>
}
open class GeneralRepository<R, T>(val table: T) where R: Record, T: Table<R>, T: Auditable {
val resource = Resource.RESOURCE
fun selectQuery() =
dsl
.select(table.asterisk())
.from(table)
.join(resource).on(resource.ID.eq(table.RESOURCE_ID()))
.where(resource.IS_ARCHIVED.eq(false))
然后您可以轻松实现通用扩展:
class UserRepo: GeneralRepository<UserRecord, User>(table = User.USER) {
fun getEmail(email: String) = Mono.fromCallable {
selectQuery().and(table.EMAIL.eq(email))
}
}
您可以生成一个接口来提供对这些列的访问:
interface Auditable {
fun RESOURCE_ID(): TableField<Record, Int?> =
(this as Table<Record>).field("resource_id") as TableField<Record, Int?>
fun ID(): TableField<Record, Int?> =
(this as Table<Record>).field("id") as TableField<Record, Int?>
fun identifier(): TableField<Record, UUID?> =
(this as Table<Record>).field("identifier") as TableField<Record, UUID?>
fun DATE_CREATED(): TableField<Record, LocalDateTime?> =
(this as Table<Record>).field("date_created") as TableField<Record, LocalDateTime?>
fun DATE_MODIFIED(): TableField<Record, LocalDateTime?> =
(this as Table<Record>).field("date_modified") as TableField<Record, LocalDateTime?>
}
然后使用 generator strategy:
将接口附加到所有生成的表<strategy>
<matchers>
<tables>
<table>
<expression>T_.*</expression>
<tableImplements>com.example.Auditable</tableImplements>
</table>
</tables>
</matchers>
</strategy>
目前无法在界面中声明属性,因为无法检测是否需要 override
修饰符,请参阅 https://github.com/jOOQ/jOOQ/issues/10776。
但现在这可能已经足够好了。然后,您可以像这样创建通用条件:
fun <T> dateCreatedCondition(table: T, dt: LocalDateTime): Condition
where T : Table<*>, T : Auditable = table.DATE_CREATED().gt(dt)