BaaS 编程之 LeanCloud 云存储

1,304 阅读11分钟
原文链接: coderafi.github.io

我们曾经介绍了BaaS的基本概念并体验了在LeanCloud提供的BaaS环境下的开发便利,本节主要针对LeanCloud的云存储模块进行深入理解。

云存储所要达到的效果包括以下几点:

  • Write Once - 服务提供商只提供平台,服务端的代码相当于容器,只需要编写一次。
  • No Business - 服务提供商不提供业务,不再为复杂的业务而苦恼。
  • Self Control - 服务使用者具备高度灵活性,完全可以自控服务的表现形式。

云存储也就是针对数据,数据从大的来说可以分为对象数据文件数据,对象数据的操作无非就是大家通常所说的CRUD,文件数据比较常见的是读和写的操作,在这些可以归纳的模式下LeanCloud的对云存储的大体设计思路如下:

设计思路

iOS SDK中主要用到的类如下:

  • AVObject
  • AVObject+Subclass
  • AVQuery
  • AVCloudQueryResult
  • AVFile
  • AVFileQuery
  • AVACL
  • AVUser
  • AVGeoPoint

下面以一个常见师生关系模型来演示以上各种功能,希望能尽量做到全面。

环境搭建

按照LeanCloud快速上手的方式搭建一个工程名叫LCStorage,别忘了用CocoaPods安装依赖库。

Podfile如下:

pod 'Masonry'  
pod 'IQKeyboardManager'  
pod 'SWTableViewCell', '~> 0.3.7'  
pod 'AVOSCloud'  
pod 'AVOSCloudIM'  
pod 'AVOSCloudCrashReporting'  

对象存储

先来看下示例程序的样子

示例程序

增删改查

1.新建RootViewController,并设置AppDelegate的 didFinishLaunchingWithOptions 方法中的启动配置(我们依然采用纯Code的方式)。

#define LeanCloudAppId      @"pzpUEBsHbPDwyN9SurdXHkMq"
#define LeanCloudAppKey     @"zxD2M123dJftfFzMgoKc3V5r"

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

    [AVOSCloud setApplicationId:LeanCloudAppId clientKey:LeanCloudAppKey];
    [AVAnalytics trackAppOpenedWithLaunchOptions:launchOptions];

    RootViewController *rootViewController = [[RootViewController alloc] init];
    UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:rootViewController];
    self.window.rootViewController = navigationController;

    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
}

2.界面部分

我们采用一个TableView来展示获取的老师列表信息,界面部分代码如下:

#pragma mark -
#pragma mark - View
- (void)createUI
{
    //MARK::Nav
    _addTeacherTF = [[UITextField alloc] initWithFrame:CGRectMake(0, 0, 260, 40)];
    _addTeacherTF.placeholder = @"请添加用户姓名";
    _addTeacherTF.delegate = self;
    _addTeacherTF.returnKeyType = UIReturnKeyDone;
    _addTeacherTF.borderStyle = UITextBorderStyleRoundedRect;
    _addTeacherTF.backgroundColor = [UIColor clearColor];
    self.navigationItem.titleView = _addTeacherTF;

    UIButton *rightBtn = [UIButton buttonWithType:UIButtonTypeCustom];
    rightBtn.frame = CGRectMake(0, 0, 40, 40);
    [rightBtn setTitle:@"批量" forState:UIControlStateNormal];
    [rightBtn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
    [rightBtn addTarget:self action:@selector(batch) forControlEvents:UIControlEventTouchUpInside];
    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:rightBtn];


    //MARK::Content
    _teachersTB = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
    _teachersTB.dataSource = self;
    _teachersTB.delegate = self;
    _teachersTB.allowsMultipleSelectionDuringEditing = NO;
    [self.view addSubview:_teachersTB];
}

