Core Data 谓词使用与文本搜索优化全解析
1. 子查询的使用场景
在通过关系匹配多个属性时,我们需要使用子查询。例如,要匹配至少有一个居民年龄小于 25 岁且拥有 2 辆车的所有城市,可以使用以下代码:
SUBQUERY(residents, $x, $x.age < 25 AND $x.carsOwnedCount == 2).@count != 0
这里会对每个
Person
单独评估年龄和车辆拥有数量的条件。与之对比的是:
ANY resident.age < 25 AND ANY resident.carsOwnedCount == 2
这个条件会匹配有年龄小于 25 岁居民和拥有 2 辆车居民的任何城市,但不考虑这两个条件是否针对同一个居民。
2. 匹配对象和对象 ID
谓词的一个强大功能是可以直接匹配对象,可通过匹配对象或其对象标识符来实现。直接匹配对象看似奇怪,但执行匹配该对象的获取请求时,会强制 Core Data 从文件系统重新加载该对象并更新行缓存,可确保对象不是故障对象,示例代码如下:
let request = Person.sortedFetchRequest
request.predicate = NSPredicate(format: "self == %@", person)
request.returnsObjectsAsFaults = false
_ = try! moc.fetch(request)
使用
IN
运算符并传入对象数组或集合,可匹配同一实体的多个对象:
let predicate = NSPredicate(format: "self IN %@", somePeople)
在这两种情况下,既可以传入对象 ID 也可以传入托管对象,Core Data 会将这些谓词转换为匹配传入对象主键的 SQL 语句。还可以在遍历关系时使用,例如匹配某人访问过的城市:
let predicate = NSPredicate(format: "%K CONTAINS %@",
#keyPath(City.visitors), person)
若需要与其他谓词组合,这种方式会更有用:
let predicate = NSPredicate(format: "%K CONTAINS %@ AND %K.@count >= 3",
#keyPath(City.visitors), person, #keyPath(City.visitors))
3. 字符串匹配
字符串匹配更为复杂,Core Data 对大多数字符串匹配和比较有强大支持,但往往不符合用户预期,因为文本本身很复杂。一般有两种情况:用户可见文本和仅由计算机解释的文本。对于不可见文本,关键是在比较运算符后使用
[n]
字符串选项,告知 Core Data 字符串是 ASCII 码。以下是不同字符串匹配运算符的示例:
| 运算符 | 示例代码 | 说明 |
| ---- | ---- | ---- |
|
==[n]
|
let predicate = NSPredicate(format: "%K ==[n] %@", #keyPath(Country.alpha3Code), "ZAF")
| 匹配特定国家代码 |
|
BEGINSWITH[n]
|
let predicate = NSPredicate(format: "%K BEGINSWITH[n] %@", #keyPath(Country.alpha3Code), "CA")
| 搜索以特定前缀开头的字符串 |
|
ENDSWITH[n]
|
let predicate = NSPredicate(format: "%K ENDSWITH[n] %@", #keyPath(Country.alpha3Code), "K")
| 搜索以特定后缀结尾的字符串 |
|
CONTAINS[n]
|
let predicate = NSPredicate(format: "%K CONTAINS[n] %@", #keyPath(Country.alpha3Code), "IN")
| 搜索包含特定部分的字符串 |
|
LIKE[n]
|
let predicate = NSPredicate(format: "%K LIKE[n] %@", #keyPath(Country.alpha3Code), "?A?")
| 支持
?
和
*
通配符 |
|
MATCHES[n]
|
let predicate = NSPredicate(format: "%K MATCHES[n] %@", #keyPath(Country.alpha3Code), "[AB][FLH](.)")
| 执行完整的正则表达式风格比较 |
|
IN[n]
|
let predicate = NSPredicate(format: "%K IN[n] %@", #keyPath(Country.alpha3Code), ["FRA", "FIN", "ISL"])
| 匹配字符串属性是否包含在给定字符串数组中 |
4. 字符串与索引
[n]
选项告知 Core Data 文本字符串可以逐字节比较,若属性有索引,SQLite 可使用该索引。
==[n]
、
BEGINSWITH[n]
和
IN[n]
运算符能使用索引,在大数据集上性能良好;而
ENDSWITH[n]
、
CONTAINS[n]
、
LIKE[n]
和
MATCHES[n]
运算符无法从索引中受益,在大数据集上可能成本较高,应尽量使用前者。
5. 可转换值
使用 Core Data 的可转换属性时,可直接对其键使用谓词。例如,
City
实体有一个可选的
NSUUID
类型的
remoteIdentiĩer
属性,创建匹配特定
remoteIdentiĩer
的谓词时,可传入
NSUUID
对象,Core Data 会将其转换为二进制数据并创建相应的 SQLite 查询:
let identiĩer: NSUUID = someRemoteIdentiĩer
let predicate = NSPredicate(format: "%K == %@",
#keyPath(City.remoteIdentiĩer), identiĩer)
还可以使用不等式运算符比较可转换值,比较结果取决于值和转换为二进制数据的方式。
6. 性能和排序表达式
执行获取请求成本高,主要是因为必须查询 SQLite 和文件系统存储。处理大数据集时,谓词的构造方式和索引的存在对性能有重要影响。构建复杂谓词时,应将简单和/或高性能部分放在前面,复杂部分放在后面。例如,若谓词同时检查年龄和车辆拥有数量属性,且只有年龄属性有索引,应将年龄部分放在前面:
let predicate = NSPredicate(format: "%K > %ld && %K == %ld",
#keyPath(Person.age), 32,
#keyPath(Person.carsOwnedCount), 2)
若要查找
hidden
为
true
且年龄大于 30 的
Person
对象,由于应用逻辑中
hidden
为
true
的对象很少,而年龄大于 30 的对象很多,应先检查
hidden
条件:
let predicate = NSPredicate(format: "%K == YES && %K > %ld",
#keyPath(Person.hidden),
#keyPath(Person.age), 30)
创建适当的索引通常能显著提升性能,但创建索引也有成本,需要测量更改前后的性能来评估效果。
7. Unicode 的复杂性
存储文本到 Core Data 很简单,但搜索和排序文本字符串可能很复杂。由于 Unicode 和语言的复杂性,两个文本字符串相等的概念与它们底层字节相等的概念不同,确定字符串的排序顺序也很复杂,这很大程度上取决于当前的语言环境。
例如,法国第 14 大城市 Saint-Étienne,字母
É
在 Unicode 中有两种表示方式,用户输入
Saint-Étienne
、
saint-étienne
或
Saint Etienne
都希望能匹配到该城市,但这些字符串的字节表示不同,简单比较不会认为它们相等。在某些语言环境中,用户输入
Århus
希望能找到丹麦城市
Aarhus
,因为字母
Å
也有两种 Unicode 表示方式。在非拉丁脚本中,还需要考虑拉丁文本是否匹配等问题。
排序也存在类似问题,即使是拉丁脚本,单词的排序也比单个字母复杂,且排序顺序取决于用户的语言环境。例如,在德国,字母
ö
有两种排序方式;在瑞典,
ö
排在其他字母之后;在丹麦,字母
Ø
排在
Z
之后。
8. 搜索方法
如果要搜索的条目数量很少,可以利用
NSPredicate
忽略字母大小写和变音符号的比较功能:
let predicate = NSPredicate(format: "%K BEGINSWITH[cd] %@",
#keyPath(City.name), searchTerm)
BEGINSWITH
运算符匹配以搜索词开头的值,
[cd]
修饰符指定搜索不区分大小写并忽略变音符号。但当数据库中有很多行时,这种匹配方式成本很高,因为
BEGINSWITH[cd]
是复杂的 Unicode 操作,SQLite 无法原生执行,需要读取每个数据库条目并传递给 Core Data 进行比较,且无法使用索引加速。
9. 字符串归一化
为了在大数据集中进行高效搜索,需要对要搜索的字符串属性进行归一化。以
City
实体为例,修改数据模型,使其有
name
和
name_normalized
属性,在
City
类中,仅暴露
name
属性,并添加逻辑,使
name_normalized
属性在
name
更改时更新:
ĩnal public class City : NSManagedObject, Managed {
public static let normalizedNameKey = "name_normalized"
@NSManaged ĩleprivate var primitiveName: String
public var name: String {
set {
willChangeValue(forKey: #keyPath(City.name))
primitiveName = newValue
updateNormalizedName(newValue)
didChangeValue(forKey: #keyPath(City.name))
}
get {
willAccessValue(forKey: #keyPath(City.name))
let value = primitiveName
didAccessValue(forKey: #keyPath(City.name))
return value
}
}
ĩleprivate func updateNormalizedName(_ name: String) {
setValue(name.normalizedForSearch, forKey: City.normalizedNameKey)
}
}
extension String {
public var normalizedForSearch: String {
let transformed = applyingTransform(
StringTransform("Any-Latin; Latin-ASCII; Lower"),
reverse: false)
return transformed as String? ?? ""
}
}
上述代码中,
normalizedForSearch
扩展方法将字符串转换为小写,去除所有变音符号,并将非拉丁脚本转换为拉丁脚本。如果需要去除归一化字符串中的非字母字符,可以使用以下转换:
Any-Latin; Latin-ASCII; Lower; [:^Letter:] Remove
10. 高效搜索
由于数据库中有了归一化的字符串,可使用高效的谓词形式:
let predicate = NSPredicate(format: "%K BEGINSWITH[n] %@",
City.normalizedNameKey, searchTerm.normalizedForSearch)
通过在
BEGINSWITH
运算符后添加
[n]
修饰符,告知 Core Data 谓词的参数已归一化,可以在 SQLite 中直接逐字节比较,无需获取所有行并执行昂贵的 Unicode 感知比较。例如,用户搜索
Béziers
时,最终的谓词如下:
name_normalized BEGINSWITH[n] "beziers"
综上所述,在使用 Core Data 进行数据查询和文本搜索时,合理运用谓词、考虑索引和字符串归一化等方法,可以显著提高性能和搜索准确性。
Core Data 谓词使用与文本搜索优化全解析
11. 总结
谓词为描述我们感兴趣的对象子集提供了一种简洁且灵活的方式。以下是对前面内容的总结:
-
子查询
:在通过关系匹配多个属性时使用,如匹配至少有一个特定居民的城市。
-
匹配对象和对象 ID
:可直接匹配对象或其标识符,还能在遍历关系时使用,结合其他谓词可实现更强大的搜索。
-
字符串匹配
:对于不可见文本,使用
[n]
选项,不同运算符有不同匹配规则,部分运算符能利用索引提高性能。
-
可转换值
:可直接对可转换属性的键使用谓词,Core Data 会处理转换。
-
性能和排序表达式
:构建谓词时合理安排顺序,创建适当索引可提升性能。
-
文本搜索
:处理 Unicode 复杂问题,可通过字符串归一化实现高效搜索。
下面通过一个表格来对比不同字符串匹配运算符的特点和是否能使用索引:
| 运算符 | 特点 | 是否能使用索引 |
| ---- | ---- | ---- |
|
==[n]
| 精确匹配特定字符串 | 是 |
|
BEGINSWITH[n]
| 搜索以特定前缀开头的字符串 | 是 |
|
ENDSWITH[n]
| 搜索以特定后缀结尾的字符串 | 否 |
|
CONTAINS[n]
| 搜索包含特定部分的字符串 | 否 |
|
LIKE[n]
| 支持
?
和
*
通配符 | 否 |
|
MATCHES[n]
| 执行完整的正则表达式风格比较 | 否 |
|
IN[n]
| 匹配字符串属性是否包含在给定字符串数组中 | 是 |
12. 操作步骤总结
为了更清晰地展示在 Core Data 中进行谓词使用和文本搜索优化的操作步骤,我们整理了以下流程:
12.1 子查询操作步骤
- 确定需要通过关系匹配的多个属性。
- 编写子查询代码,例如:
SUBQUERY(residents, $x, $x.age < 25 AND $x.carsOwnedCount == 2).@count != 0
12.2 匹配对象和对象 ID 操作步骤
- 直接匹配对象:
let request = Person.sortedFetchRequest
request.predicate = NSPredicate(format: "self == %@", person)
request.returnsObjectsAsFaults = false
_ = try! moc.fetch(request)
- 匹配多个对象:
let predicate = NSPredicate(format: "self IN %@", somePeople)
- 遍历关系匹配:
let predicate = NSPredicate(format: "%K CONTAINS %@",
#keyPath(City.visitors), person)
12.3 字符串匹配操作步骤
-
对于不可见文本,使用
[n]选项,根据不同需求选择运算符,如:
let predicate = NSPredicate(format: "%K ==[n] %@", #keyPath(Country.alpha3Code), "ZAF")
12.4 字符串归一化操作步骤
-
修改数据模型,添加归一化属性,如
City实体添加name_normalized。 - 在类中实现属性逻辑,确保归一化属性随原始属性更新:
ĩnal public class City : NSManagedObject, Managed {
public static let normalizedNameKey = "name_normalized"
@NSManaged ĩleprivate var primitiveName: String
public var name: String {
set {
willChangeValue(forKey: #keyPath(City.name))
primitiveName = newValue
updateNormalizedName(newValue)
didChangeValue(forKey: #keyPath(City.name))
}
get {
willAccessValue(forKey: #keyPath(City.name))
let value = primitiveName
didAccessValue(forKey: #keyPath(City.name))
return value
}
}
ĩleprivate func updateNormalizedName(_ name: String) {
setValue(name.normalizedForSearch, forKey: City.normalizedNameKey)
}
}
extension String {
public var normalizedForSearch: String {
let transformed = applyingTransform(
StringTransform("Any-Latin; Latin-ASCII; Lower"),
reverse: false)
return transformed as String? ?? ""
}
}
12.5 高效搜索操作步骤
- 构建高效谓词:
let predicate = NSPredicate(format: "%K BEGINSWITH[n] %@",
City.normalizedNameKey, searchTerm.normalizedForSearch)
13. 流程图
下面是一个 mermaid 格式的流程图,展示了在 Core Data 中进行文本搜索的整体流程:
graph TD;
A[开始] --> B{数据量少?};
B -- 是 --> C[使用 NSPredicate 忽略大小写和变音符号];
B -- 否 --> D[字符串归一化];
D --> E[构建带 [n] 选项的谓词];
C --> F[执行搜索];
E --> F;
F --> G[结束];
通过以上的总结和操作步骤,我们可以更系统地掌握 Core Data 谓词使用和文本搜索优化的方法,在实际开发中根据具体需求灵活运用,提高应用的性能和用户体验。
超级会员免费看
19

被折叠的 条评论
为什么被折叠?



