ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Ent Go 框架谓词(Predicates)完全指南:字段过滤、边查询、自定义 SQL 与 JSON 谓词实战

Ent Go 框架谓词(Predicates)完全指南:字段过滤、边查询、自定义 SQL 与 JSON 谓词实战 Ent Go 框架谓词Predicates完全指南字段过滤、边查询、自定义 SQL 与 JSON 谓词实战【免费下载链接】entAn entity framework for Go项目地址: https://gitcode.com/gh_mirrors/en/entEnt 是面向 Go 的实体框架Entity Framework其生成的查询 API 通过谓词Predicates提供类型安全、可组合的过滤能力。本文以官方文档 doc/md/predicates.md 为骨架深入拆解 Ent 谓词体系从字段谓词、边谓词到 NOT / OR / AND 逻辑组合再到可完全掌控 SQL 的自定义谓词与官方sqljson包提供的 JSON 列查询能力。读完本文你将能够熟练编写从简单等值过滤到复杂子查询、JSON 路径匹配在内的各类 Ent 查询并理解其底层的 SQL 生成原理。字段谓词Field PredicatesEnt 为每种字段类型生成对应的谓词函数调用方式是Type.FieldOp(value)例如user.NameEQ(a8m)、pet.AgeGT(3)。谓词函数接受一个或多个参数返回一个可用于Where(...)的谓词值。不同类型的字段支持的谓词由代码生成器按类型严格裁剪其操作符集合定义在 entc/gen/predicate.go 中const ( EQ Op iota // NEQ // GT // GTE // LT // LTE // IsNil // IS NULL / has NotNil // IS NOT NULL / hasNot In // within NotIn // without EqualFold // equals case-insensitive Contains // containing ContainsFold // containing case-insensitive HasPrefix // startingWith HasSuffix // endingWith )同一文件中还定义了各类型可用的操作符集合entc/gen/predicate.goboolOps []Op{EQ, NEQ} enumOps append(boolOps, In, NotIn) numericOps append(enumOps, GT, GTE, LT, LTE) stringOps append(numericOps, Contains, HasPrefix, HasSuffix) nillableOps []Op{IsNil, NotNil}由此可以得出各字段类型的完整谓词能力矩阵字段类型支持的谓词Bool,!即EQ/NEQNumericint、float 等,!,,,,以及IN,NOT INTime,!,,,,以及IN,NOT INString,!,,,,IN,NOT INContains,HasPrefix,HasSuffixContainsFold,EqualFold为SQL 方言专属大小写不敏感匹配JSON,!嵌套值JSON path上的,!,,,,与ContainsHasKey,LenP嵌套值的null检查详见下文 JSON 谓词一节Optional可空字段IsNil,NotNil对应 SQL 的IS NULL/IS NOT NULL说明IN与NOT IN是变参操作符Variadic接受一个值列表IsNil/NotNil是零参操作符Niladic无需传值。代码生成模板 entc/gen/template/dialect/sql/predicate.tmpl 会根据这两个特性决定生成调用时的参数形态如ids...展开。使用示例// 等值与比较 client.User.Query().Where(user.NameEQ(a8m)).All(ctx) client.Pet.Query().Where(pet.AgeGT(3), pet.AgeLTE(10)).All(ctx) // 集合匹配 client.User.Query().Where(user.IDIn(1, 2, 3)).All(ctx) client.User.Query().Where(user.NameNotIn(a8m, foo)).All(ctx) // 字符串模式 client.Pet.Query().Where(pet.NameContains(ri)).All(ctx) client.Pet.Query().Where(pet.NameHasPrefix(Ari)).All(ctx) client.Pet.Query().Where(pet.NameHasSuffix(a)).All(ctx) // 可空字段 client.User.Query().Where(user.PhoneIsNil()).All(ctx) client.User.Query().Where(user.PhoneNotNil()).All(ctx)其中In/NotIn的空参数列表被底层 dialect/sql/builder.go 特殊处理In无参数时生成FALSE常量NotIn无参数时生成NOT FALSE避免产生非法 SQLIN ()。边谓词Edge Predicates边谓词用于按关联关系过滤查询是 Ent 图遍历能力的核心入口其模板实现位于 entc/gen/template/dialect/sql/predicate.tmpl。HasEdgeHasEdge检查实体是否存在某条关联边。例如对Pet类型的名为owner的边查询所有有主人的宠物client.Pet. Query(). Where(pet.HasOwner()). All(ctx)从底层实现看HasOwner()会构建一个sqlgraph.Step并调用sqlgraph.HasNeighbors见 dialect/sql/sqlgraph/graph.go。根据边的关系类型HasNeighbors会生成不同的 SQL多对多边通过中间表生成IN (SELECT ...)子查询反向单边检查外键列是否为NOT NULL正向单边生成EXISTS (SELECT ...)子查询。HasEdgeWithHasEdgeWith在HasEdge基础上允许对边的另一端实体追加谓词过滤。例如查询主人名为a8m的所有宠物client.Pet. Query(). Where(pet.HasOwnerWith(user.Name(a8m))). All(ctx)HasEdgeWith会调用sqlgraph.HasNeighborsWith见 dialect/sql/sqlgraph/graph.go把传入的谓词应用到邻居表的Selector上再通过IN子查询或EXISTS组合进主查询。HasEdgeWith支持传入多个谓词它们之间是 AND 关系。逻辑组合否定NOT、析取OR与合取ANDWhere接受多个谓词时默认按 AND 组合但 Ent 还提供了显式的逻辑组合函数用于表达更复杂的布尔条件。否定 NOTclient.Pet. Query(). Where(pet.Not(pet.NameHasPrefix(Ari))). All(ctx)pet.Not将内部谓词取反等价于 SQL 的NOT (...)。它同样可以包裹pet.Or(...)、pet.And(...)等组合谓词。析取 ORclient.Pet. Query(). Where( pet.Or( pet.HasOwner(), pet.Not(pet.HasFriends()), ) ). All(ctx)pet.Or接收多个谓词任一为真即匹配等价于(... OR ...)。合取 ANDclient.Pet. Query(). Where( pet.And( pet.HasOwner(), pet.Not(pet.HasFriends()), ) ). All(ctx)pet.And要求所有谓词同时为真等价于(... AND ...)。底层通过 dialect/sql/builder.go 的Or/And实现组合谓词会用括号包裹以避免运算符优先级问题。三种逻辑组合可以任意嵌套例如pet.Or(pet.And(pet.HasOwner(), pet.AgeGT(2)), pet.Not(pet.HasFriends()))。自定义谓词Custom Predicates当生成的标准谓词无法满足需求——例如需要编写方言专属逻辑或完全控制生成的查询时Ent 允许通过Where(func(s *sql.Selector) { ... })传入一个操作sql.Selector的闭包。这是整个谓词体系中最强大也最灵活的入口sql.Selector的完整 API 定义在 dialect/sql/builder.go 中。示例 1按 ID 列表过滤IN 子句获取用户 1、2、3 的宠物pets : client.Pet. Query(). Where(func(s *sql.Selector) { s.Where(sql.InInts(pet.FieldOwnerID, 1, 2, 3)) }). AllX(ctx)sql.InInts是IN谓词的 int 专用版本见 dialect/sql/builder.go。上述代码生成的 SQLSELECT DISTINCT pets.id, pets.owner_id FROM pets WHERE owner_id IN (1, 2, 3)示例 2JSON 字段路径检查统计URL字段包含Scheme键的用户数count : client.User. Query(). Where(func(s *sql.Selector) { s.Where(sqljson.HasKey(user.FieldURL, sqljson.Path(Scheme))) }). CountX(ctx)生成的 SQL 按方言不同而不同-- PostgreSQL SELECT COUNT(DISTINCT users.id) FROM users WHERE url-Scheme IS NOT NULL -- SQLite 和 MySQL SELECT COUNT(DISTINCT users.id) FROM users WHERE JSON_EXTRACT(url, $.Scheme) IS NOT NULL方言差异由sqljson.HasKey内部实现见 dialect/sql/sqljson/sqljson.goPostgreSQL 使用-操作符MySQL/SQLite 使用JSON_EXTRACTSQLite 实际用JSON_TYPE判断非 NULL。示例 3关联子查询的三种等价写法查询所有拥有Tesla汽车的用户的普通 Ent 写法users : client.User.Query(). Where(user.HasCarWith(car.Model(Tesla))). AllX(ctx)该查询可以用IN、JOIN、EXISTS三种 SQL 形式等价重写// IN 版本。 users : client.User.Query(). Where(func(s *sql.Selector) { t : sql.Table(car.Table) s.Where( sql.In( s.C(user.FieldID), sql.Select(t.C(user.FieldID)).From(t).Where(sql.EQ(t.C(car.FieldModel), Tesla)), ), ) }). AllX(ctx) // JOIN 版本。 users : client.User.Query(). Where(func(s *sql.Selector) { t : sql.Table(car.Table) s.Join(t).On(s.C(user.FieldID), t.C(car.FieldOwnerID)) s.Where(sql.EQ(t.C(car.FieldModel), Tesla)) }). AllX(ctx) // EXISTS 版本。 users : client.User.Query(). Where(func(s *sql.Selector) { t : sql.Table(car.Table) p : sql.And( sql.EQ(t.C(car.FieldModel), Tesla), sql.ColumnsEQ(s.C(user.FieldID), t.C(car.FieldOwnerID)), ) s.Where(sql.Exists(sql.Select().From(t).Where(p))) }). AllX(ctx)三者生成等价结果的 SQL-- IN 版本。 SELECT DISTINCT users.id, users.age, users.name FROM users WHERE users.id IN (SELECT cars.owner_id FROM cars WHERE cars.model Tesla) -- JOIN 版本。 SELECT DISTINCT users.id, users.age, users.name FROM users JOIN cars ON users.id cars.owner_id WHERE cars.model Tesla -- EXISTS 版本。 SELECT DISTINCT users.id, users.age, users.name FROM users WHERE EXISTS (SELECT * FROM cars WHERE cars.model Tesla AND users.id cars.owner_id)EXISTS版本中使用的sql.Exists/sql.NotExists实现位于 dialect/sql/builder.go。三种写法在语义上等价实际选择取决于表大小、索引与优化器行为。示例 4自定义 LIKE 模式生成代码提供了HasPrefix、HasSuffix、Contains、ContainsFold谓词但如果需要LIKE运算符的自定义模式如通配符_、%可以这样做pets : client.Pet.Query(). Where(func(s *sql.Selector){ s.Where(sql.Like(pet.Name, _B%)) }). AllX(ctx)sql.Like实现见 dialect/sql/builder.go。生成的 SQLSELECT DISTINCT pets.id, pets.owner_id, pets.name, pets.age, pets.species FROM pets WHERE name LIKE _B%示例 5使用内置 SQL 函数DATE 等需要使用DATE()等内置 SQL 函数时有两种方式方式 1方言感知的谓词函数sql.Pusers : client.User.Query(). Select(user.FieldID). Where(func(s *sql.Selector) { s.Where(sql.P(func(b *sql.Builder) { b.WriteString(DATE().Ident(last_login_at).WriteByte()).WriteOp(OpGTE).Arg(value) })) }). AllX(ctx)生成的 SQLSELECT id FROM users WHERE DATE(last_login_at) ?方式 2内联表达式sql.ExprPusers : client.User.Query(). Select(user.FieldID). Where(func(s *sql.Selector) { s.Where(sql.ExprP(DATE(last_login_at) ?, value)) }). AllX(ctx)生成的 SQL 相同SELECT id FROM users WHERE DATE(last_login_at) ?sql.P与sql.ExprP均定义于 dialect/sql/builder.gosql.P接受一个操作sql.Builder的闭包可在闭包内使用WriteString、Ident、WriteOp、Arg等构建方法按方言拼接任意表达式sql.ExprP则直接接收一个带?占位符的表达式字符串与参数列表。前者更灵活可按方言分支后者更简洁。JSON 谓词sqljson 包JSON 谓词不会作为代码生成的一部分默认生成。Ent 提供了官方包sqljson源码位于 dialect/sql/sqljson/sqljson.go用于配合上文的自定义谓词能力对 JSON 列进行查询。使用时将返回的*sql.Predicate传给s.Where(...)即可。比较 JSON 值sqljson.ValueEQ(user.FieldData, data) sqljson.ValueEQ(user.FieldURL, https, sqljson.Path(Scheme)) sqljson.ValueNEQ(user.FieldData, content, sqljson.DotPath(attributes[1].body.content)) sqljson.ValueGTE(user.FieldData, status.StatusBadRequest, sqljson.Path(response, status))ValueEQ/ValueNEQ/ValueGT/ValueGTE/ValueLT/ValueLTE分别对应、!、、、、比较。Path按段指定 JSON 路径DotPath则接受a.b[2].c形式的点分字符串解析器ParsePath见 dialect/sql/sqljson/sqljson.go支持引号、数组索引等。对 PostgreSQL比较非字符串参数时会自动附加类型转换如Cast(int)、Cast(bool)以避免 missing type casts 错误这是normalizePG函数dialect/sql/sqljson/sqljson.go的职责。检查 JSON 键是否存在sqljson.HasKey(user.FieldData, sqljson.Path(attributes, [1], body)) sqljson.HasKey(user.FieldData, sqljson.DotPath(attributes[1].body))注意值为null字面量的键也会匹配该操作HasKey判定的是键存在且非 NULL。检查 JSONnull字面量sqljson.ValueIsNull(user.FieldData) sqljson.ValueIsNull(user.FieldData, sqljson.Path(attributes)) sqljson.ValueIsNull(user.FieldData, sqljson.DotPath(attributes[1].body))注意ValueIsNull仅在值是JSONnull字面量时返回 true而不是数据库NULL。若需检查数据库 NULL 或键是否存在应使用sql.IsNull或sqljson.HasKey见 dialect/sql/sqljson/sqljson.go 的实现说明。比较 JSON 数组长度sqljson.LenEQ(user.FieldAttrs, 2) sql.Or( sqljson.LenGT(user.FieldData, 10, sqljson.Path(attributes)), sqljson.LenLT(user.FieldData, 20, sqljson.Path(attributes)), )长度谓词族包括LenEQ、LenNEQ、LenGT、LenGTE、LenLT、LenLTE见 dialect/sql/sqljson/sqljson.go。底层按方言生成JSONB_ARRAY_LENGTHPostgreSQL、JSON_LENGTHMySQL或JSON_ARRAY_LENGTHSQLite。sqljson还提供OrderLen/OrderLenDesc按 JSON 长度排序以及ValuePath/OrderValue按 JSON 值排序的辅助函数。检查 JSON 值包含另一个值sqljson.ValueContains(user.FieldData, data) sqljson.ValueContains(user.FieldData, attrs, sqljson.Path(attributes)) sqljson.ValueContains(user.FieldData, code, sqljson.DotPath(attributes[0].status_code))实现见 dialect/sql/sqljson/sqljson.goMySQL 生成JSON_CONTAINS(...)PostgreSQL 生成包含操作符SQLite 则通过JSON_EACH展开后做等值匹配。检查 JSON 字符串的前缀、后缀与子串sqljson.StringContains(user.FieldURL, github, sqljson.Path(host)) sqljson.StringHasSuffix(user.FieldURL, .com, sqljson.Path(host)) sqljson.StringHasPrefix(user.FieldData, 20, sqljson.DotPath(attributes[0].status_code))这三个函数内部会自动附加Unquote(true)选项去除 JSON 字符串的引号后再做模式匹配见 dialect/sql/sqljson/sqljson.go。检查 JSON 值是否属于列表sqljson.ValueIn(user.FieldURL, []any{https, ftp}, sqljson.Path(Scheme)) sqljson.ValueNotIn(user.FieldURL, []any{github, gitlab}, sqljson.Path(Host))ValueNotIn在参数为空时退化为sql.NotIn(column)见 dialect/sql/sqljson/sqljson.go若参数全是字符串会自动附加Unquote(true)。字段间比较Comparing Fieldsdialect/sql包还提供一组字段与字段比较的函数用于比较同一行内两个字段列的大小关系而不是与常量比较。这些函数定义在 dialect/sql/sql.go 中。client.Order.Query(). Where( sql.FieldsEQ(order.FieldTotal, order.FieldTax), sql.FieldsNEQ(order.FieldTotal, order.FieldDiscount), ). All(ctx) client.Order.Query(). Where( order.Or( sql.FieldsGT(order.FieldTotal, order.FieldTax), sql.FieldsLT(order.FieldTotal, order.FieldDiscount), ), ). All(ctx)完整的字段间比较函数族包括FieldsEQ、FieldsNEQ、FieldsGT、FieldsGTE、FieldsLT、FieldsLTE以及FieldsHasPrefix、FieldsHasSuffix、FieldsContains、FieldsContainsFold、FieldsEqualFold等字符串变体。它们底层复用sql.ColumnsEQ、sql.ColumnsGT等列间比较谓词见 dialect/sql/builder.go 的ColumnsOp并可通过sql.FieldsEQ(...)与order.Or(...)/order.And(...)组合嵌套。注意这些函数返回的是func(*Selector)可直接作为Where的参数。谓词与代码生成的关系理解谓词体系后可以顺带理清其与代码生成的联系每个实体类型如User、Pet的谓词常量user.FieldName、pet.FieldOwnerID等与谓词函数pet.NameHasPrefix、pet.HasOwner都由entc代码生成器产出模板位于 entc/gen/template/dialect/sql/predicate.tmpl 与 entc/gen/template/dialect/sql/where.tmpl。标准字段谓词最终映射为sql.FieldEQ、sql.FieldGT等底层函数dialect/sql/sql.go而边谓词映射为sqlgraph.HasNeighbors/sqlgraph.HasNeighborsWithdialect/sql/sqlgraph/graph.go。pet.And、pet.Or、pet.Not由模板生成时引用sql.AndPredicates、sql.OrPredicates、sql.NotPredicates见 entc/gen/template/dialect/sql/predicate.tmpl。因此无论是使用生成的类型安全谓词还是直接操作sql.Selector编写自定义 SQL 逻辑最终都汇聚到dialect/sql与dialect/sql/sqlgraph这两个底层包这也是 Ent 谓词 API 既保持类型安全、又能完整下探到原生 SQL 的原因所在。更多谓词与查询的配套用法分页、聚合、遍历等可进一步参考仓库中的 doc/md/crud.mdx、doc/md/paging.mdx 与 doc/md/predicates.md 原文。【免费下载链接】entAn entity framework for Go项目地址: https://gitcode.com/gh_mirrors/en/ent创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表