ARTICLE DETAIL

资讯详情

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

SQLDelight 查询参数(Bind Args)完整指南:类型推断、命名参数与可变参数实战

SQLDelight 查询参数(Bind Args)完整指南:类型推断、命名参数与可变参数实战 后端ORM【免费下载链接】sqldelightSQLDelight - Generates typesafe Kotlin APIs from SQL项目地址https://gitcode.com/gh_mirrors/sq/sqldelight点击查看免费下载导读本文围绕 SQLDelight 中.sq文件内的查询参数query arguments / bind args机制展开系统讲解 SQLDelight 如何把 SQL 语句中的?占位符转换为类型安全、可空性安全的 Kotlin 函数参数涵盖类型推断、命名参数、IN子句可变参数、INSERT数据类绑定以及输入消毒等完整主题。读完本文你将掌握在 Android SQLite以及各平台、各方言场景下正确编写带参数查询语句、理解编译期参数类型推断规则并能用源码级证据解释参数命名与去重等底层行为。本文主体内容来自仓库文档 docs/android_sqlite/query_arguments.md该文档通过模板包含机制引用了 docs/common/query_arguments_sqlite.md 与 docs/common/query_arguments.md 的完整内容。一、Bind Args 基础?占位符与生成方法签名SQLDelight 的.sq文件在参数语法上与 SQLite 原生保持完全一致其中?形式的占位符对应 SQLite 的 bind args绑定参数。这是 SQLDelight 查询参数体系的最底层约定只要一条被命名的语句中包含 bind args生成的 Kotlin 方法就必然要求对应数量的参数。这一点在 docs/common/query_arguments_sqlite.md 中明确说明.sq文件使用与 SQLite 完全相同的语法包括 SQLite Bind Args。如果一条语句包含 bind args与之关联的生成方法就需要相应的参数。例如定义如下带占位符的查询selectByNumber: SELECT * FROM hockeyPlayer WHERE player_number ?;SQLDelight 会在生成的PlayerQueries对象上生成对应方法selectByNumber(player_number: Long)调用时必须传入该参数val playerQueries: PlayerQueries database.playerQueries val selectNumber10 playerQueries.selectByNumber(player_number 10) println(selectNumber10.executeAsOne()) // Prints Corey Perry这里的player_number 10是 Kotlin 具名实参named argument其参数名player_number并非凭空而来——它由编译期从占位符所对应的列名自动推断详见下文命名推断。从编译器实现看.sq中每条带标签的语句都会被收集为一个可绑定查询Bindable Query。在 BindableQuery.kt 中parameters被定义为该查询在生成 API 中暴露的参数集合普通查询按arguments.sortedBy { it.index }排序得到而INSERT语句在满足特定条件时会退化为单参数见INSERT 数据类绑定小节。每个参数对应一个绑定索引index生成的查询方法签名由此确定。1.1 一个查询包含多个占位符一个语句中可以有多个?占位符生成的方法会按占位符出现顺序即绑定索引顺序依次接收参数。BindArgsTest中的二进制表达式测试展示了三参数场景见 BindArgsTest.ktsomeSelect: SELECT * FROM User WHERE type ? AND first_name LIKE ? AND last_name LIKE ?;编译器会将三个占位符分别解析为type、first_name、last_name三个参数且各自类型与对应列的类型一致TEXT映射为String。二、类型推断参数类型与可空性自动推导SQLDelight 会为运行时参数自动推断正确的类型与可空性nullability包括自定义列类型custom column types。这是其类型安全 API的核心能力之一也是 docs/common/query_arguments.md 首先强调的特性。回到开头的示例selectByNumber: SELECT * FROM hockeyPlayer WHERE player_number ?;player_number列的 SQLite 类型是INTEGER因此生成的参数类型为Long如果列被定义为TEXT参数类型则对应为String如果列允许为空无NOT NULL参数类型会自动变为可空类型String?等。2.1 推断的来源列、别名与表达式上下文从 BindArgsTest.kt 的测试可以看到参数类型推断的完整规则从列推断占位符与某列比较时参数继承该列的方言类型与 Kotlin 类型参数名也继承列名bind arg inherit name from column测试从别名推断占位符与SELECT ... AS data_id生成的别名比较时参数名与类型继承别名bind args inherit name from alias测试从子查询别名推断多层嵌套的子查询SELECT id AS some_alias同样可继承bind args inherit alias name测试从复合查询推断UNION等复合 SELECT 中VALUES (?)里的占位符类型从另一侧复合查询的列推断bind args in compound select inherit type from compounded query测试即使包着多层括号VALUES (((?)))也能解析INSERT 的 VALUES 占位符VALUES (?), (?)中的每个占位符都继承对应列的类型与名称见bind args inherit names in insert statements与default insert statements测试UPDATE / UPSERT 推断SET id ? WHERE id ?以及INSERT ... ON CONFLICT DO UPDATE SET中的参数同样继承列类型见bind args for update statements ...与bind args for upsert do update statements ...测试IN子句WHERE id IN ?会推断为集合参数见下文可变参数LIKE与ESCAPEurl LIKE :urlLike ESCAPE :escape中:urlLike与:escape均为TEXT类型见like bind args have correct types in binary op expression测试。2.2 二元表达式中的类型推断与算术上下文参数出现在算术/比较表达式内部时推断会更复杂WHERE datum :datum1 - 2.5 AND datum :datum2 2.5由于与2.5REAL进行算术运算datum1、datum2被推断为Long见 BindArgsTest.ktSELECT CAST(:datum1 AS REAL) CAST(:datum2 AS INTEGER) - 10.5CAST显式决定了参数类型——CAST(... AS REAL)推断为Double?CAST(... AS INTEGER)推断为Long?自定义类型 区间运算PostgreSQL 方言下created_at :createdAt - INTERVAL 2 days可推断出Instant类型参数见bind arg in binary expression can be cast as custom type测试。2.3 无法推断时的编译提示使用 CAST类型推断并非总是成功。当参数出现在 SQL 类型不确定的上下文例如MAX(:input)这种多态聚合函数时编译器无法确定 Kotlin 类型会直接抛出错误The Kotlin type of the argument cannot be inferred, use CAST instead.BindArgsTest用assertFailsWithIllegalStateException验证了这一行为见 BindArgsTest.ktmaxSupportsManySqlTypes: SELECT 1 FROM dummy WHERE MAX(:input) 1; -- 报错无法推断请使用 CAST但MAX(1, :input)因为其他实参整数1提供了类型锚点input可以被推断为Long?见bind arg kotlin type can be inferred with other types测试而MAX(1, FOO, :input)因混合INTEGER与TEXT而再次失败。实战建议当参数处于类型模糊的表达式聚合函数、多态函数等中时显式使用CAST(:param AS 具体类型)来锚定类型。三、命名参数与索引参数两种绑定方式SQLDelight 同时支持命名参数与索引参数二者可以混用。命名参数使用:name前缀索引参数使用?index形式如?1、?2未编号的?则按出现顺序自动分配索引。3.1 命名参数文档示例见 docs/common/query_arguments.md 的 Named Arguments 小节firstOrLastName: SELECT * FROM hockeyPlayer WHERE full_name LIKE (% || :name) OR full_name LIKE (:name || %);生成的 Kotlin 方法可以直接按名字传参playerQueries.firstOrLastName(name Ryan)注意命名参数在 SQL 中即使出现多次如上例:name用了两次生成的方法也只有一个参数——同一名字的占位符共享同一个参数。这正是 BindableQuery.kt 中namesSeen集合的作用重复名称的 bind 表达式会被合并进同一个Argument。3.2 索引参数与自动索引索引参数用于精确控制绑定位置。测试用例见 BindableQueryTest.kt揭示了如下规则同索引复用WHERE _id ?1 AND _id ?1只生成一个参数WHERE _id ? AND _id ?1中未编号的?自动取到已使用的索引1同样合并为一个参数arguments with the same index are reused、argument indexed to an already-used index is reused测试自动取下一个可用索引WHERE _id ?20 AND value ?中第二个?自动分配索引21auto-index takes next available index测试生成方法的参数顺序按索引排序。3.3 命名冲突的自动消解当自动生成的参数名与用户显式指定的参数名冲突时编译器会自动为后出现的参数追加下划线后缀_。例如selectForStuff: SELECT * FROM data WHERE _id :value AND value ?;第二个占位符原本应继承列名value但与用户命名参数:value冲突于是被重命名为value_见 BindableQueryTest.kt 的auto-generated parameter name conflicts with user-specified name测试。对应的消解逻辑位于 BindableQuery.kt编译器循环追加_直到名字唯一。实战提示当你看到生成的参数名带_后缀时说明它与另一个参数重名建议主动给其中一个占位符起个不同的名字让代码更可读。四、可变参数把集合传给IN子句SQLDelight 支持把**一组值集合**作为单个参数传入典型场景是IN子句。文档示例Variable Arguments 小节selectByNames: SELECT * FROM hockeyPlayer WHERE full_name IN ?;注意语法要点IN后面直接跟?而非IN (?)这样 SQLDelight 才能识别这是一个集合参数并展开为IN (?, ?, ...)。调用时传入一个 Kotlin 集合playerQueries.selectByNames(listOf(Alec, Jake, Matt))生成的参数类型为CollectionString之类的集合类型。编译器通过SqlBindExpr.isSqlInExprArrayParameter()等辅助函数识别IN子句参数见 BindArgsTest.kt 的bind args for in statement inherit column name测试此时参数类型、名称仍继承自IN左侧的列如id IN ?的集合元素类型为列的 Kotlin 类型。需要说明的是不同方言的集合参数写法略有差异PostgreSQL 方言还支持WHERE data.id ANY (?)此时参数推断为CollectionInt集合类型且因包裹在括号中参数名默认为value见 BindArgsTest.kt 的两个ANY操作符测试。五、INSERT 参数绑定到表的数据类INSERT VALUES语句的参数可以直接绑定到**该表生成的数据类data class**上这是 SQLDelight 提高写入代码可读性的典型特性。文档示例Inserts 小节insertPlayer: INSERT INTO hockeyPlayer VALUES ?;VALUES ?中的?表示整行数据作为参数。生成的方法接收HockeyPlayer数据类实例val rickardRakell HockeyPlayer( full_name Rickard Rakell, number 67 ) playerQueries.insertPlayer(rickardRakell)这里HockeyPlayer正是 SQLDelight 为hockeyPlayer表生成的数据类构造函数参数与表列一一对应。从源码看编译器对这类语句做了专门处理在 BindableQuery.kt 中当语句是INSERT且满足接受表接口acceptsTableInterface()条件时parameters直接退化为单个参数——类型为以表名命名的数据类参数名即表名其arguments则按表的列展开每个绑定位置继承对应列的类型与可空性见 BindableQuery.kt。另一个值得注意的细节对于INTEGER PRIMARY KEY列INSERT时该列参数会被自动置为可空asNullable()因为 SQLite 允许主键以NULL插入并自动分配自增值见 BindableQuery.kt 的注释与逻辑。如果不想绑定整行数据类也可以显式列出列并逐个传参例如 docs/common/index_queries.md 中的写法insert: INSERT INTO hockeyPlayer(player_number, full_name) VALUES (?, ?);playerQueries.insert(player_number 10, full_name Corey Perry)两种方式生成的 API 风格不同VALUES (?, ?)生成多参数方法VALUES ?生成单数据类参数方法。六、输入消毒占位符与驱动层实现最后一个主题是安全性。SQLDelight 的机制是通过查询占位符placeholder把参数传入查询而非拼接 SQL 字符串。因此实际的参数消毒sanitization即正确转义、类型转换与空值处理由各平台、各方言的底层驱动实现driver implementation负责而不是 SQLDelight 编译期或运行时本身见 docs/common/query_arguments.md 的 Input Sanitization 小节。这意味着你编写的?占位符参数不会以字符串拼接方式注入 SQL从根本上规避了 SQL 注入类问题具体的绑定行为如null绑定为 SQLNULL、Long/String与列类型的映射、批量展开等取决于所选驱动Android 原生驱动、JDBC 驱动、native 驱动、JS/SQL.js 驱动等及方言SQLite、PostgreSQL、MySQL、HSQL 等。从运行时接口可以印证这一点。所有驱动都必须实现 SqlDriver.kt 中的execute方法其签名是fun execute( identifier: Int?, sql: String, parameters: Int, binders: (SqlPreparedStatement.() - Unit)? null, ): QueryResultLong其中binders是一个在语句执行前把参数绑定到SqlPreparedStatement的 lambda——消毒/绑定正是在这个驱动层回调里完成的。生成代码负责把每个具名/索引参数按顺序填入该回调具体取值与类型转换由具体驱动决定。6.1 使用建议始终通过占位符传参不要用字符串拼接拼 SQL若想观察驱动实际执行的绑定过程可借助LogSqliteDriver等日志驱动位于 runtime打印执行语句与参数注意不同方言对集合参数IN ?与 PostgreSQLANY (?)的处理方式不同切换方言时需同步调整写法。七、完整实战示例从表定义到参数化调用综合以上所有特性一个完整的.sq文件可以是-- src/main/sqldelight/com/example/hockey/data/Player.sq CREATE TABLE hockeyPlayer ( player_number INTEGER NOT NULL, full_name TEXT NOT NULL ); selectByNumber: SELECT * FROM hockeyPlayer WHERE player_number ?; selectByName: SELECT * FROM hockeyPlayer WHERE full_name :name; selectByNames: SELECT * FROM hockeyPlayer WHERE full_name IN ?; insertPlayer: INSERT INTO hockeyPlayer VALUES ?;生成的PlayerQueries提供playerQueries.selectByNumber(player_number 10) // 参数类型 Long playerQueries.selectByName(name Ryan Getzlaf) // 参数类型 String playerQueries.selectByNames(listOf(Alec, Jake, Matt)) // 参数类型 CollectionString playerQueries.insertPlayer(HockeyPlayer(67, Rickard Rakell)) // 参数类型 HockeyPlayer结语SQLDelight 的查询参数体系在编译期完成了三项关键工作类型与可空性推断、命名/索引解析与冲突消解、集合参数识别最终把参数化的 SQL 转换为类型安全的 Kotlin API而将底层绑定与消毒交给各平台驱动。理解这些规则尤其是类型推断失败时的CAST提示、同名参数合并、索引复用与命名冲突自动消解能帮助你在实际项目中写出更干净、更可预测的.sq文件。想深入验证上述行为可直接阅读仓库中的编译器测试 BindArgsTest.kt 与 BindableQueryTest.kt以及核心模型 BindableQuery.kt。赞分享后端ORM【免费下载链接】sqldelightSQLDelight - Generates typesafe Kotlin APIs from SQL项目地址https://gitcode.com/gh_mirrors/sq/sqldelight点击查看免费下载相关推荐SQLDelight查询参数详解类型安全参数传递的完整教程SQLDelight查询参数详解类型安全参数传递的完整教程 SQLDelight是一个强大的Kotlin数据库库能够从SQL语句生成类型安全的Kotlin后端ORM告别SQL参数混乱DBeaver参数化查询命名最佳实践指南告别SQL参数混乱DBeaver参数化查询命名最佳实践指南 你是否遇到过这样的情况接手一个项目时面对满屏的 ? 和无意义的 param1 、 var2 参数据库客户端桌面应用数据库FluidNC 参数列表命令 $PL 完全指南全局命名参数与作业局部参数的查询、作用域与调试实践FluidNC 参数列表命令 $PL 完全指南全局命名参数与作业局部参数的查询、作用域与调试实践 导读 $PL Parameters/List是 Flui嵌入式固件硬件开发智能硬件上一篇UIEffect特效插件终极安装配置指南下一篇如何永久保存微信聊天记录WeChatMsg免费导出与年度报告完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表