#pragma mark - UITableView DataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return _teachers.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    [tableView setTableFooterView:[[UIView alloc] initWithFrame:CGRectZero]];

    static NSString *cellId = @"Teacher Cell";
    SWTableViewCell *cell = (SWTableViewCell *)[tableView dequeueReusableCellWithIdentifier:cellId];
    if (!cell) {
        cell = [[SWTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellId];
        cell.selectionStyle = UITableViewCellSelectionStyleGray;
        cell.rightUtilityButtons = [self cellRightButtons];
        cell.delegate = self;
    }

    AVObject *teacher = [_teachers objectAtIndex:indexPath.row];
    cell.textLabel.text = [NSString stringWithFormat:@"%@ -- %d", [teacher objectForKey:@"name"], [[teacher objectForKey:@"age"] intValue]];
    cell.textLabel.font = [UIFont systemFontOfSize:26];

    return cell;
}

#pragma mark - UITableView Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
}

#pragma mark - SWTableView Cell Delegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return 60.0f;
}

- (void)swipeableTableViewCell:(SWTableViewCell *)cell didTriggerRightUtilityButtonWithIndex:(NSInteger)index
{
    [cell hideUtilityButtonsAnimated:YES];

    NSIndexPath *indexPath = [_teachersTB indexPathForCell:cell];
    AVObject *teacher = [_teachers objectAtIndex:indexPath.row];

    switch (index) {
        case 0:
            [self changeToModifyStatus:teacher];
            break;
        case 1:
            [self deleteTeacher:teacher];
            break;
        default:
            break;
    }
}

#pragma mark - UITextField Delegate

- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
    if (!_modifyObject) {
        [self addTeacher:textField.text];
    } else {
        [self modifyTeacher:textField.text];
    }
    _modifyObject = nil;
    textField.text = @"";
    [textField resignFirstResponder];
    return YES;
}

3.添加老师,我们给老师两个属性:姓名和年龄,代码如下:

- (void)addTeacher:(NSString *)teacherName
{
    AVObject *object = [AVObject objectWithClassName:@"Teacher"];
    [object setObject:teacherName forKey:@"name"];
    [object setObject:@(30) forKey:@"age"];
    [object saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
        if (error) {
            NSLog(@"添加失败");
        } else {
            NSLog(@"添加成功");
            [self getAllTeachers];
        }
    }];
}

4.获取全部老师列表,代码如下:

- (void)getTeachers
{
    AVQuery *query = [AVQuery queryWithClassName:@"Teacher"];
    [query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {

        if (error) {
            NSLog(@"加载失败");
        } else {
            NSLog(@"加载成功");
            [_teachers removeAllObjects];
            [_teachers addObjectsFromArray:objects];
            [_teachersTB reloadData];
        }

    }];
}

5.修改老师信息,代码如下:

- (void)modifyTeacher:(NSString *)teacherName;
{
    [_modifyObject setObject:teacherName forKey:@"name"];
    [_modifyObject saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
        if (error) {
            NSLog(@"修改失败");
        } else {
            NSLog(@"修改成功");
            [self getTeachers];
        }
    }];
}

6.删除老师,代码如下:

- (void)deleteTeacher:(AVObject *)teacher
{
    [teacher deleteInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
        if (error) {
            NSLog(@"删除老师失败");
        } else {
            NSLog(@"删除老师成功");
            [self getTeachers];
        }
    }];
}

7.给对象添加数组数据,比方说给老师添加一列技能的数据,代码如下:

- (void)addArrayProperty
{
    if (!_modifyObject) {
        return;
    }
    AVObject *teacher = _modifyObject;
    [teacher addObjectsFromArray:@[@"编程", @"射击", @"绘画"] forKey:@"skills"];
    [teacher saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
        if (error) {
            NSLog(@"添加数组属性数据失败");
        } else {
            NSLog(@"添加数组属性数据成功");
        }
    }];
}

既然可以在对象中存储数组类型的数据,那其他一般类型可以吗?答案是肯定的,对象一般可以存储如下类型的数据:

  • String
  • Number
  • Boolean
  • Date
  • Array
  • Dictionary(Map)
  • Data(大小不超过128K)

批处理

有时候数据量比较多,太多的网络请求会造成时间和流量的浪费,所以可以把很多数据操作合并到一起,这种操作叫做批处理。在LeanStorage中增、删、改、查都支持批处理,都是在AVObject中定义的,如下:

