带FMDB的SQLite

FMDB是Apple为SQLite提供的C样式API的Objective-C包装。 请务必注意,SQLite不是Apple专有技术。 SQLite是Apple,Android,Skype和许多其他客户端和应用程序使用的开源C语言库,可提供强大的数据库引擎。 SQLite预先安装在所有iOS和Android设备上。 它也是iOS上用于Core Data的默认后备存储(尽管您可以选择其他选项)。 在这篇博客文章中,我们将仔细研究SQLite和iOS设备存储。

我们需要做的第一件事是使用Carthage(我的首选方法),CocoaPods或下载存储库来安装FMDB。 我不会在这篇文章中介绍这些步骤,因为它们在其他地方都可以广泛使用。 我将注意到,如果您使用的是迦太基,请将该框架拖到“目标”的“常规”选项卡的“嵌入式二进制文件”部分中。 这会将其添加到“嵌入式二进制文件”和“链接的框架和库”部分。 然后在您的桥接头中导入框架

#import

在某些时候,您可能需要检查实际的SQLite文件。 有几种工具可让您执行此操作,我喜欢使用的一种工具称为“用于SQLite的DB浏览器”。 它是SQLite数据库的可视查看器和编辑器。 您可以添加新条目,执行查询以及浏览数据库表。 我发现此应用程序对于使用Core Data非常有用。 请注意,如果在数据库浏览器中打开了SQLite文件,它将被锁定,因此FMDB事务将失败。 可以使用Homebrew如下安装DB Browser:

brew cask install db-browser-for-sqlite

您可以使用终端的find命令find . -name test.sqlite在模拟器中find SQLite文件的位置find . -name test.sqlite find . -name test.sqlite 。 矿山位于以下位置:

/Users/user/Library/Developer/CoreSimulator/Devices/89D0C0F5–3842–4E53-A060–8E3D4BDF134E/data/Containers/Data/Application/E37428BB-544C-4A17–9216–1BB3901ED33B/Library/Application Support/test.sqlite

要开始使用FDMB,我们首先需要创建一个SQLite文件。 有关如何执行此操作的基本说明,请参见FMDB存储库上的自述文件。 在此示例中,我们仅打算有一个数据库,但是如果需要,可以有多个数据库。 我只想使用单个数据库,并且由于在多线程环境中使用数据库可能非常危险,因此我选择创建一个单例来访问数据库并进行更新(稍后会详细介绍这种方法的原因)。 我创建了一个类,在访问数据库时充当包装器。 该类将提供基本访问所需的集中访问权限,以调用各种SQL命令。 FMDBDatabase类具有两个主要的静态属性sharedDatabasesharedQueue 。 这些使调用者可以在需要时访问数据库,并可以使用共享队列来确保执行事务时的线程安全。

该仓库包含有关使用FMDB时线程安全性的部分。 自述文件明确指出:“ 一次从多个线程 使用 FMDatabase 的单个实例 是一个坏主意 您还将在SQLite主页上找到类似的警告。 因此,在更新数据库时,强烈建议您在单个线程上执行此操作,否则会发生不良事件 ”。 幸运的是,FMDB提供了一个串行队列,可用于查询和更新数据库。

