- ORM
- 后端
- 数据存储
【免费下载链接】Exposed
Kotlin SQL Framework
导读
本文以 JetBrains Exposed(Kotlin SQL Framework)官方文档仓库中的exposed-data-typessnippets 示例项目为主线,系统讲解 Exposed 支持的全部数据类型家族——从最基础的数字/字符串/布尔列,到数组、二进制(BLOB)、枚举、JSON/JSONB,再到三套日期时间 API(java.time、Joda-Time、kotlinx-datetime)。你将掌握示例项目的配置、构建、运行方式,并读懂每类数据类型在真实表定义、插入、查询场景下的完整写法,为后续阅读 Exposed 官方数据类型文档 等主题打下可直接运行的代码基础。
一、示例项目定位与整体结构
exposed-data-types是一个独立的 Gradle 应用,位于仓库的 documentation-website/Writerside/snippets/exposed-data-types 目录。它的作用是演示 Exposed 中有哪些数据类型、以及它们如何被定义与使用——文档中明确说明“The files are referenced in the Schema's Data types topics”,即该示例中的代码文件被官方文档的 Data types 系列主题(如 Array-types.topic、Binary-types.topic、Numeric-Boolean-String-Types.topic、Enumeration-types.topic、json-and-jsonb-types.md)按行号引用。因此,阅读本文就等于拿到了官方文档示例的“源码级注释”。
项目代码组织如下:
exposed-data-types/ └── src/main/kotlin/org/example/ ├── App.kt # 程序入口,负责数据库连接与示例调度 └── examples/ ├── BasicTypesExamples.kt # 数字/布尔/字符串基础类型 ├── ArrayExamples.kt # 一维、多维数组 ├── BinaryExamples.kt # blob / binary 二进制类型 ├── EnumerationExamples.kt # enumeration 枚举类型 ├── JSONandJSONBExamples.kt # json / jsonb 及操作函数 ├── JavaTimeExamples.kt # java.time 日期时间 ├── JodaTimeExamples.kt # Joda-Time 日期时间 └── KotlinDateTimeExamples.kt # kotlinx-datetime 日期时间整个snippets是一个多模块 Gradle 工程,settings.gradle.kts 中通过include("exposed-data-types")注册该模块,rootProject 名为snippets,并配置了 foojay-resolver 插件以支持自动下载 JDK。模块内部还依赖了exposed-dao、exposed-dsl等其他兄弟示例,方便读者在一个工程内横向对照。
注意:示例代码头部都有一段注释(如
BasicTypesExamples.kt顶部),提示“本文件被Numeric-Boolean-String-types.topic按行号引用,若增删改行务必同步更新 topic 中的code-block”。这说明示例代码与官方文档是严格同步维护的,参考价值很高。
二、配置:数据库连接与示例调度入口
2.1 修改连接凭据
根据 README 的说明,配置步骤是:打开App.kt,确保数据库连接凭据与你的环境一致。以 PostgreSQL 为例:
val postgreSQL = Database.connect( "jdbc:postgresql://localhost:5432/postgres", driver = "org.postgresql.Driver", user = "user", password = "password" )在真实的 App.kt 中,runPostgreSQLExamples()函数正是这样建立的连接,并在transaction(postgreSQL)中依次执行:
addLogger(StdOutSqlLogger):把生成的 SQL 打印到标准输出,便于观察每条 DSL 对应的 SQL 语句;SchemaUtils.create(TeamsTable)/SchemaUtils.create(TeamProjectsTable):通过SchemaUtils.create自动建表;- 依次调用各数据类型的示例函数。
2.2 三种内置数据库示例
App.kt的main()中内置了三个连接入口(默认注释掉前两个、只跑 H2):
| 入口函数 | JDBC URL | 驱动 | 说明 |
|---|---|---|---|
runMySQLExamples() | jdbc:mysql://localhost:3306/test?allowMultiQueries=true | com.mysql.cj.jdbc.Driver | 需 MySQL 服务,注意allowMultiQueries=true参数以支持多条语句 |
runH2Examples() | jdbc:h2:mem:test | org.h2.Driver | 内存数据库,零外部依赖,开箱即用,且配置了DatabaseConfig { useNestedTransactions = true } |
runPostgreSQLExamples() | jdbc:postgresql://localhost:5432/postgres | org.postgresql.Driver | 需 PostgreSQL 服务 |
从代码看,H2 示例是默认推荐路径:它不依赖外部数据库,并且会执行最全的示例集合(Kotlin 日期时间、JSON、枚举、二进制四大类)。如果你要验证 MySQL / PostgreSQL 专属特性(如 JSON 的EXTRACT、CONTAINS、PG 原生 ENUM 类型),取消对应函数的注释即可。此外App.kt还提供了runJSONandJSONBExamples()、runJavaTimeExamples()、runJodaTimeExamples()等独立入口,可按需单独执行某一类示例。
注意:示例代码中使用的包路径均为 Exposed 1.x 的新 API 命名空间(
org.jetbrains.exposed.v1.core、org.jetbrains.exposed.v1.jdbc等)。若你的项目使用更早的 Exposed 版本,包名通常为org.jetbrains.exposed.sql,需要按版本迁移指南调整 import。
三、构建与运行
README 给出的两条核心命令均在snippets目录下执行:
./gradlew :exposed-data-types:build./gradlew :exposed-data-types:run- build:编译整个模块。由于
snippets工程启用了 foojay-resolver,构建时会自动按需解析 JDK 工具链。 - run:执行
main()。README 明确说明运行效果是“执行查询、创建新表,并运行/examples文件夹中的所有函数”。 - 只跑某个示例:修改
App.kt(例如注释掉不需要的调用、只保留感兴趣的xxxExamples()函数),再重新运行./gradlew :exposed-data-types:run。
运行时建议保留addLogger(StdOutSqlLogger),这样每次插入/查询都会在控制台打印对应的 SQL 与参数,是学习“DSL 到底生成什么 SQL”的最佳途径。
四、基础类型示例:Numeric / Boolean / String
BasicTypesExamples.kt 用一个Users表集中展示了最常用的五类基础列:
object Users : Table() { val id = integer("id").autoIncrement() val name = varchar("name", NAME_LENGTH) // NAME_LENGTH = 50 val bio = text("bio") val age = short("age") val rating = decimal("rating", RATING_TOTAL_DIGITS, RATING_DECIMAL_DIGITS) // 5 total digits, 2 after decimal point val isActive = bool("is_active") override val primaryKey = PrimaryKey(id) }逐列解读(与 Exposed 的列类型 API 一一对应):
integer("id").autoIncrement():自增主键整数列,配合PrimaryKey(id)声明主键;varchar("name", 50):变长字符串,第二个参数为长度上限(常量NAME_LENGTH = 50);text("bio"):大文本列;short("age"):短整型(对应 SQL 的 SMALLINT);decimal("rating", 5, 2):定点小数,5 位总精度、小数点后 2 位(代码注释明确标注),适合存储评分等精确数值;bool("is_active"):布尔列。
与之配套的还有long、double、float、byte、char、blob、binary等列类型,完整清单可参考官方 Numeric-Boolean-String-Types.topic。这些基础类型共同构成了 Exposed 列定义的最小单元,是所有高级数据类型(数组、JSON 等)的基础。
五、数组类型:一维、带类型的二维与多维数组
ArrayExamples.kt 是仓库中信息量最大的示例之一,覆盖了数组列三种典型定义方式:
5.1 简单数组列(类型推断)
object SimpleArrays : Table("teams") { val memberIds = array<UUID>("member_ids") val memberNames = array<String>("member_names") val budgets = array<Double>("budgets") }泛型参数直接决定元素类型,Exposed 会自动映射到对应数据库的数组类型。
5.2 显式指定元素列类型 + 可空 + 默认值
object AdvancedArrays : Table("teams") { val memberNames = array<String>("member_names", VarCharColumnType(colLength = MEMBER_NAME_LENGTH)) val deadlines = array<LocalDate>("deadlines", KotlinLocalDateColumnType()).nullable() val expenses = array<Double?>("expenses", DoubleColumnType()).default(emptyList()) }要点:
- 第二个参数传入元素列类型(如
VarCharColumnType(colLength = 32)、KotlinLocalDateColumnType()、DoubleColumnType()),用于精确控制数组内部元素在数据库中的形态; .nullable()允许整列为空;.default(emptyList())设定空数组为默认值。
5.3 多维数组
object MultiDimArrays : Table("teams") { val nestedMemberIds = array<UUID, List<List<UUID>>>("nested_member_ids", dimensions = 2) val hierarchicalMemberNames = array<String, List<List<List<String>>>>( "hierarchical_member_names", VarCharColumnType(colLength = MEMBER_NAME_LENGTH), dimensions = 3 ) }array的第二个泛型参数声明 Kotlin 侧的嵌套容器类型,dimensions指定维度数——这里分别定义了二维 UUID 数组和三维字符串数组。
5.4 数组的查询能力:索引、切片与运算符
示例还演示了 Exposed 对数组的 DSL 级支持:
// 索引访问:取出第 1 个成员(索引从 1 开始) val firstMember = SimpleArrays.memberIds[FIRST_MEMBER_INDEX] // FIRST_MEMBER_INDEX = 1 SimpleArrays.select(firstMember) .where { SimpleArrays.budgets[FIRST_MEMBER_INDEX] greater MIN_BUDGET } // 切片:取第 1 到第 3 个成员名 SimpleArrays.select(SimpleArrays.memberNames.slice(SLICE_START, SLICE_END_SMALL)) // 集合运算符:ALL 与 ANY SimpleArrays.selectAll() .where { SimpleArrays.budgets[FIRST_MEMBER_INDEX] lessEq allFrom(SimpleArrays.budgets) } SimpleArrays.selectAll() .where { stringParam("Member A") eq anyFrom(SimpleArrays.memberNames.slice(SLICE_START, SLICE_END_LARGE)) }这里体现了 Exposed 数组 DSL 的完整能力:
- 索引:
column[i],示例常量FIRST_MEMBER_INDEX = 1说明数组下标按数据库习惯从 1 开始; - 切片:
slice(from, to)对应 SQL 的数组切片语法; - 全称量词
allFrom/ 存在量词anyFrom:分别对应 SQL 的<= ALL(...)与= ANY(...),可用于把数组元素与整列集合做比较; stringParam("Member A"):把 Kotlin 字符串作为查询参数绑定,避免 SQL 注入。
六、二进制类型:BLOB 与定长 binary
BinaryExamples.kt 展示了 Exposed 处理二进制数据的三种列:
object Files : Table() { val id = integer("id").autoIncrement() val name = varchar("name", 255) val content = blob("content") val simpleData = binary("simple_data") // 无长度版本 val thumbnail = binary("thumbnail", 1024) // 指定长度版本 override val primaryKey = PrimaryKey(id) }两者的区别(源码注释直接说明):
blob(...):对应 SQL 的 BLOB,适合大体积二进制内容(文件、图片等);binary(...):无长度时是“simple version”,指定长度(如binary("thumbnail", 1024))时是“length-specified version”,二者从结果集读取时都返回ByteArray。
6.1 写入与读取
Files.insert { it[name] = "example.txt" it[content] = ExposedBlob("Hello, World!".toByteArray()) // BLOB 用 ExposedBlob 包装 it[simpleData] = "simple binary data".toByteArray() // binary 直接传 ByteArray it[thumbnail] = "thumbnail data".toByteArray() } // 读取 val content = file[Files.content] val text = String(content.bytes) // 适合小文件 val text2 = content.inputStream.bufferedReader().readText() // 适合大文件关键 API 是ExposedBlob(位于org.jetbrains.exposed.v1.core.statements.api包):BLOB 列通过它包装ByteArray,并且提供.bytes(快速取字节数组)与.inputStream(流式读取大文件)两种读取方式;而binary列则直接以ByteArray读写,无需包装。
6.2 查询参数绑定优化
it[content] = blobParam(ExposedBlob("Some data".toByteArray()))blobParam(...)用于在查询语句中高效绑定 BLOB 参数(代码注释说明这是“Efficient handling of binary data in queries”),适用于插入、更新或 WHERE 条件中传递二进制数据。
七、枚举类型:四种定义方式
EnumerationExamples.kt 展示了 Exposed 枚举列的四种典型方案,覆盖从“存序号/存名字”到“对接数据库原生枚举”的全部场景:
enum class Foo { BAR, BAZ } // 1) 存储枚举的 ordinal 序号 object BasicEnumTable : Table() { val enumOrdinal = enumeration("enumOrdinal", Foo::class) } // 2) 存储枚举的名字(需指定列长度) object NamedEnumTable : Table() { val enumName = enumerationByName("enumName", ENUM_NAME_COLUMN_LENGTH, Foo::class) // 长度 10 }enumeration(...):把枚举按 ordinal 整数落库,插入时传Foo.BAR即可;enumerationByName(name, length, enumClass):把枚举按 name 字符串落库,第二个参数是该字符串列的长度(示例用常量 10)。
7.1 对接 PostgreSQL 原生 ENUM(自定义列类型)
class PGEnum<T : Enum<T>>(enumTypeName: String, enumValue: T?) : PGobject() { init { value = enumValue?.name type = enumTypeName } } object EnumTable : Table() { val enumColumn: Column<Foo> = customEnumeration( name = "enumColumn", sql = "FooEnum", // 数据库中的自定义类型名 fromDb = { value -> Foo.valueOf(value as String) }, toDb = { PGEnum("FooEnum", it) } ) }配套建表代码(createTableWithEnumColumn())先执行CREATE TYPE FooEnum AS ENUM ('BAR', 'BAZ');再SchemaUtils.create(EnumTable)。这里的PGEnum继承自 PostgreSQL 驱动的PGobject,用于把 Kotlin 枚举映射为 PG 原生枚举类型。
7.2 通用 ENUM 与既有枚举列
// 使用 SQL 内联 ENUM 定义 object NewEnumTable : Table() { val newEnumColumn: Column<Foo> = customEnumeration( name = "enumColumn", sql = "ENUM('BAR', 'BAZ')", fromDb = { value -> Foo.valueOf(value as String) }, toDb = { it.name } ) } // 对接数据库中已存在的枚举列(不写 sql,不负责建表) object ExistingEnumTable : Table() { val existingEnumColumn: Column<Foo> = customEnumeration( name = "enumColumn", fromDb = { value -> Foo.valueOf(value as String) }, toDb = { it.name } ) }customEnumeration是枚举场景的“瑞士军刀”:通过sql声明 DDL 片段(可以是类型名,也可以是内联的ENUM('BAR', 'BAZ')),通过fromDb/toDb完成数据库值与 Kotlin 值的双向转换。当表已经存在于数据库时(如示例中通过exec("CREATE TABLE IF NOT EXISTS EXISTINGENUM ...")预建),可以省略sql参数、只提供转换函数即可——这就是ExistingEnumTable的用法,对应insertEnumIntoTableWithExistingEnumColumn()示例。
八、JSON 与 JSONB:序列化、查询与函数
JSONandJSONBExamples.kt 依赖exposed-json模块,是示例中最复杂也最具实战价值的一节。
8.1 列定义:kotlinx.serialization 与 Jackson 双路线
@Serializable data class Project(val name: String, val language: String, val active: Boolean) val format = Json { prettyPrint = true } // 路线一:基于 kotlinx.serialization(推荐,等价于显式传 Project.serializer()) object TeamsTable : Table("team") { val groupId = varchar("group_id", 32) val project = json<Project>("project", format) } // 路线二:基于 Jackson 的 lambda 编解码 val mapper = jacksonObjectMapper() object JacksonTeamsTable : Table("team") { val groupId = varchar("group_id", 32) val project = json( "project", { mapper.writeValueAsString(it) }, // 序列化 { mapper.readValue<Project>(it) } // 反序列化 ) }json<T>(name, format)的第二个参数传入StringFormat(如Json.Default或带prettyPrint的Json实例)。代码注释明确:json<Project>("project", format)与json("project", format, Project.serializer())等价。若你的项目用 Jackson,则用三个参数的json(name, serializer, deserializer)形式。
8.2 JSON 数组列
object TeamProjectsTable : Table("team_projects") { val memberIds = json<IntArray>("member_ids", Json.Default) val projects = json<Array<Project>>("projects", Json.Default) }注释还给出了等价写法:@OptIn(ExperimentalSerializationApi::class) json("projects", Json.Default, ArraySerializer(Project.serializer()))。插入时直接赋值intArrayOf(1, 2, 3)与arrayOf(Project(...), Project(...)),生成的 SQL 形如INSERT INTO team_projects (member_ids, projects) VALUES ([1,2,3], [{...},{...}])(源码注释中给出了完整生成 SQL)。
8.3 JSON 操作函数:extract / exists / contains
示例对同一个project列演示了 Exposed 提供的四个 JSON 查询函数(import 自org.jetbrains.exposed.v1.json):
// extract:取出 JSON 字段,可再组合 lowerCase() 等表达式 val projectName = TeamsTable.project.extract<String>(".name") val languageIsKotlin = TeamsTable.project.extract<String>(".language").lowerCase() eq "kotlin" TeamsTable.select(projectName).where { languageIsKotlin } // exists:判断字段是否存在(还可传 filter path + optional 参数,如 PG 的 ? (@ == ...)) val hasActiveStatus = TeamsTable.project.exists(".active") // contains:判断 JSON 是否包含指定片段(MySQL 下还可加路径参数) val usesKotlin = TeamsTable.project.contains("{\"language\":\"Kotlin\"}") val usesKotlinWithPath = TeamsTable.project.contains("\"Kotlin\"", ".language")源码注释给出了useExtract()生成的 MySQL SQL 示例:
SELECT JSON_UNQUOTE(JSON_EXTRACT(team.project, "$.name")) FROM team WHERE LOWER(JSON_UNQUOTE(JSON_EXTRACT(team.project, "$.language"))) = 'kotlin'同时示例明确指出:exists/contains的可选参数(filter path、path、optional 参数)因数据库而异——PostgreSQL 的 filter path 写法(如".name ? (@ == \\$main)")与 MySQL 的路径参数写法(如".language")在代码中分别给出了独立函数。
8.4 JSONB 与 castToJson
object TasksTable : Table("tasks") { val complete = bool("complete").default(false) val project = jsonb<Project>("project", Json.Default) .default(Project("Main", "Kotlin", true)) }jsonb(...)用于 PostgreSQL 的 JSONB 列,示例注释展示了其生成的建表 SQL 与插入 SQL(VALUES (JSONB('{"name":"Main",...}')))。它还支持castToJsonFormat = false参数(见TasksRawTable)控制是否在 SQL 中加JSONB(...)转换。
最后,useJsonCast()演示了castToJson()的两种用法:把 JSONB 列当作普通表达式输出、或把存有 JSON 字符串的 varchar 列解析为类型化对象:
JsonCastTable.select( JsonCastTable.project.castToJson(), // 假设该列是 Project 类型的 JSONB 列 JsonCastTable.projectText.castToJson<Project>() // 假设该列是合法 JSON 字符串 ).toList()九、日期时间类型:三套 API 全对照
示例工程把日期时间拆成了三个文件,分别对应 Exposed 的三个时间模块。三者表结构高度相似,可对照学习。
9.1 java.time(exposed-java-time 模块)
JavaTimeExamples.kt:
object JavaTimeEvents : Table() { val id = integer("id").autoIncrement() val name = varchar("name", NAME_LENGTH) val startDate = date("start_date") val startTime = time("start_time").nullable() val createdAt = datetime("created_at").defaultExpression(CurrentDateTime) val lastModified = timestamp("last_modified") val scheduledAt = timestampWithTimeZone("scheduled_at") val period = duration("period") override val primaryKey = PrimaryKey(id) }插入时对应 Kotlin 类型:LocalDate→date、LocalTime→time、LocalDateTime→datetime、Instant→timestamp、OffsetDateTime→timestampWithTimeZone、Duration→duration。其中createdAt用defaultExpression(CurrentDateTime)让数据库在插入时自动写入当前时间。
9.2 Joda-Time(exposed-jodatime 模块)
JodaTimeExamples.kt 结构几乎一致,只是类型换成org.joda.time的DateTime、LocalTime,并额外展示了时区写法:
it[startDate] = DateTime(SAMPLE_YEAR, SAMPLE_MONTH, SAMPLE_DAY, STANDUP_HOUR, STANDUP_MINUTE) it[startTime] = LocalTime(STANDUP_HOUR, STANDUP_MINUTE) // 09:00 it[createdAt] = DateTime.now() it[scheduledAt] = DateTime.now().withZone(DateTimeZone.UTC)9.3 kotlinx-datetime(exposed-kotlin-datetime 模块)
KotlinDateTimeExamples.kt 面向 Kotlin 多平台场景,使用kotlinx.datetime:
object Events : Table() { val id = integer("id").autoIncrement() val name = varchar("name", NAME_LENGTH) val startDate = date("start_date") val startTime = time("start_time").nullable() val createdAt = datetime("created_at").defaultExpression(CurrentDateTime) @OptIn(ExperimentalTime::class) val lastModified = timestamp("last_modified") val scheduledAt = timestampWithTimeZone("scheduled_at") val period = duration("period") override val primaryKey = PrimaryKey(id) } // 插入:使用 kotlin.time.Clock 与 Duration it[startDate] = LocalDate(SAMPLE_YEAR, SAMPLE_MONTH, SAMPLE_DAY) it[startTime] = LocalTime(STANDUP_HOUR, STANDUP_MINUTE) // 09:00 it[createdAt] = Clock.System.now().toLocalDateTime(TimeZone.UTC) it[lastModified] = Clock.System.now() it[scheduledAt] = Clock.System.now().toJavaInstant().atOffset(ZoneOffset.UTC) it[period] = SAMPLE_DURATION.hours注意其中的@OptIn(ExperimentalTime::class):kotlin.time的Clock/InstantAPI 目前仍标记为实验性,使用时需要显式 OptIn。三套 API 的列名与语义完全一致(date、time、datetime、timestamp、timestampWithTimeZone、duration),差别仅在于绑定的 Kotlin 类型与所在模块(v1.javatime/v1.jodatime/v1.datetime包),这为项目在 JVM 与多平台之间切换时间库提供了无缝迁移路径。
十、执行顺序与联动:从 App.kt 看完整运行闭环
结合 App.kt 的默认 H2 路径,一次./gradlew :exposed-data-types:run的实际执行序列是:
Database.connect("jdbc:h2:mem:test", "org.h2.Driver", DatabaseConfig { useNestedTransactions = true })建立内存库连接;transaction(h2Db)开启事务,addLogger(StdOutSqlLogger)开启 SQL 打印;SchemaUtils.create(TeamsTable)建 JSON 示例所需表;- 执行
runKotlinDateTimeExamples()(建Events表并插入事件); - 执行
jsonExamples.example()(JSON 插入/更新/查询); - 执行
enumExamples.createTableWithExistingEnumColumn()与insertEnumIntoTableWithExistingEnumColumn()(预建枚举表 + 插入); - 执行
binaryExamples.basicUsage()与parameterBinding()(BLOB 读写与参数绑定)。
也就是说,默认运行并不会执行数组、基础类型等全部示例——ArrayExamples、BasicTypesExamples的方法在main()中未被调用。README 对此的指引是:“要只运行特定示例,请修改App.kt并重新运行”。这也是本示例项目被设计成“可自由裁剪”的沙盒的原因:按需打开注释即可。
十一、延伸阅读
本文涉及的每个数据类型家族,在仓库的 Writerside/topics 下都有对应的官方主题文档,示例代码正是它们的可运行载体:
- Numeric-Boolean-String-Types.topic — 对应
BasicTypesExamples.kt - Array-types.topic — 对应
ArrayExamples.kt - Binary-types.topic — 对应
BinaryExamples.kt - Enumeration-types.topic — 对应
EnumerationExamples.kt - json-and-jsonb-types.md — 对应
JSONandJSONBExamples.kt - date-and-time-types.md — 对应三套日期时间示例
- data-types.html — 聚合的数据类型速查页
若要在自己的工程中复现这些示例,需要按需引入exposed-core、exposed-jdbc、exposed-json、exposed-java-time/exposed-jodatime/exposed-kotlin-datetime模块,并参考 documentation-website/Writerside/snippets/settings.gradle.kts 中的多模块组织方式。从本仓库的模块划分(exposed-core、exposed-jdbc、exposed-r2dbc、exposed-dao等)看,exposed-json与各时间模块属于独立发行单元,按需取用即可。
- ORM
- 后端
- 数据存储
【免费下载链接】Exposed
Kotlin SQL Framework
相关推荐
Kotlin ORM框架Exposed终极指南:类型安全数据库操作完整教程
Kotlin ORM框架Exposed终极指南:类型安全数据库操作完整教程 在当今的Kotlin开发生态系统中,Exposed作为JetBrains官方推出的轻
ORM后端数据存储Synology硬盘兼容性突破:5步解锁第三方硬盘的完全指南
Synology硬盘兼容性突破:5步解锁第三方硬盘的完全指南 Synology_HDD_db是一个专门为群晖NAS设计的硬盘兼容性数据库修改工具,能够让你在群晖
运维存储CLIData-Science-For-Beginners 第 03 课:定义数据——掌握数据类型、数据结构与数据来源的完整分类框架
Data Science For Beginners 第 03 课:定义数据——掌握数据类型、数据结构与数据来源的完整分类框架 本课是 Data Science
数据科学教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考