在ios实际开发当中,我们常常用到Core Data做为数据储存首选。但在处理一些大量复杂的数据值且数据之间相互关联的时候,这就不得不使用关系型数据库来实现。例如一个导航程序,自身应该包含大量的地图自身数据并且数据需要在app启动的时候就开始读取加载。而且数据本身变动不是特别频繁。重复向服务器发送请求获取信息是一件十分浪费的事情。因此我们可以用一个本地数据文件来直接配置。做为轻量级关系型数据库的sqlite是ios开发首选。而xcode本身包含了sqlite库,因此在ios使用的时候不需要额外配置文件,十分方便。下面就用一个简单例子讲述一下从安装sqlite3到一个简单demo的编写过程。
其实现效果如下:点击左边cell,可以展示商品详细信息
首先需要安装 sqlite,当前大多数使用版本为3以上。mac版传送门:https://www.sqlite.org/2016/sqlite-dll-win32-x86-3150100.zip。安装完成,在终端输入$sqlite3 ,显示版本信息即安装成功
其次需要一个简单的数据库。这里的需求是一个公司的产品目录,每个产品包括有制造商、产品名称等详细信息需要展示。sqlite有许多可视化创建渠道,用户比较多的如SQlite Manager 或者Navicat,后者是一个支持多种数据库可视化编辑的软件。在本文中不考虑可视化创建,直接使用命令行的形式。
打开mac终端输入sqlite3 (file name).db,此语句启动sqlite3 并创建一个db数据文件,(file name)为自定义文件名字。输入命令之后终端进入sqlite编辑模式,在命令提示符处输入语句:
CREATE TABLE "main"."product" ("ID" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"Name" TEXT,"ManufacturerID" INTEGER,"Details" TEXT,"Price" DOUBLE,"QuantityOnHand"INTEGER,"CountryOfOriginID" INTEGER,"Image" TEXT); 语句可自由换行,以分号结尾回车执行就得到了一个创建好的表格,其语句内容解释如下图(只解释了前三行,后序内容一样,只是数据格式不同):
为了更好的解释sqlite的关系型数据特质,我们还需要创建两个表格 语句如下:
CREATE TABLE "main"."Manufacturer"("ManufacturerID"INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"Name" TEXT NOT NULL);
CREATE TABLE "main"."Country"("CountryId" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "Country" TEXT NOT NULL);
现在我们已经有了一个简单的数据库,它拥有三个表格,但还没有数据填充,没有什么实际价值,接下来应该把数据整理到数据库里,在命令行里,我们可以使用INSERT INTO 关键字来插入一行信息到表格中。但这个方法效率及其低下,一般是做为程序运行时进行数据添加的时候使用,在后台数据整理的时候,一般是把需要的数据整理成文本文档进行导入,文本内容如下:所有文本内容和完整代码贴于 https://github.com/huafushengweirui/data/tree/master/two
1 widget A 1 details of widget A 1.29 5 1 Canvas_1
2 widget B 1 details of widget B 4.29 15 2 Canvas_2
3 widget X 1 details of widget X 0.29 25 3 Canvas_3
4 widget Y 1 details of widget Y 1.79 5 3 Canvas_4
5 widget Z 1 details of widget Z 6.26 15 4 Canvas_5
6 widget R 1 details of widget R 2.29 45 1 Canvas_6
7 widget S 1 details of widget S 3.29 55 1 Canvas_7
8 widget T 1 details of widget T 4.29 15 2 Canvas_8
9 widget L 1 details of widget L 5.29 50 3 Canvas_9
10 widget N 1 details of widget N 6.29 89 3 Canvas_10
11 widget E 1 details of widget E 17.29 26 4 Canvas_11
12 Part alpha 2 details of widget apla 1.49 25 1 Canvas_12
13 Part bata 2 details of widget bata 1.89 35 1 Canvas_13
14 Part gamma 2 details of widget gamma 3.46 45 2 Canvas_14
15 device N 3 details of device N 9.29 15 3 Canvas_15
16 device O 3 details of device O 21.29 15 3 Canvas_16
17 device P 3 details of device P 51.29 15 4 Canvas_17
18 tool A 4 details of tool A 14.99 5 1 Canvas_18
19 tool B 4 details of tool B 44.57 5 1 Canvas_19
20 tool C 4 details of tool C 6.99 5 1 Canvas_20
21 tool D 4 details of tool D 8.29 5 1 Canvas_21
自行编写的一定要注意,字段的顺序一定要与创建时候的顺序一致并且一定要用制表符tab键来分割。处理好了文本文档在sqlite命令提示符出输入.separator "\t" ,即指定制表符做为数据的分隔符,接着输入.import "(filename).txt" Priduct 回车执行,没有错误警告的话,数据内容已经导入到表格内,如果有可视化软件可以直接打开查看,如果没有,可以在终端用sql语句select * from Product查询 结果命令行显示文本内容则数据导入无误。同样方法为Manufacturer和Country填充数据,数据内容如下:
country的内容:
1 USA
2 Taiwan
3 China
4 Singapore
manufacyure的内容:
1 sprit industries
2 industrial designs
3 design intl
4 tool masters
在正式进入demo之前,或许应该提一下关系型数据库的优势与特点。认真观察一下product表格会发现在制造商以及原产地国家这样的数据栏里,我们填写的是数字integer,这明显与事实不符合,因为制造商和原产地都不可能是数字。但注意观察会发现而country和Manufacturer表格里,制造商和国家分别有不同的编号来表示,在终端输入SELECT name,country FROM Product,Country where Product.CountryOfOriginID = Country.CountryID;结果如下图:
可以在命令行看到查询结果,所有的编号内容被替换为country表格中的id对应国家,那么问题豁然明了,我们并不在主产品的数据表中明确输入制造商和原产地,而是关联其他表格中的数据标识,这样的方式在很大程度上避免了我们输入大量的重复数据,这正是关系型数据库最大的特点。例如可能是数百件产品来自同一原产地。同理我们可以通过sql关联语句查询出产品的制造商。同时,还可以直接对数据进行过滤,例如查询所有中国制造的产品 语句SELECT name,country FROM Product,Country where Product.CountryOfOriginID = Country.CountryID AND Country.Country = "China"; 其结果如下图:
在处理好数据库之后,剩下就是在代码里读取数据并展示。关于详情展示,为了方便。这里直接使用Master-details模板。这是一个简单的主从界面,能快速完成数据展示,而不用额外去设计代码。直接打开xcode,新建一个项目,选择Master-details模板。其界面如下图所示:
项目初始界面如上,在storyboard一共可以看到五个控制器视图,分别是根控制器,两个Navagation控制器,剩下俩个需要重点关注也是需要编写代码的显示视图,即master和details。根据MVC的原则分析一下项目文件,还缺一个模型类用于封装解析的数据,该模型对象内容应该包括数据表中的字段。新建一个oc类命名为Product,在头文件中添加对应每个数据库字段的属性 代码如下:
#import <Foundation/Foundation.h>
@interface Product : NSObject
@property(nonatomic)int ID;
@property(nonatomic,copy)NSString *name;
@property(nonatomic,copy)NSString *manufacture;
@property(nonatomic,copy)NSString *details;
@property(nonatomic)float price;
@property(nonatomic)int quantity;
@property(nonatomic,copy)NSString *countryforgin;
@property(nonatomic,copy)NSString *image;
@end
在处理完模型类之后,就应该从数据库从中获取数据并赋值给模型中的代码,在开发中,对于一个功能进行单独类封装能极好的降低耦合性,因此,如果编写一个通用类来执行访问数据库并获取数据的功能,会使代码更加灵活直观。所以我们需要创建一个类来与数据库交互,提供初始化数据库,关闭数据库,以及获取数据并返回为Product模型对象集合。在iOS中要使用sqlite,首先需要把sqlite的模块添加进项目。添加方法如下图
添加完sqlite3相关库之后,把编写好的数据库db文件,拖入项目文件夹,尽量放在支持文件中方便直接读取。然后创建用来和数据库交互的类,新建一个oc类,命名为Dbaccess,先导入sqlite3.h,因为我们需要用到相关函数,然后导入Product,因为我们要在拿到数据的同时对Product进行赋值,所以肯定会使用到这个类。然后分析一下类的方法需求,首先是一个初始化数据库的方法,然后是关闭数据库,这中间需要一个方法获取到数据来创建模型对象、赋值,并返回数据。大致需要三个方法。分别命名initializeDatebase、closeDatabase、getAllproducts。前两个方法没有返回对象,最后一个用于返回已经赋好值得模型对象,所以,应该是一个数组。模型对象是逐一生成的,因此不能一次性全部处理,所以为可变数组。这三个方法都需要给外界调用。所以需要在.h文件里面声明。到此为止Dbaccess.h文件的代码如下:
#import <Foundation/Foundation.h>
#import "Product.h"
#import <sqlite3.h>
@interface Dbaccess : NSObject
- (void)initializeDatebase;
-(void)closeDatabase;
-(NSMutableArray *)getAllproducts;
@end
然后是在.m文件里进行方法实现,实现代码如下:
#import "Dbaccess.h"
@implementation Dbaccess
sqlite3 *database; // 添加一个类变量保存对数据库的引用
-(id)init{ 重新类初始化方法
if (self = [super init]) {
[self initializeDatebase]; //在类初始化的时候 调用initializeDatebase方法
}
return self;
}
-(void)initializeDatebase{ //初始化数据库
NSString *path = [[NSBundle mainBundle] pathForResource:@"catalog" ofType:@"db"];定义数据表格路径
if (sqlite3_open([path UTF8String], &database) == SQLITE_OK) { //打开数据表格,如果成功 输出log
NSLog(@"opening database");
}else{// 否则 关闭数据库,并用NSAssert1函数调试,抛出异常
sqlite3_close(database); //
NSAssert1(0, @"faildd to open database:'%s'.", sqlite3_errmsg(database));
}
}
-(void)closeDatabase{ // 关闭数据库
if (sqlite3_close(database) != SQLITE_OK) {// 如果没有正常关闭,抛异常
NSAssert1(0, @"error:faild to close database:'%s'.", sqlite3_errmsg(database));
}
}
-(NSMutableArray *)getAllproducts{ 获取所有的数据,封装成模型,并返回所有的模型对象
NSMutableArray *products = [[NSMutableArray alloc]init]; // 生成一个可变数组
const char *sql = "SELECT product.ID,product.Name,\ \\设置一个变量来储存需要用到的sql语句
Manufacture.name,product.details,product.Price,\
product.Quantityonhand,country.country, \
product.Image FROM Product,Manufacture, \
Country where manufacture.manufactureID = product.manufactureID \
and product.countryforginID = country.countryid"; \\语句设置完毕
sqlite3_stmt *statement; // 创建一个sqlite语句对象 该对象用于执行sql语句
int sqlresult = sqlite3_prepare_v2(database, sql, -1, &statement, nil);//,连接数据库与sql语句,并返回结果
if (sqlresult == SQLITE_OK) { 如果返回结果为SQLTME_OK 对表格行数进行遍历
while (sqlite3_step(statement) == SQLITE_ROW) {
Product *product = [[Product alloc]init]; 每返回一行,生成一个product对象
char *name = (char *)sqlite3_column_text(statement, 1); 定义字符串name 保存从表格中取到的第一列
该函数索引是基于0的所以name是第1列
product.name = (name) ? [NSString stringWithUTF8String:name]:@""; 对Product的name属性进行赋值。
如果name不为空,则product.name = name(字符串内容),如果为空,则product.name = ""(即空字符串)
char *manufacture = (char *)sqlite3_column_text(statement, 2);
product.manufacture = (manufacture) ? [NSString stringWithUTF8String:manufacture] : @"";
char *details = (char *)sqlite3_column_text(statement, 3);
product.details = (details) ? [NSString stringWithUTF8String:details] : @"";
char *countryforgin = (char *)sqlite3_column_text(statement, 6);
product.countryforgin = (countryforgin) ? [NSString stringWithUTF8String:countryforgin] : @"";
char *image = (char *)sqlite3_column_text(statement, 7);
product.image = (image) ? [NSString stringWithUTF8String:image] : @"";
// 以上为字符串属性赋值,为了安全,校验了一下是否为空,而对于int数据类型,则直接获取赋值
product.ID = sqlite3_column_int(statement, 0);直接取出数据对Product的ID属性赋值
product.price = sqlite3_column_double(statement, 4);
product.quantity = sqlite3_column_int(statement, 5);
[products addObject:product];// 将赋值好的模型添加进数组
}
sqlite3_finalize(statement);
}else{
NSLog(@"problem with the database:");
NSLog(@"%d",sqlresult);
}
return products;// 返回包含Product的数组
}
先在数据已经全部读取完成,剩下的就是展示界面代码编写了,首先是master界面的控制器,先导入自定义的两个类,因为肯定要用到模型和数据库访问类。,然后我们需要一个属性来保存DBAccess获取到的所有模型对象,也就是一个数组,所以MasterViewController.h的代码如下:
#import <UIKit/UIKit.h>
#import "Product.h" // 导入模型类
#import "Dbaccess.h" // 导入数据访问类
@class DetailViewController;
@interface MasterViewController : UITableViewController
@property (strong, nonatomic) DetailViewController *detailViewController; 模板自带属性,保存从控制器
@property(strong,nonatomic)NSMutableArray *products; 定义数组属性保存模型对象
@end
然后编写MasterViewController.m的内容代码如下:移除了一些无需使用的代码,同时移除了模板本身提供的edit和add按钮的默认代码,因为我们只需要展示即可
#import "MasterViewController.h"
#import "DetailViewController.h"
@interface MasterViewController ()
@property NSMutableArray *objects;
@end
@implementation MasterViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
Dbaccess *dbaccess = [[Dbaccess alloc]init]; //初始化dba类对象 同时初始化数据库
self.products = [dbaccess getAllproducts]; 调用getallproduct方法 赋值给数组
[dbaccess closeDatabase];//关闭数据库
self.detailViewController = (DetailViewController *)[[self.splitViewController.viewControllers lastObject] topViewController];//设置界面
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.products count]; // 设返回值为数组的长度即product的个数
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifire = @"cell";//
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifire];
if (cell == nil) {
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifire];
Product *product = [self.products objectAtIndex:indexPath.row];
cell.textLabel.text = product.name;// 设置cell标题为product的name
}
return cell;
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
// Return NO if you do not want the specified item to be editable.
return NO; // 禁止编辑 因为模板的cell是可编辑的,我们只需要展示
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
self.detailViewController.detailItem = [self.products objectAtIndex:indexPath.row];
// 在选中某一个cell的时候为detailViewController的detailItem属性赋值
[self.navigationController pushViewController:self.detailViewController animated:YES]; // push到从界面
}
@end
在编写从界面也就是商品详情界面的代码之前,首先要搭一下界面的UI,效果如下:值得注意的是,最外层有一个透明按钮,为了页面好看一些,禁用了返回按钮,可以用透明按钮来实现轻点界面即返回的效果
在从界面控制器种关联标签属性,并且我们需要一个detailItem来保存在master界面中选中的商品,因此DetailViewController.h代码如下:
#import <UIKit/UIKit.h>
#import "Product.h"//导入模型类,因为需要用到模型
@interface DetailViewController : UIViewController
@property (weak, nonatomic) IBOutlet UILabel *namelabel;//设置文本标签关联,一定要与storyboard里的文本对应连线
@property (weak, nonatomic) IBOutlet UILabel *manufacturelabel;
@property (weak, nonatomic) IBOutlet UILabel *detailslabel;
@property (weak, nonatomic) IBOutlet UILabel *pricelabel;
@property (weak, nonatomic) IBOutlet UILabel *quantitylabel;
@property (weak, nonatomic) IBOutlet UILabel *countrylabel;
@property (strong, nonatomic) id detailItem;// 设置属性保存选中的商品信息
@end
然后进行DetailViewController.m的编写,代码如下:
#import "DetailViewController.h"
@interface DetailViewController ()
@end
@implementation DetailViewController
#pragma mark - Managing the detail item
- (void)setDetailItem:(id)newDetailItem {// 重写Detailitemset方法,可以适时刷新展示的商品内容
if (_detailItem != newDetailItem) {
_detailItem = newDetailItem;
// Update the view.
[self configureView];// 重新对展示内容赋值
}
}
- (void)configureView {// 对文本进行赋值具体实现
Product *theproduct = (Product *) self.detailItem;
self.namelabel.text = theproduct.name;
self.manufacturelabel.text = theproduct.manufacture;
self.detailslabel.text = theproduct.details;
self.detailslabel.text = theproduct.details;
self.pricelabel.text = [NSString stringWithFormat:@"%.2f",theproduct.price];
self.quantitylabel.text = [NSString stringWithFormat:@"%d",theproduct.quantity];
self.countrylabel.text = theproduct.countryforgin;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self configureView];// 对文本进行赋值
}
- (IBAction)comback:(id)sender {//监听覆盖界面的按钮点击,触发则返回上一界面
[self.navigationController popViewControllerAnimated:YES];// 返回产品栏页面
}
@end
以上,所有内容完成,博客代码可能跟源代码略有出入,所有数据都可以在https://github.com/huafushengweirui/data/tree/master/two拿到。数据和知识点来自Patrick Alessi的ios数据库应用高级编程一书当然我们没有使用到一些数据,如图片编号等,这涉及到自定义cell来展示,本篇重点在于如何从数据库获取数据而不是tableview的用法。在实际使用中,可能还涉及到对数据的参数化查询,以及将读取的数据缓存等各个方面。这里只是简单介绍一下ios和sqlite的交互。