我按照自述文件中的说明进行操作,这些说明说明了如何在FMDatabaseQueue类型上使用inTransaction函数来解决多线程问题。

  queue.inTransaction {db,回滚 
做{
试试db.executeUpdate(“ INSERT INTO table_name VALUES(?)”,
} {
//处理回滚
}
}

但是,我一直使错误消息失败:每当我尝试使用inTransaction函数时, 数据库都被锁定 。 我一直无法确定导致此错误消息的确切原因,但发现使用inDatabaseinDeferredTransaction函数可以缓解此错误。 延迟事务是SQLite中的标准方法,因此我使用inDeferredTransaction实现了查询和更新。

FMDB将字符串用于所有SQL命令(CREATE,INSERT,DELETE等)。 如果不是最好的用户体验,则使用字符串可能会造成麻烦,并且会浪费时间进行调试,这是因为拼写错误。 我想通过API从用户那里抽象出严格性。 FMDBDatabase类API包含一些静态函数,调用者可以使用这些静态函数:创建表,插入记录,删除记录或查询数据库。 由于我在SQLite方面的直接经验有限,因此这是一小部分基本功能。 在功能完善的应用程序中,我们希望支持将多个记录插入/删除到批处理数据库中以提高效率。 FMDB也可以使用前面提到的功能来支持此功能。

创建表需要输入一种特殊格式的字符串,其中包括CREATE TABLE命令以及列名及其各自的值类型。 假设我们要创建一个名为Tutorials的表,该表有四列,则该语句的格式如下:

  “如果不存在,则创建表教程(ID整数主键自动递增,标题文本,作者文本,PublicationDate日期);” 

在此示例中,我们创建四列:ID,标题,作者和出版物日期。 每列分别具有关联的整数,文本,文本和日期类型。 我们使用关键字Primary key将ID字段设置为表的主键,并使用AutoIncrement关键字指示数据库应通过增加最后一个条目的值来生成此值。 每个表都应有一个主键,并且可以根据需要添加辅助键和索引。

FMDB提供了用于表示列值的类型的扩展列表,部分列表包括: Int, Double, Bool, String, Date, Data, and Null 。 实际上,SQLite使用动态类型系统仅支持有限数量的存储类型。 这些类型是Integer,Real,Text,Blob或Null。 幸运的是,当您使用占位符( ? )作为值时,FMDB会处理将其内部类型映射到SQLite支持的内部类型(强烈建议)。 稍后将在表中插入记录时,我们将看到此示例。

具有为我们创建表,接受表名和某些列属性的功能会很好。 考虑到FMDB使用严格的SQL语句,这有点挑战。 使用列和类型的字典是我的最初想法,但是,这种方法会丢失列的顺序。 另一种选择是使用Tuple包含两个值。 最后,我选择的简单解决方案是使用结构数组。 TableColumn只是具有两个值(列名和类型)的结构。 create函数将使用表名和TableColumn的数组,我们可以使用它们创建具有关联类型的列。

这给我们提供了一个功能,可以动态创建用户提供的给定类型的列。 我们可能仍想提供一个create函数的版本,该版本为那些以这种方式创建列可能更加复杂或耗时的实例使用原始SQL语句字符串。

现在我们可以创建表了,我们将需要能够将数据插入到表中。 将数据插入表中需要另一个SQL语句字符串。 这包括INSERT命令,目标表名称和占位符,以表示要插入表中的值。 假设我们想将数据插入Tutorials表中,那么使用占位符的SQL语句将采用以下格式:

“INSERT OR REPLACE INTO Tutorials VALUES (NULL, ?, ?, ?, ?);”

将使用绑定到占位符( ? ,…,?)值的值数组来调用insert函数,从而允许FMDB处理值到列的映射。 由于第一列是ID列并且是自动递增的,因此我们必须将其值设置为null否则SQLite会认为它需要相应的值。 我们需要将要插入表中的值映射到INSERT语句中的绑定( ? )。 我们想要的是一种动态方式,可以为要插入数据库的任意数量的参数生成绑定语句(NULL,?,?,?,…,?)。 通过创建一个基本字符串并根据插入过程中传递的值附加正确数量的占位符,这很容易。

该insert语句假设所有必需的值都存在于values数组中。 我们不检查表的列数是否匹配values数组的数。 如果values数组不完整,则会出现错误,指示表中的列数和接收到的值数不匹配。 如果数组中的实际值顺序错误或类型错误,您将不会收到错误消息。 这是因为SQLite使用动态类型。 SQLite FAQ将动态类型描述为“ 值的数据类型与值本身而不是其容器相关联 ”。 通常,在SQL表中,设置为保留Integer类型的列只能容纳整数,但是,在SQLite中,它可以存储任何存储类的值。 docs中记录的一个例外是Integer Primary Key列,它可能仅包含64位带符号整数。 在真实的应用程序中,我们希望确保在给定表中的每次插入都保持顺序。 可以使用一个结构来表示表的列和值,这将有助于确保所有值都存在,正确排序和正确键入。

太好了,现在我们可以创建一个表,插入一些值,接下来我们将要查询该表以获取数据。 在FMDB中,查询是仅有的两种语句类型之一(更新是另一种语句类型,用于创建,插入,设置,删除等)。 基本上,如果它不是查询,则是更新。 查询语句的一般形式包括QUERY命令,表名和WHERE子句,用于根据指定的条件选择记录。

  var sqlStatement =“ SELECT * FROM table_name在哪里column_name =?,column_name =?” 
  database.executeQuery(sqlStatement,值:值) 

查询语句使用WHERE子句限制检索到的记录。 每个条件都需要一个列名和一些关联的过滤器值。 您可以根据需要提供无条件(获取表中的所有记录)或多个条件。 匹配的实际值作为单独的数组传递给executeQuery函数。 我们可以使用Tuple来表示每个where条件,但是我选择了一个简单的结构来表示where条件。 WhereCondition结构包含两个属性,column为列名,value为要匹配的值。

查询返回一个FMResultSet对象,该对象具有next函数,该函数使您可以迭代查询返回的记录。 您可以通过两种主要方法来访问各个列的值,一种是通过列的名称。

 让title = fmresult.string(forColumn:“ Title”) 
让作者= fmresult.string(forColumn:“作者”)
让日期= fmresult.double(forColumn:“ PublicationDate”)

另一种选择是按列位置访问项目。

 让title = fmresult.string(forColumnIndex:1) 
让作者= fmresult.string(forColumnIndex:2)
让日期= fmresult.double(forColumnIndex:3)

我们还可以选择按列索引检索列名,然后检索列数据。 所有对象方法( StringDateData等)都返回可选值,而原始数据类型( IntegerDouble等)则返回非可选值。 最终,我们将想知道每列中存储了哪种类型,以便使用正确的方法以适当的格式提取内容。 可以使用while循环和上述用于访问列值的方法来访问带有结果集的Tutorial表上查询记录的记录。

 如果让fmresult = fmresult { 
而fmresult.next(){
让行= fmresult.int(forColumn:“ ID”)
让标题= fmresult.string(forColumn:“标题”)
让作者= fmresult.string(forColumn:“作者”)
让日期= fmresult.double(forColumn:“ PublicationDate”)
}
}

如果可以从表中以某种类型的形式获取一行数据,然后将其用于传递给内部数据类型,那将是很好的。 我没有在文档中看到任何可能的方式。 尽管FMResultSet对象中有一些类型,例如resultDictionarycolumnNameToIndexMap这些类型仅将列名映射到一个位置,这两个都不包含有关存储在这些列中的值的信息。

最后,我们将要删除表中的某些行。 这将采用与插入和查询功能类似的形式。 我们需要使用DELETE命令,并将其与WHERE子句结合使用,以便我们可以删除所需的行。 WHERE子句的基本条件是列名和要匹配的值的占位符。

  var sqlStatement =“从\(表)中删除column_name =?,columnName =?” 
  database.executeUpdate(sqlStatement:sqlStatement,值:值) 

我们想要的是能够根据一组条件从表中动态删除行。 由于删除条件是列名和值,因此我们可以重复使用先前为查询命令定义的WhereCondition结构。 因此,我们的删除功能采用以下形式:

我们传入一个where条件数组,并从列名中形成命令字符串,并将值传递给update语句。 由于where条件数组不是可选的,因此必须至少提供一个条件。 从表中删除时,我们确实要小心,我们不希望意外删除所有行,因为我们没有指定任何条件来约束删除。

实际上,删除表本身并不重要,因为我们需要的只是DROP命令和表名。

  let sqlStatement =“如果存在\(table),则删除表” 
database.excuteUpdate(sqlStatement:sqlStatement,值:无)

FDMB正在从诸如Core Data之类的框架中降低一个层次,该框架实际上是SQLite数据库之上的对象图抽象。 您将直接使用SQLite。 每当您降低抽象级别时,您往往会负责处理更多的实现细节,更高的抽象可以为您解决。 这意味着对于FMDB,您负责任何版本控制,迁移,回滚或对表的更改。 例如,如果要向表中添加新列,则需要创建新表,复制数据,填充新字段,然后删除旧表。 所有这些都必须由您实现。

可以在github上找到一个示例项目。 该项目使您可以创建表,插入记录和查询数据库。 下次您认为Core Data可能无法缓存数据,或者您需要简单的脱机模式时,FMDB可能是一种解决方案。

资料来源
调频数据库
SQLite的
使用FMDB和SQLite
持久性商店类型
数据库浏览器
SQLite视图
SQLiteStudio
Ilija Boshkov在Unsplash上​​的照片