// 批量创建、更新
+ (BOOL)saveAll:(NSArray *)objects error:(NSError **)error;
+ (void)saveAllInBackground:(NSArray *)objects block:(AVBooleanResultBlock)block;

// 批量删除
+ (BOOL)deleteAll:(NSArray *)objects error:(NSError **)error;
+ (void)deleteAllInBackground:(NSArray *)objects block:(AVBooleanResultBlock)block;

// 批量获取
+ (BOOL)fetchAll:(NSArray *)objects error:(NSError **)error;
+ (void)fetchAllInBackground:(NSArray *)objects block:(AVArrayResultBlock)block;

例如要批量更新老师的年龄为50岁,代码如下:

- (void)batchUpdateAge
{
    //批量更新年龄
    for (AVObject *teacher in _teachers) {
        teacher[@"age"] = @(50);
    }
    [AVObject saveAllInBackground:_teachers block:^(BOOL succeeded, NSError *error) {
        if (error) {
            NSLog(@"更新老师年龄失败");
        } else {
            NSLog(@"更新老师年龄成功");
            [self getTeachers];
        }
    }];
}

子类化

你可能会想,这种对象结构太弱了吧,所有操作全用AVObject靠字符串匹配创建数据结构,这一点都不方便,我们都是自定义各自不同业务的数据结构(Model/Bean)来处理的;其实LeanCloud 云存储也是支持的,下面我们以学生的操作来看LeanCloud是如何做到的:

1.继承 AVObject
2.遵循 AVSubclassing 协议
3.实现类方法 parseClassName,返回你想定义的类名字,这就相当于[AVObject objectWithClassName:@"Your Class Name"]
4.在应用程序启动时注册一下,[YourClass registerSubclass]
5.在.h文件中定义属性
6.在.m文件中@dynamic实现属性

#import "AVObject.h"

@interface Student : AVObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSNumber *age;
@property (nonatomic, strong) AVObject *teacher;

@end
#import "Student.h"

@implementation Student

@dynamic name;
@dynamic age;
@dynamic teacher;

+ (NSString *)parseClassName
{
    return @"Student";
}

@end

像上面这样就可以自定义对象数据结构,下面我们结合关联数据来看下是如何对自定义子类进行操作的。

关联数据

现在大家可能质疑了,有了增删改查,这只是针对单一数据类型,如果不同的数据之间有关联关系怎么办?LeanCloud也是支持的,废话少说,我们来看下代码:

- (void)addStudent:(NSString *)studentName
{
    Student *student = [Student object];
    student.name = studentName;
    student.age = @(21);
    student.teacher = self.currentTeacher;
    [student saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
        if (error) {
            NSLog(@"学生保存失败");
        } else {
            NSLog(@"学生保存成功");
            [self getStudents];
        }
    }];
}

上面代码就把当前选中的老师作为了新添加学生的一个属性,逻辑层的描述就是"名字叫'studentName'的学生年龄为21岁,他的老师是currentTeacher";注意虽然这里属性teacher的值是一个AVObject对象,但是在云上只是保存老师的objectId与学生的对应关系; 同时也看到子类化是如何进行操作的。

查询

对象数据查询总体归纳为:单一查询约束查询关系查询缓存查询复合查询CQL查询 四种。

单一查询 比如我们查询下ID为‘5635c25e60b22ab52f088344’的老师,代码如下:

AVQuery *query = [AVQuery queryWithClassName:@"Teacher"];  
[query getObjectInBackgroundWithId:@"5635c25e60b22ab52f088344" block:^(AVObject *object, NSError *error) {
    if (error) {
        NSLog(@"加载失败");
    } else {
        NSLog(@"加载成功");
        [_teachers removeAllObjects];
        [_teachers addObject:object];
        [_teachersTB reloadData];
    }
}];

约束查询 比如我们查询下年龄为30且能绘画的老师,代码如下:

