原文:http://wiki.appcelerator.org/display/guides/Working+with+Local+Data
使用本地数据
内容速览
* 要点
* 使用本地数据
o 选用哪种类型的数据存储?
* App属性
o 读写属性
o 以JSON形式,把JS对象存储在属性中
* 文件系统存储
o 对象
o 属性
o 方法
o 写文件
o 读文件
o 移除文件
o 完整代码
* SQLite 数据库
o SQL基础和更多阅读
o 创建数据库
o 准备好数据库
+ 把数据库包含在App包中
+ 从远程下载数据库
o 存数据
o 读数据
o 更新数据
o 关闭数据库和数据集
要点
* 本地存储的不同模式,何时使用
* 怎样用程序属性
* 怎样从本地文件系统中存取数据
* 怎样和本地SQLite 数据库交互
使用本地数据
一般程序都有本地存储需求。Titanium 提供统一方便的接口。使用以下对象:* Titanium.App.Properties 用来存储App相关设置
* Titanium.Filesystem 文件和目录操作
* Titanium.Database 操作本地SQLite3数据库
选用哪种类型的数据存储?
取决如下因素:1. App属性 - 当满足下面一条或全部满足时:
* 数据只是简单的键值对
* 数据和App本身相关而不是和用户相关
* 该数据独立,不再需要其他数据才变得有意义或有用
* 任何时候,只需要数据的一个版本
2. 文件系统- 当满足下面一条或全部满足时:
* 数据以文件形式提供
* 数据是图片文件
3. 数据库 -当满足下面一条或全部满足时:
* 有很多条类似的数据item
* 数据item和其他item相关
* 取数据时要求灵活提取
* 数据随时间而积累,如历史事务清单, 登录或档案数据
注意:尽管图片数据可以以2进制(blob )形式保存在数据库中。但建议不要这样做。而应用Titanium.Database存储图片文件路径和图片名,用Titanium.Filesystem管理物理文件。
想要本地文件系统仅仅有条,易于管理和备份。最好把类似的文件放在一起,比如图片,放到同一位置。这样一来,就用不着在数据库存路径。程序中所有图片都可以这样做,只用App属性设置就好了。
App属性
iOS和Android 都有特定文件用来存储App属性。iOS 是NSUserDefaults,在应用程序library 目录下的.plist 文件中。Android则是标准的XML文件,位于/data/data/com.domainname.appname/shared_prefs/titanium.xml.App一加载,App属性就存到内存。等App关掉,就自动从内存释放。理论上访问很快,但会受到App占有内存的影响。
读写属性
Titanium.App.Properties有6个get/set方法,分别操作6种不同数据:* getBool() / setBool(): 布尔值 (true, false)
* getDouble() / setDouble(): 双精度浮点数值
* getInt() / setInt(): 整型
* getList() / setList():数组
* getString() / setString(): 字符串
get方法接受 属性名 参数,并返回默认值,如果属性从未被设置,就返回默认值。每一个 set 方法都要传进 属性名-属性值 对。范例如下:
var window = Titanium.UI.createWindow({
backgroundColor:'#999'
});
var myArray = [ { name:'Name 1', address:'1 Main St'}, {name:'Name 2', address:'2 Main St'}, {name:'Name 3', address:'3 Main St'}, {name:'Name 4', address:'4 Main St' } ];
Ti.App.Properties.setString('myString','This is a string');
Ti.App.Properties.setInt('myInt',10);
Ti.App.Properties.setBool('myBool',true);
Ti.App.Properties.setDouble('myDouble',10.6);
Ti.App.Properties.setList('myList',myArray);
// **********************************************
// Notice the use of the second argument of the get* methods below
// that would be returned if no property exists with that name
// **********************************************
Ti.API.info("String: "+Ti.App.Properties.getString('myString','This is a string default'));
Ti.API.info("Integer: "+Ti.App.Properties.getInt('myInt',20));
Ti.API.info("Boolean: "+Ti.App.Properties.getBool('myBool',false));
Ti.API.info("Double: "+Ti.App.Properties.getDouble('myDouble',20.6));
Ti.API.info("List: "+Ti.App.Properties.getList('myList'));
window.open();
执行输出:
String: This is a string
Integer: 10
Boolean: true
Double: 10.600000381469727
List:
{ 'address' : '1 Main St' 'name' : 'Name 1', },
{ 'address' : '2 Main St' 'name' : 'Name 2', },
{ 'address' : '3 Main St' 'name' : 'Name 3', },
{ 'address' : '4 Main St' 'name' : 'Name 4', }
以JSON形式,把JS对象存储在属性中
如果是复杂的js对象,要用Titanium.JSON的JSON.stringify()把js对象转成JSON字符串。这样一来,就能用the Titanium.App.Properties.setString() 存到数据库中了。var window = Titanium.UI.createWindow({
backgroundColor:'#999'
});
var weatherData = { "reports" : [ { "city": "Mountain View", "condition": "Cloudy", "icon": "http://www.google.com/weather/cloudy.gif" }, { "city": "Washington, DC", "condition": "Mostly Cloudy", "icon": "http://www.google.com/weather/mostly_cloudy.gif" }, { "city": "Brasilia", "condition": "Thunderstorm", "icon": "http://www.google.com/weather/thunderstorm.gif" } ] };
Ti.App.Properties.setString('myJSON', JSON.stringify(weatherData));
var retrievedJSON=Ti.App.Properties.getString('myJSON', 'myJSON not found');
Ti.API.info("The myJSON property contains: " + retrievedJSON);
window.open();
测试输出:
The myJSON property contains: {"reports":[{"icon":"http:\/\/www.google.com\/ig\/images\/weather\/cloudy.gif","condition":"Cloudy","city":"Mountain View"},{"icon":"http:\/\/www.google.com\/ig\/images\/weather\/mostly_cloudy.gif","condition":"Mostly Cloudy","city":"Washington, DC"},{"icon":"http:\/\/www.google.com\/ig\/images\/weather\/thunderstorm.gif","condition":"Thunderstorm","city":"Brasilia"}]}
取值时再用JSON.parse()还原成JS对象:
var myObject = JSON.parse(Ti.App.Properties.getString('myJSON'));
文件系统存储
用Titanium 作CRUD(增删)操作很简单。在看Demo前先看下常用属性和方法:对象
Titanium.Filesystem是顶级的文件系统模块。用来读写设备上的文件和目录.Titanium.Filesystem.File,文件对象,支持文件的创建,读写,删除操作。
属性
数据存储位置:* applicationDataDirectory应用程序数据目录: 常量,只读。指定数据目录位置。应用程序位于此目录下.
* resourcesDirectory资源目录: 常量,只读。即应用程序的资源目录。
* tempDirectory缓存目录: 常量,只读。放缓存文件的地方。
方法
* getFile(): 返回Titanium.Filesystem.File对象,完整格式的文件路径* deleteFile(): 删除文件
* exists(): 如果文件货目录存在返回true。
* extension(): 返回文件扩展名
* move(): 移动文件到其他路径
* rename(): 给文件重命名
* nativePath(): 返回完整的原生路径
* read(): 以blob形式返回文件内容
* write(): 写文件
* writeable(): 是否可写
* spaceAvailable(): 路径是否有空间存储。
写文件
用简单例子解释.先创建JS对象:var dataToWrite = {"en_us":{"foo":"bar"}};
接着创建目录:
// NOTE: use applicationDataDirectory for writes!!
var newDir = Titanium.Filesystem.getFile(Titanium.Filesystem.applicationDataDirectory,'mydir');
newDir.createDirectory();
Ti.API.info('Path to newdir: ' + newDir.nativePath);
现在有了特定的applicationDataDirectory作为新目录。程序可以对其读写操作。通常都得这样做。
注意,程序在运行时,对于文件系统的resourcesDirectory资源目录,可读不可写!
在刚刚创建的文件夹中添加新的文件:
var newFile = Titanium.Filesystem.getFile(newDir.nativePath,'newfile.json');
这样就添加了空白文件,但最好加点数据。初始化 dataToWrite 对象。和前面App属性做的一样。用Titanium.JSON。用JS对象写:
//Stringify the JavaScript object we created earlier and write it out to the new file
newFile.write(JSON.stringify(dataToWrite));
读文件
这样更新dataToWrite.en_us.foo呢?这样做:1. 读文件
2. 反序列化存储在文件中的对象
3. 更新相关属性
4. 序列化对象
5. 把序列化后的对象写回文件
var newFile = Titanium.Filesystem.getFile(newDir.nativePath,'newfile.json');
var resources = JSON.parse(newFile.read().text);
resources.en_us.foo = 'baz'; //bar becomes baz
newFile.write(JSON.stringify(resources));
移除文件
没有人要垃圾。我们自己得注意收检。先移除文件,再移除目录。//We already have references to the file and directory objects.
//We just need to call their cooresponding delete methods.
newFile.deleteFile();
newDir.deleteDirectory();
完整代码
var dataToWrite = {"en_us":{"foo":"bar"}};
//NOTE: remember to use applicationDataDirectory for writes
var newDir = Titanium.Filesystem.getFile(Titanium.Filesystem.applicationDataDirectory,'mydir');
newDir.createDirectory();
Ti.API.info('Path to newdir: ' + newDir.nativePath);
var newFile = Titanium.Filesystem.getFile(newDir.nativePath,'newfile.json');
//Stringify the JavaScript object we created earlier and write it out to the new file
newFile.write(JSON.stringify(dataToWrite));
var newFile = Titanium.Filesystem.getFile(newDir.nativePath,'newfile.json');
Ti.API.info('JSON.parse(newFile.read().text).en_us.foo = ' + JSON.parse(newFile.read().text).en_us.foo);
var resources = JSON.parse(newFile.read().text);
resources.en_us.foo = 'baz'; //bar becomes baz
newFile.write(JSON.stringify(resources));
Ti.API.info('JSON.parse(newFile.read().text).en_us.foo = ' + JSON.parse(newFile.read().text).en_us.foo);
//We already have references to the file and directory objects.
//We just need to call their cooresponding delete methods.
newFile.deleteFile();
newDir.deleteDirectory();
SQLite 数据库
SQLite3 是SQLite的SQL基础型相关数据管理系统(RDMS)第3版。Apple,Google 和RIM 都在选择它,来进行本地数据存储。SQLite 是当今世界使用最为广泛的数据库。得益于其开源,小型架构(footprint ),不用像其他DBMSs那样安装服务,设置和维护也超级简单。
第一次使用SQLite要注意以下几点,这会影响到你的开发方式:
*其数据库文件就是一个简单的文本文件. 不存在安全域(granular security )或者用户权限(user privileges).因为通常没有编码, 文件系统中的任何人都可以访问文件读取内容。
* 只有5种基础类型:TEXT, NUMERIC, INTEGER, REAL, NONE. 更多细节参考SQLite帮助。
* 因其存储为2进制对象(BLOBs) 并用文本形式表现,进入BLOBs并不理想. 所以, 建议把文件系统的二进制地址存到数据库, 而不是二进制数据自身。
* SQLite支持同时多人读操作,但是,一次只能有一个用户执行写操作. 因为当进行写操作时,文件就做了文件系统锁定.这一点在做多线程App时要死死的记住。
* 默认不支持引用完整性(referential integrity).详见SQLite Foreign Key Support
* 不支持 RIGHT and FULL OUTER JOINs
* 支持部分 ALTER TABLE; 不能修改删除列
SQL基础和更多阅读
因为很多人都在用SQLite,所以网上资料非常多。我们整理这些链接对初学者很有用:* SQLite官网
* SQLite官网的入门教程
* SQLite视频教程
* 我们的 第3方工具指南, 介绍了一些图形化工具,使SQLite更易使用
此外,在freenode IRC server的#sqlite栏目有很多人讨论。
用Titanium.Database模块访问SQLite,返回Titanium.Database.DB对象,可以安装和打开数据库,执行命令。做查询命令时,则返回Titanium.Database.ResultSet。
创建数据库
新建Titanium.Database实例对象,调用open() 或 install()方法来打开数据库文件。open()打开数据库文件,位于设备下应用程序目录的数据库目录中。安卓就是applicationDataDirectory/../databases/,和applicationDataDirectory属于同一级目录。如果数据库文件不存在,就会自动创建一个新的,空的SQLite 数据库文件。var db = Ti.Database.open('weatherDB');
这将创建名为weatherDB的文件。最好加上.sqlite或者.db后缀名,当然这不是必须的。
另一方面,install() 会从Titanium的资源目录复制已有的数据库,或者一部分(descendants)。applicationDataDirectory/../databases/返回对打开的数据库的引用。如果同名文件已经存在,就会不做复制,而仅仅打开数据库。
var db = Ti.Database.install('/mydata/weatherDB', 'weatherDB');
这里,weatherDB在Resources/mydata/ directory目录下初始,再复制到App的数据库目录中。
打开数据库后,用execute()方法创建表:
db.execute('CREATE TABLE IF NOT EXISTS city (id INTEGER PRIMARY KEY, name VARCHAR(16) NOT NULL, continent VARCHAR(16) NOT NULL, temp_f VARCHAR(4), temp_c VARCHAR(4), condition_id INTEGER NOT NULL)');
db.execute('CREATE TABLE IF NOT EXISTS condition (id INTEGER PRIMARY KEY, summary VARCHAR(16) NOT NULL, icon TEXT NOT NULL)');
IF NOT EXISTS部分是标准的SQLite 语法,来确保不会重写覆盖表。以上这些,就是SQLite操作的常用套路,用来防止因为丢失数据库或数据表而无法启动App的情况发生。
准备好数据库
把数据库包含在App包中
如果程序中已经有数据库,要用到里面的数据。有2种办法。1 是嵌入到App的发布包中,放到Titanium的资源文件夹。因为设备上的资源目录可读不可写,一旦安装就无删除。install() 会把文件复制到applicationDataDirectory/../databases/ 文件夹中,结果就是有2份文件副本。
2 如果数据库对你App的使用真的很重要。就要考虑在启动App时加载完整的数据库,这样就不用担心在初始化安装后被替换掉。
从远程下载数据库
作为上面做法的候补项,你可以加载“骨感”数据库文件。只有少量数据以供app运行。再通过用户授权,从远程下载数据库,像这样:buttonInstallRemote.addEventListener('click', function(){
var fileWeatherDB = Ti.Filesystem.getFile(Ti.Filesystem.applicationDataDirectory,'../databases/'+filename);
var c = Ti.Network.createHTTPClient();
c.setTimeout(10000);
c.onload = function(e){
fileWeatherDB.write(this.responseData);
Ti.API.info("ONLOAD = "+e);
Ti.UI.createAlertDialog({title:'Info', message:'Database installed', buttonNames: ['OK']}).show();
};
c.onerror = function(e){
Ti.UI.createAlertDialog({title:'Error', message:'Error: ' + e.error, buttonNames: ['OK']}).show();
};
c.open('GET',"http://wiki.appcelerator.org/download/attachments/6261518/weatherDB");
c.send();
});
存数据
要往数据库插入一条记录,调用execute()方法传递带SQL INSERT语句的参数,用问号作为替换符:db.execute('INSERT INTO city (name,continent,temp_f,temp_c,condition_id) VALUES (?,?,?,?,?)',importName,importContinent,importTempF,importTempC,dbConditionId);
记住SQL的特性:一次只能加一条记录,一次只能加一个表。
读数据
给execute()方法传递SQL SELECT语句作为参数,得到数据集对象,一般要赋给变量,好访问数据。数据集对象是Titanium.Database.ResultSet。下面代码遍历每一行,用isValidRow()返回结束。fieldByName()通过列名返回值,也可以在SQL 语句中就指定好。使用next()方法把ResultSet行的指针移到下一行。var cityWeatherRS = db.execute('SELECT id,name,continent FROM city');
while (cityWeatherRS.isValidRow())
{
var cityId = cityWeatherRS.fieldByName('id');
var cityName = cityWeatherRS.fieldByName('name');
var cityContinent = cityWeatherRS.fieldByName('continent');
Ti.API.info(cityId + ' ' + cityName + ' ' + cityContinent);
cityWeatherRS.next();
}
cityWeatherRS.close();
像标准SQL一样,可以把数据连接起来,下面返回city记录,和weather 记录。
db.execute('SELECT city.id,name,cond.id,cond.summary,cond.icon FROM city LEFT JOIN condition cond WHERE city.condition_id = cond.id');
或者仅返回天气是"Partly Cloudy"的行:
var cityWeatherRS = db.execute('SELECT city.id AS city_id,name,cond.id AS cond_id,cond.summary,cond.icon FROM city LEFT JOIN condition cond WHERE city.condition_id = cond.id WHERE cond.summary=?', "Partly Cloudy");
注意city.id和cond.id使用了别名。因为如果你写着这样:
fieldByName(id). 仅仅传进名字的参数,
fieldByName() 方法不可能区分它们。数据集代码如下:
while (cityWeatherRS.isValidRow())
{
var cityId = cityWeatherRS.fieldByName('city_id');
var cityName = cityWeatherRS.fieldByName('name');
var cityConditionId = cityWeatherRS.fieldByName('cond_id');
var cityConditionSummary = cityWeatherRS.fieldByName('summary');
var cityConditionIcon = cityWeatherRS.fieldByName('icon');
Ti.API.info(cityId + ' ' + cityName + ' ' + cityConditionId + ' ' + cityConditionSummary + ' ' + cityConditionIcon);
cityWeatherRS.next();
}
cityWeatherRS.close();
更新数据
和插入数据类似,和SQL 稍有不同db.execute('UPDATE condition SET icon=? WHERE id=?',importIcon,dbConditionId);
关闭数据库和数据集
前面讲过,SQLite 一次只能有一个进程写操作。所以当你完成了任何的INSERT 或 UPDATE 操作,关闭数据库极其重要。否则你会在下次写入的时候收到"DatabaseObjectNotClosed" 的错误。db.close();
开启数据集不是一个大问题,但最好养成习惯关闭它以释放系统资源。
cityWeatherRS.close();
(完)