从SQLite获取数据完成一个产品信息展示

     在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的交互。

posted @ 2016-11-22 23:49  佶屈聱牙  阅读(1219)  评论(1编辑  收藏  举报