云API是什么?我们为什么要用?
首先,我们来看一看官网对云API的介绍。
APICloud的“云API”规划的初衷是为了让开发者只需要少量、甚至不需要服务端编码,就可以自动生成移动应用所需要的各种云服务接口。“云API”服务包括数据服务、推送、云修复和大数据分析等。
APICloud的数据服务支持自动生成RESTful API,在移动场景中为APP提供灵活的数据服务支持。同时NoSQL的大量应用为APICloud的大数据提供了充分支撑。APICloud的“云API”提供基于ACL(Access Control List)和RBAC (Role Based Access Control)的访问控制模型安全机制,让开发者的移动应用在业务灵活性和安全性中找到平衡。
APICloud的“云API”具有典型的“云”特性和“大数据”特性。
为App开发者提供云端的API服务和数据存储服务,动态生成RESTful API,支持在线NoSQL数据表设计、API调试及用量分析;同时提供推送、云修复、大数据分析等服务, 极大的简化了服务器端开发工作。
一句话来说就是,我们不需要编写后台代码,直接就可以通过系统生成的RESTful API去访问数据库的内容,除此之外还提供了很多其他的功能。
废话不多说,下面就开始APICloud的云API的探索。
1.进入apicloud官网,注册一个账号。
2.登录进入开发控制台,新建一个应用。
输入应用名称与说明,创建。
3.进入到该应用的大主页。
在这里,我们可以看到我们这个应用的appKey,appID,以及其他的标识。后面会用得到。
4.我们关注的是云数据库,所以点击左侧栏Database。
点击打开云服务。
选择一个云存储服务商。七牛云是以图片音频为主的,我们暂且不需要,所以我们选择又拍云服务商,确定。
我们会发现,这个数据库自带5张系统表:accessToken、file、role、roleMapping、user。
前面的下划线,只是为了区分出这是系统表。而真正使用的时候,是不带下划线的。
他们都有什么具体的作用呢,暂且我们先不管,后面再做介绍。
这里可以自己创建class,这里的class与SQL数据库的表相对应。创建一个class,暂且可以理解为创建了一张表。我们尝试着创建一个信息Class–message。
点击创建
我们会发现,这个class默认就有3个字段。分别是id,createdAt,updateAt,代表什么意思,不言而喻。
我们为message创建一个String 类型的content字段。
其他的删除,重命名等操作,都可以在上面找得到。大家可以自行尝试。
我们想试着去操作一下这个数据库的增删改查。
OK,点击左侧的API调试。
在这里可以进行API在线测试,对表进行增删改查的操作。
1.试着先给message增加一条数据。
class选择message表,method选择create a new instance。
在body处输入:
{“content”:”第一条message的内容”}
点击发送请求
我们可以看得到请求URL路径,响应的Response Body,以及响应码,响应头。
还能看得到这个增添操作的ajax的代码实现。
好的,我们回到数据库,看看有什么变化。
的确message里面多了一条数据,测试成功。
API在线测试,还有很多,这里就不一一去尝试了。
API在线测试是成功了,那么我们在android中,如何实现这个操作呢?官网并没有给出java的实现例子。那么没关系,我们可以通过模仿ajax的网络请求去实现。
在我们的项目中,大家对okhttp的应用颇有了解。那么,我们就用okhttp去实现。
在写代码之前,首先要对RESTFUL 风格的API做一个简单的介绍。
Restful API可以让您用任何可以发送 http 请求的设备来与 API Cloud 进行交互。
这里拿官网的例子来看看。
HTTP请求的方法POST,DELETE,PUT,GET分别对应着增删改查。
我们很容易就找到了规律。
例如第一行,post是创建,路径是/mcm/api/,
也就是说为className增加一条数据。
这里不做深入的解析,有兴趣的,来这里看:
http://www.ruanyifeng.com/blog/2014/05/restful_api.html
现在开始代码测试:
在这之前,我已经给系统表user增加了一系列的自定义字段,nickname,realname等;
OkHttpUtils.post()
.url("https://d.apicloud.com/mcm/api/user")
.addHeader("Content-Type", " application/json;charset=UTF-8")
.addHeader("X-APICloud-AppId", Constant.APPID)
.addHeader("X-APICloud-AppKey", Constant.getAppkey())
.addParams("username", "qq")
.addParams("password", "123456")
.addParams("email", "270949894@qq.com")
.addParams("nickname", "wym")
.addParams("studentID", "201324133121")
.addParams("realname", "吴一鸣")
.addParams("sex", "0")
.addParams("mobile", "15622625081")
.build()
.execute(new StringCallback() {
@Override
public void onError(Call call, Exception e, int id) {
Toast.makeText(MainActivity.this, e.getMessage(), Toast.LENGTH_SHORT).show();
}
@Override
public void onResponse(String response, int id) {
Toast.makeText(MainActivity.this, response, Toast.LENGTH_SHORT).show();
}
});
运行第一次,创建一个用户,
运行第二次,用户已存在,
到数据库去看,已经多了一条数据。
下面来分析一下代码:
.addHeader("Content-Type", " application/json;charset=UTF-8")
.addHeader("X-APICloud-AppId", Constant.APPID)
.addHeader("X-APICloud-AppKey", Constant.getAppkey())
对于POST和PUT请求,请求的主体必须是 JSON 格式,而且 HTTP header 的 Content-Type 需要设置为 application/json。
用户验证是通过 HTTP header 来进行的。
X-APICloud-AppId头标明正在运行的是哪个App程序。
X-APICloud-AppKey头用来授权鉴定终端。
那么AppId和Appkey怎么得到,我们看:
public class Constant {
public static String APPID = "A6929067641945";
public static String APPKEY = "AC9AE246-4F70-D990-96E7-183E2D9EA001";
public static String getAppkey(){
long now = new Date().getTime();
return Decript.SHA1(Constant.APPID + "UZ" + Constant.APPKEY + "UZ" + now) + "." + now;
}
}
AppId从应用控制管理页面可以得到。
Appkey的获得,有这样的一个公式:
appKey = SHA1(应用ID + ‘UZ’ + 应用KEY +’UZ’ + 当前时间毫秒数)+ ‘.’ +当前时间毫秒数
其中SHA1是一个加密的方法。
.addParams("username", "qq")
.addParams("password", "123456")
不言而喻。
这里贴上SHA1的算法,也可以自己去下载:
public static String SHA1(String decript) {
try {
MessageDigest digest = java.security.MessageDigest
.getInstance("SHA-1");
digest.update(decript.getBytes());
byte messageDigest[] = digest.digest();
// Create Hex String
StringBuffer hexString = new StringBuffer();
// 字节数组转换为 十六进制 数
for (int i = 0; i < messageDigest.length; i++) {
String shaHex = Integer.toHexString(messageDigest[i] & 0xFF);
if (shaHex.length() < 2) {
hexString.append(0);
}
hexString.append(shaHex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return "";
}
好了,接着继续:
服务器数据库有了user的信息,我们就尝试获取user信息。
我们可以通过这个url去访问指定id的user。
https://d.apicloud.com/mcm/api/user/[id]
在数据库可以看到,id = 57e7905be26c90a0079722ea
我们就获取它的数据:
String url = "https://d.apicloud.com/mcm/api/user/" + "57e7905be26c90a0079722ea";
OkHttpUtils.get()
.url(url)
.addHeader("Content-Type", " application/json;charset=UTF-8")
.addHeader("X-APICloud-AppId", Constant.APPID)
.addHeader("X-APICloud-AppKey", Constant.getAppkey())
.build()
.execute(new StringCallback() {
@Override
public void onError(Call call, Exception e, int id) {
Toast.makeText(MainActivity.this, e.getMessage(), Toast.LENGTH_SHORT).show();
}
@Override
public void onResponse(String response, int id) {
Toast.makeText(MainActivity.this, response, Toast.LENGTH_SHORT).show();
}
});
返回401,401代表没有权限。
我们到后台去修改一下权限
把Get权限设置为所有用户都拥有的权限
再一次运行,可以获得数据了。
我们用膝盖想一想,估计就是没有登录,才无法获得权限。
好的,先把权限设置回仅登录才拥有权限。
接下来,就做登录操作:
访问url
https://d.apicloud.com/mcm/api/user/login
OkHttpUtils.post()
.url("https://d.apicloud.com/mcm/api/user/login")
.addHeader("Content-Type", " application/json;charset=UTF-8")
.addHeader("X-APICloud-AppId", Constant.APPID)
.addHeader("X-APICloud-AppKey", Constant.getAppkey())
.addParams("username", "qq"
.addParams("password", "123456"
.build()
.execute(new StringCallback() {
@Override
public void onError(Call call, Exception e, int id) {
Toast.makeText(MainActivity.this, e.getMessage(), Toast.LENGTH_SHORT).show();
}
@Override
public void onResponse(String response, int id) {
Toast.makeText(MainActivity.this, response, Toast.LENGTH_SHORT).show();
}
});
运行结果:
从返回结果里面,可以看到一个id,估计就是一个类token的东西。我们
用膝盖想一想,估计这个id就是证明已经登录的密钥。我们去看看数据库:
这里有一个accessTokens(数据类型为Relation,后面再解释),他是可以点击的,我们点击进去。
左侧_user下面列出了一个_accessToken,有一条数据,她的内容是:
lmmuarLbfE0k18XIakPtOZLbigtURnEKubtWRAXc0tfDxS9XfhNqppsj3SxN3W7D
他恰好就是用户登录的时候,返回的那个id。
好了,我们有了密钥,再去访问user表拿数据。依上面的例子:
添加上这一句:
.addHeader(“authorization”,”lmmuarLbfE0k18XIakPtOZLbigtURnEKubtWRAXc0tfDxS9XfhNqppsj3SxN3W7D”)
OkHttpUtils.get()
.url(url)
.addHeader("Content-Type", " application/json;charset=UTF-8")
.addHeader("X-APICloud-AppId", Constant.APPID)
.addHeader("X-APICloud-AppKey", Constant.getAppkey())
.addHeader("authorization","lmmuarLbfE0k18XIakPtOZLbigtURnEKubtWRAXc0tfDxS9XfhNqppsj3SxN3W7D")//就多了这一句
.build()
.execute(new StringCallback() {
@Override
public void onError(Call call, Exception e, int id) {
Toast.makeText(MainActivity.this, e.getMessage(), Toast.LENGTH_SHORT).show();
}
@Override
public void onResponse(String response, int id) {
Toast.makeText(MainActivity.this, response, Toast.LENGTH_SHORT).show();
}
});
这样,就可以正常访问user的数据了。
那么accessToken到底是个啥?
我们可以看到,_accessToken是个系统表。点开看:
我们能猜到,这个表就是存放登录产生的token的地方。
每一次登录,返回的id都是不一样的,那么这里肯定也就每次产生一条新的数据存放。
我们来进行多几次登录的请求:
刷新一下,_accessToken里确实多了几条数据。
接下来,继续做测试。
下面测得是,更新user的一条数据。
https://d.apicloud.com/mcm/api/user/[id]
OkHttpUtils.post()
.url("https://d.apicloud.com/mcm/api/user/"+"57e7905be26c90a0079722ea")
.addHeader("Content-Type", " application/json;charset=UTF-8")
.addHeader("X-APICloud-AppId", Constant.APPID)
.addHeader("X-APICloud-AppKey", Constant.getAppkey())
.addHeader("authorization","lmmuarLbfE0k18XIakPtOZLbigtURnEKubtWRAXc0tfDxS9XfhNqppsj3SxN3W7D")
.addParams("_method","PUT")
.addParams("nickname","更新名字")
.build()
.execute(new StringCallback() {
@Override
public void onError(Call call, Exception e, int id) {
Toast.makeText(MainActivity.this, e.getMessage(), Toast.LENGTH_SHORT).show();
}
@Override
public void onResponse(String response, int id) {
Toast.makeText(MainActivity.this, response, Toast.LENGTH_SHORT).show();
}
});
更新成功。
其实一切的方法都可以用post去提交,然后通过这个方法来设置方法。
.addParams(“_method”,”PUT”)
下面测试批次操作:
为了减少网络交互的次数太多带来的时间浪费, 您可以在一个请求中对多个对象进行 create/update/delete 操作.
在一个批次中每一个操作都有相应的方法、路径和主体, 这些参数可以代替您通常会使用的 HTTP 方法. 这些操作会以发送过去的顺序来执行,
官方ajax的实现是这样的:
$.ajax({
"url": "https://d.apicloud.com/mcm/api/batch",
"type": "POST",
"cache": false,
"headers": {
"X-APICloud-AppId": "{{your_app_id}}",
"X-APICloud-AppKey": "{{加密后的key}}"
},
"data": {
"requests": [{
"method": "POST",
"path": "/mcm/api/company",
"body": {
"name": "apicloud",
"address": "北京市..."
}
},
{
"method": "POST",
"path": "/mcm/api/company",
"body": {
"name": "百度",
"address": "北京市西二旗"
}
}]
}
}).done(function (data, status, header) {
//success body
}).fail(function (header, status, errorThrown) {
//fail body
})
不过,使用okhttp似乎是模拟不了这个参数的传递。如果你有方法,请告诉我。
官方提供了httpclient的一个封装java-sdk包,去操作这些增删改查。可以到这里下载:
https://github.com/APICloud-com/Java-sdk
里面也有使用教程。
下面我们进行一个批次操作,生成两个user对象。
Resource resource = new Resource(Constant.APPID, Constant.APPKEY, null);
//如果操作需要权限,在这里设置权限,设置token
//resource.setAuthorization("lmmuarLbfE0k18XIakPtOZLbigtURnEKubtWRAXc0tfDxS9XfhNqppsj3SxN3W7D");
JSONObject params = new JSONObject();
JSONArray array = new JSONArray();
JSONObject json = new JSONObject();
json.put("method", "POST");
json.put("path", "/mcm/api/user");
JSONObject body = new JSONObject();
body.put("username", "user1");
body.put("password", "123456");
json.put("body", body);
array.add(json);
JSONObject json1 = new JSONObject();
json1.put("method", "POST");
json1.put("path", "/mcm/api/user");
JSONObject body1 = new JSONObject();
body1.put("username", "user2");
body1.put("password", "123456");
json1.put("body", body1);
array.add(json1);
params.put("requests", array);
final String returnJson = resource.batch(params);
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, returnJson, Toast.LENGTH_SHORT).show();
}
});
}
}).start();
返回数据正常。
数据库里也多了两条数据。
需要注意的是:
1.okhttp的请求是在子线程执行的,而这个java-sdk是基于httpclient的,她的请求是在原线程执行的,所以要放到子线程里去操作。
2.这里的appkey是不需要自己手动去加密的,sdk已经进行了封装。
我们可以看一下他的源码:
private String domain = "https://d.apicloud.com";
/**
* @param appId
* @param appKey
* @param domain 为空或者null为默认https
*/
public Resource(String appId,String appKey,String domain){
if(null!=domain&&!"".equals(domain)){
this.domain = domain;
}
headers.put("X-APICloud-AppId", appId);
headers.put("X-APICloud-AppKey", HttpUtils.encrypt(appId,appKey,"SHA-1"));
}
你们做了尝试了吗?是否发现结果报错了。
java.lang.ClassCastException: com.alibaba.fastjson.JSONArray cannot be cast to com.alibaba.fastjson.JSONObject
没错,因为这一次返回的不是一个对象,而是一个数组。
我们可以追踪代码
public String batch(JSONObject params){
//校验是否传递参数
if(params==null){
//return JSONObject.parseObject("{status:0,msg:\"请传递参数\"}");
return "{status:0,msg:\"请传递参数\"}";
}
String url = domain+"/mcm/api/batch";
headers.put("Content-Type", "application/json");
return HttpUtils.doPost(url, headers, null, params.toJSONString());
}
batch调用了doPost
public static JSONObject doPost(String url,Map<String,String> headers,Map<String,String> params,String jsonString) {
HttpClient client = new HttpClient();
//post请求
PostMethod postMethod = new PostMethod(url);
//设置header
setHeaders(postMethod,headers);
//设置post请求参数
setParams(postMethod,params);
//设置post传递的json数据
if(jsonString!=null&&!"".equals(jsonString)){
postMethod.setRequestEntity(new ByteArrayRequestEntity(jsonString.getBytes()));
}
String responseStr = "";
try {
client.executeMethod(postMethod);
responseStr = postMethod.getResponseBodyAsString();
} catch (Exception e) {
log.error(e);
e.printStackTrace();
responseStr="{status:\""+e.toString()+"\"}";
//responseStr="{status:0}";
}
return JSONObject.parseObject(responseStr);
}
错误就在这一句
JSONObject.parseObject(responseStr);
因为respinseStr是一个数组。
我们可以手动改成直接改成
return responseStr;
不需要进行转化。其他的地方跟着改变一下即可。
批量操作的响应会是一个列表, 列表的元素数量和给定的操作数量是一致的。
剩下的操作,都是类似的了。自己去摸索吧。
下面把该sdk的操作都集中快速过一遍
创建对象createUser:
resource = new Resource(Constant.APPID, Constant.APPKEY, null);
JSONObject jsonObject = new JSONObject();
jsonObject.put("username", "user4");
jsonObject.put("password", "123456");
JSONObject jsonObjectResult = resource.createUser(jsonObject);
用户登录:
resource = new Resource(Constant.APPID, Constant.APPKEY, null);
JSONObject jsonObjectResult = resource.userLogin("qq", "123456");
获取指定id的对象:
resource = new Resource(Constant.APPID, Constant.APPKEY, null);
resource.setAuthorization(loginBean.getId());
JSONObject jsonObjectResult = resource.getObject("user",loginBean.getUserId());
筛选指定条件的user:
resource.setAuthorization(loginBean.getId());
JSONObject jsonObjectResult = resource.doFilterSearch("user", "{\"where\":{\"username\":\"qq\"}}");
更新user:
resource.setAuthorization(loginBean.getId());
JSONObject jsonObject = new JSONObject();
jsonObject.put("nickname", "yummy");
JSONObject jsonObjectResult = resource.updateObject("user", loginBean.getUserId(), jsonObject);
下面是重点了。
针对1对n的设计,我们需要考虑集合的大小规模,并不是所有一切都在父文档中内嵌一个数组子文档,这有三种方案:
一.1对很少(例如一个人有多部手机,或者多个地址,这些顶多就数个,很少)
这种情况,可以直接把文档嵌入。不过需要考虑到,是否需要独立实体。如果需要独立,则采用二方法。
因为这种设计具有内嵌文档设计中所有的优缺点。最主要的优点就是不需要单独执行一条语句去获取内嵌的内容。最主要的缺点是你无法把这些内嵌文档当做单独的实体去访问。
二.1对多(例如一个产品有几百个零件,一般不会超过1000个)
这种情况,用一个数组存放objectId。
这种引用的方式是对内嵌优缺点的补充。每个零件是个单独的文档,可以很容易的独立去搜索和更新他们。
巧妙的是,这一种方式,其实也是多对多的一种解决方案。一个产品可以嵌入多个产品objectID,一个零件又可以被多个产品嵌入objectID,所以在多对多时不需要一张单独的连接表。
三.1对很多(例如一部机器有非常多的日志)
所以采用“父级引用”,可以理解为关系数据库的外键一样。在每个日志文档中保存这个主机的ObjectID。
关于数据库设计,参考以下链接,有兴趣的自己去读。
http://www.jianshu.com/p/bb0caddff60a
有了这些设计的概念,接下来,我们就开始设计我们的数据库了。
假设一个用户user,会收到消息message。
很明显这是一个一对多的例子,一个user会有多条信息。
1.先去建立一个数据表message,并且添加一个content(String)字段。
2.user表和message产生关联。
这里考虑到,一个用户会有很多的信息。我们考虑采用上面提到的数据库设计法则的方法二。
这里采用Relation(用于一对多)类型来实现。为user表添加一个Relation类型的指向目标表message的字段。
我们又想有一个需求,能通过某条信息,快速定位这条信息是由谁发出的。
这里采用Pointer(用于一对一)类型来实现.为message表添加一个Pointer类型的指向目标表user的字段。
表单创建完毕,回到程序中做测试:
假设现在有一条信息,归属于某个指定id的user,我们可以这样存储这条信息。
resource.setAuthorization(loginBean.getId());
JSONObject params = new JSONObject();
params.put("content", "信息");
params.put("user", loginBean.getUserId());
JSONObject returnJson = resource.createRelationObject("user", loginBean.getUserId(), "message", params);
返回结果:
数据库:
user表
点击message的Relation,进入user下的message
message表
这样就实现了一对多的需求。
我们直接也通过id去访问user,
resource.getObject("user",loginBean.getUserId())
返回结果:
同样的操作,如果是通过okhttp来访问,则返回这样的结果:
比上面的多了几个字段:status,realm,verificationToken。
猜出,或许为null值的,okhttp一样解析。而resource的处理则是去掉Null值的字段。
在结果里并没有返回有关于message的相关信息。
我们采用getRelationObject去获取指定用户的所有message:
resource.setAuthorization(loginBean.getId());
JSONObject params = new JSONObject();
JSONObject returnJson = resource.getRelationObject("user", loginBean.getUserId(), "message");
返回了message信息的结果,里面有一个user(uz*R*id)的字段。
我们直接也通过id去访问Message,返回结果:
这个结果和上面的访问结果是一模一样的。
而作为pointer类型的”user”字段,存放的是user的id;
我们可以通过include 来展示relation 和pointer指向的数据。
JSONObject jsonObjectResult = resource.doFilterSearch("user", "{\"include\":{\"message\":\"userPointer\"},\"where\":{\"username\":\"qq\"}}");
不知为何,总是报401,理应登录了就可以正常获取数据。问了客服也没有解决。暂且先放着这个问题。
为了能继续进行我们的探究,把后台user表find权限打开,再一次运行
可能有点乱,不过还是可以看得到这里筛选出指定username为”qq”的User信息,并且把user里面的relation指向的message数据也展示出来。message里面的pointer指向的数据user也展示出来。
大体上,我们就把api走了一个流程,其中的几个坑也都遇到了。对于其他的更多了方法,可以去官方文档去看。
最后才发现,我的截图数据,都是我的信息~~好可怕。也不想回去做重复工作去改了。