AVQuery *query = [AVQuery queryWithClassName:@"Teacher"];  
[query whereKey:@"age" equalTo:@(30)];
[query whereKey:@"skills" containsString:@"绘画"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
    if (error) {
        NSLog(@"加载失败");
    } else {
        NSLog(@"加载成功");
        [_teachers removeAllObjects];
        [_teachers addObjectsFromArray:objects];
        [_teachersTB reloadData];
    }
}];

关系查询 比如我们查询有名字叫'2222'年龄为21岁的学生的老师,代码如下:

AVQuery *studentQuery = [Student query];  
[studentQuery whereKey:@"age" equalTo:@(21)];
[studentQuery whereKey:@"name" equalTo:@"2222"];
[studentQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
    if (error) {
        NSLog(@"查询学生失败");
    } else {
        NSLog(@"查询学生成功");
        for (Student *student in objects) {
            AVObject *teacher = student.teacher;
            [teacher fetchIfNeededInBackgroundWithBlock:^(AVObject *object, NSError *error) {
                [_teachers addObject:object];
                [_teachersTB reloadData];
            }];
        }
    }
}];

缓存查询 在开发过程中,通常需要将一些网络请求的数据缓存到本地,以便在网络状态不好甚至是没有网络的时候依然可以有较好的用户体验,这点主要靠AVQuery来实现,它有两个属性cachePolicy和maxCacheAge,分别可以控制查询的策略和缓存数据的时间,通常查询的策略有以下几种:

  • kAVCachePolicyIgnoreCache 从不执行缓存操作
  • kAVCachePolicyCacheOnly 只查询缓存,如果没有缓存数据返回错误。
  • kAVCachePolicyCacheElseNetwork 首先尝试查询缓存数据,如果没有缓存或者缓存加载失败,则进行网络加载,如果网路加载同样失败,返回错误。
  • kAVCachePolicyNetworkElseCache 首先尝试网络加载,如果网络加载失败,则从缓存加载,如果缓存同样加载失败,返回错误。
  • kAVCachePolicyCacheThenNetwork 先执行缓存加载,再执行网络加载,这种情况下回调函数被执行两次。

比方我们加载全体老师数据,并将其缓存在本地,代码如下:

AVQuery *query = [AVQuery queryWithClassName:@"Teacher"];  
query.cachePolicy = kAVCachePolicyNetworkElseCache;  
query.maxCacheAge = 24 * 3600;  
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
    if (error) {
        NSLog(@"加载老师失败");
    } else {
        NSLog(@"加载老师成功");
        [_teachers removeAllObjects];
        [_teachers addObjectsFromArray:objects];
        [_teachersTB reloadData];
    }
}];

复合查询 例如我们查询年龄为50岁且名字中包含'n'字母的老师,代码如下:

AVQuery *nameQuery = [AVQuery queryWithClassName:@"Teacher"];  
[nameQuery whereKey:@"name" containsString:@"n"];

AVQuery *ageQuery = [AVQuery queryWithClassName:@"Teacher"];  
[ageQuery whereKey:@"age" equalTo:@(50)];

AVQuery *unionQuery = [AVQuery andQueryWithSubqueries:@[nameQuery, ageQuery]];  
[unionQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
    if (error) {
        NSLog(@"加载老师失败");
    } else {
        NSLog(@"加载老师成功");
        [_teachers removeAllObjects];
        [_teachers addObjectsFromArray:objects];
        [_teachersTB reloadData];
    };
}];

andQueryWithSubqueries 这里指的逻辑运算 'and or' 也就是说可以讲多个条件进行逻辑运算符合成一个查询条件,这里的符合查询要与前面的条件查询对比,符合查询一般是针对同一个对象多个条件进行复合来查询而条件查询一般是牵扯到数据与数据之间的一对多、多对一、多对多的查询。

CQL查询 我们可以用类SQL语句来进行数据的操作,这对关系型数据库比较熟的朋友来说,是非常友好的。 比方说我现在要统计所有老师的数量,代码如下:

