2015年5月19日
教程:初始化可写的 SQLite 数据库
使用 SQLite 可能具有挑战性,但 Corona 开发人员遇到的一个特定问题是将预先存在的数据库放入可以读取和插入/更新数据的位置。
问题源于这样一个事实:当您为 iOS 创建应用程序包或为 Android 创建 APK 文件时,它是一个自包含的“文件夹”,其中包含安装到设备的文件。这恰好是只读的,因此您无法更新其中包含的数据库。此应用程序包在代码中被有效地引用为 system.ResourceDirectory。
除了 system.ResourceDirectory 之外,您的应用程序还有三个您可以写入文件的文件夹,包括更新这些文件夹中的数据库。它们是 system.DocumentsDirectory、system.CachesDirectory 和 system.TemporaryDirectory。对于您计划更新的数据库,这些文件夹的逻辑位置是 system.DocumentsDirectory。为什么?因为与“缓存”和“临时”文件夹不同,此文件夹是一个持久文件夹,只要应用程序仍然安装在设备上,它就会存在。
初始化数据库
当用户首次安装您的应用程序时,system.DocumentsDirectory 文件夹基本上是空的。如果您的应用程序需要访问初始信息的数据库,您首先需要将该数据库从只读空间 (system.ResourceDirectory) 移动到“documents”文件夹,在该文件夹中可以插入/更新记录。如果它是一个非常简单的数据库,您实际上可以使用 Corona 的 sqlite3.* 调用在 system.DocumentsDirectory 中创建一个全新的数据库,然后插入您的记录。但是,对于具有多个表和大量记录的更复杂的数据库,这个概念变得更加困难和不实用。因此,更简单的方法是一次性复制整个预先存在的数据库。
考虑以下名为 copyDBto.lua
的模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
local M = {} function M.copyDatabaseTo( filename, destination ) assert( type(filename) == "string", "第一个参数应为字符串,但得到 " .. type(filename) .. " instead." ) assert( type(destination) == "table", "第二个参数应为表,但得到 " .. type(destination) .. " instead." ) local sourceDBpath = system.pathForFile( filename, system.ResourceDirectory ) -- io.open 打开路径中的文件;如果未找到文件,则返回 nil local readHandle, errorString = io.open( sourceDBpath, "rb" ) assert( readHandle, "无法从 system.ResourceDirectory 读取位于 " .. filename .. " 的数据库" ) assert( type(destination.filename) == "string", "filename 应为字符串,它的类型为 " .. type(destination.filename) ) print( type(destination.baseDir) ) assert( type(destination.baseDir) == "userdata", "baseName 应为有效的系统目录" ) local destinationDBpath = system.pathForFile( destination.filename, destination.baseDir ) local writeHandle, writeErrorString = io.open( destinationDBpath, "wb" ) assert( writeHandle, "无法打开 " .. destination.filename .. " 进行写入。" ) local contents = readHandle:read( "*a" ) writeHandle:write( contents ) io.close( writeHandle ) io.close( readHandle ) return true end return M |
在模块的主函数 (copyDatabaseTo()
) 中,您传入数据库的名称,该名称被假定为您应用程序捆绑包中的现有数据库文件 (system.ResourceDirectory)。此外,您还传入一个包含 filename
和 baseDir
的表格,您希望将数据库移动到该位置,在本例中为 system.DocumentsDirectory,如上所述。
1 2 3 |
local dbfunc = require( "copyDBto" ) local result = dbfunc.copyDatabaseTo( "data.db", { filename="data.db", baseDir=system.DocumentsDirectory } ) |
假设没有错误,这将把整个数据库从您的资源捆绑包复制到您指定为 filename
的可写文件夹。当然,此操作只应在应用程序的**首次**运行时发生,以便数据库不会每次都被替换/覆盖。
考虑在 main.lua
中对以上 2 行进行更彻底的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
local sqlite3 = require( "sqlite3" ) local dbfunc = require( "copyDBto" ) local filename = "data.db" local baseDir = system.DocumentsDirectory -- 打开 "data.db"。如果文件不存在,它将被创建 local path = system.pathForFile( filename, baseDir ) local doesExist = io.open( path, "r" ) if not doesExist then local result = dbfunc.copyDatabaseTo( "data.db", { filename="data.db", baseDir=system.DocumentsDirectory } ) assert( result, "数据库复制失败。请检查日志。") else io.close( doesExist ) end local db = sqlite3.open( path ) -- 处理 "applicationExit" 事件以关闭数据库 local function onSystemEvent( event ) if ( event.type == "applicationExit" ) then db:close() end end -- 打印表格内容 for row in db:nrows( "SELECT * FROM highscores" ) do local text = row.name .. " : " .. row.score local t = display.newText( text, 120, 30*row.id, nil, 16 ) t:setFillColor( 1, 0, 1 ) end -- 设置事件监听器以捕获 "applicationExit" Runtime:addEventListener( "system", onSystemEvent ) |
在第 2 行,我们 require()
copyDBto.lua
模块。在第 4 和 5 行,我们设置数据库文件的名称和目标位置,并在第 8 行,我们为其创建内部路径引用。
第 10-16 行检查数据库是否已存在于您期望的位置。如果不存在(首次运行),我们会在第 12 行调用该函数来复制数据库。之后,我们像往常一样打开数据库并处理它——在此示例中,这包括一个在用户退出应用程序时关闭数据库的函数,以及一个测试 for
循环来显示数据库中的一些数据。
结论
如您所见,创建一个预先存在且预先填充的数据库非常容易,将其捆绑在您的应用程序资源中,并快速将其复制到可写文件夹。请注意,此功能同样适用于文本文件和 JSON 文件,它们也可用于跟踪/存储应用程序中的持久数据,如高分、设置等。
Ed Maurina
发布于 09:57,5 月 21 日好文章,Rob。这应该有助于为那些最近一直在与数据库问题和 R/W 访问作斗争的人们澄清问题,并为未来遇到同样难题的开发者解答疑问。