NSString *cql = [NSString stringWithFormat:@"select count(*) from Teacher"];  
[AVQuery doCloudQueryInBackgroundWithCQL:cql callback:^(AVCloudQueryResult *result, NSError *error) {

    if (error) {
        NSLog(@"CQL查询失败");
    } else {
        NSLog(@"所有老师的数量为:%ld", result.count);
    }

}];

文件存储

这里我们举一个简单的例子,就是以文件的形式给老师添加简介,代码如下:

- (void)addTeacherIntroduction
{
    if (!_modifyObject) {
        return;
    }
    NSString *introdcution = @"Steve Shih Chen 陈士俊(LeanCloud创始人),生于1978年8月,中华台北市,是大众视频网站YouTube的共同创始人和首席技术官。 目前是美国杂志Business 2.0公布的全球排名第28最具影响力企业人物。陈从小在台北生活,八岁时随家人移居美国。在John Hersey读完高中,然后在Illinois Math and Science Academy和University of Illinois完成大学学业。陈是PayPal的早期雇员,在那里工作期间他认识了Chad Hurley和Jawed Karim。在2005年,这3位共同创立了YouTube。陈也是Facebook的早期雇员,然而他工作了7个月即离开并去创建了YouTube";
    NSData *introdcutionData = [introdcution dataUsingEncoding:NSUTF8StringEncoding];
    AVFile *introductionFile = [AVFile fileWithName:@"introduction.txt" data:introdcutionData];
    [_modifyObject setObject:introductionFile forKey:@"introduction_file"];
    [_modifyObject saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
        if (error) {
            NSLog(@"保存简介文件失败.");
        } else {
            NSLog(@"保存简介文件成功.");
        }
    }];

}

结果如下: LeanCloud结果

当然文件按存储主要还是用在图片处理的地方比较多,LeanCloud 提供了对图片的存取删、图片元数据操作、获取图片缩略图、获取进度等功能,这里就不再一一举例。

云代码调用

在很多业务中还是希望把比较复杂的业务逻辑统一放在云端处理,这样就避免了不同客户端业务写法不同造成的问题,也避免了客户端高频次的更新,所以LeanCoud 提供了云引擎功能,让你可以编写自己的代码统一定义接口。

1.环境配置

brew install nodejs (需翻墙)  
npm install -g avoscloud-code  

2.克隆快速上手Demo

git clone https://github.com/leancloud/node-js-getting-started.git  
cd node-js-getting-started  

3.添加应用程序配置

avoscloud add    

4.安装NodeJS 依赖库

npm install  

5.安装云引擎中间件

npm install leanengine --save  

6.定义运函数
在cloud.js添加如下代码:

var AV = require('leanengine');

/**
 * 一个简单的云代码方法
 */
AV.Cloud.define('hello', function(request, response) {  
  response.success('Hello world!');
});

/**
 * 获取老师的平均年龄
 */
AV.Cloud.define('getAverageAge', function(request, response) {  
  var query = new AV.Query('Teacher');
  query.find({
    success: function(results) {
      var sum = 0;
      for (var i = 0; i < results.length; ++i) {
        sum += results[i].get('age');
      }
      response.success(sum / results.length);
    },
    error: function() {
      response.error('get average age failed');
    }
  });
});

module.exports = AV.Cloud;  

7.部署,这里我们直接部署到正式环境。

avoscloud publish  

8.iOS客户端调用,测试代码如下:

- (void)getCloudAverageAge
{
    [AVCloud callFunctionInBackground:@"hello" withParameters:nil block:^(id object, NSError *error) {
        NSLog(@"the object is :%@", object);
    }];

    [AVCloud callFunctionInBackground:@"getAverageAge" withParameters:nil block:^(id object, NSError *error) {
        if (error) {
            NSLog(@"调用云引擎代码失败");
        } else {
            NSLog(@"调用云引擎代码成功");
            NSLog(@"老师的平均年龄是:%d", (int)[object intValue]);
        }
    }];
}

总结

云存储主要是跟数据打交道,需要对数据结构的根基比较深厚,方可封装出好用且性能好的服务,通过写完这个自己的Demo和这篇文章我对BaaS模式下的云服务封装也是理解的更加深刻了,希望这篇文章对大家有用。

Github代码