I will hold a workshop of Google App Engine for the next codekata activity, welcome to join me.
Preview:
Click Here: http://jhaddressbook.appspot.com/contact
Slides:
I will hold a workshop of Google App Engine for the next codekata activity, welcome to join me.
Preview:
Click Here: http://jhaddressbook.appspot.com/contact
Slides:
今天运气很不错,小路考抽到起伏路,Happy,顺利通过。
代码如下:
<script type="text/javascript" src="http://code.jquery.com/jquery-1.4.2.min.js"></script>
<script type="text/javascript" src="http://www.haojii.com/webdev/fancyboxdemo/fancybox/jquery.mousewheel-3.0.2.pack.js"></script>
<script type="text/javascript" src="http://www.haojii.com/webdev/fancyboxdemo/fancybox/jquery.fancybox-1.3.1.js"></script>
<link rel="stylesheet" type="text/css" href="http://www.haojii.com/webdev/fancyboxdemo/fancybox/jquery.fancybox-1.3.1.css" media="screen" />
<script type="text/javascript">
$(document).ready(function() {
$("#login").fancybox({
'padding' : 0,
'autoScale' : false,
'transitionIn' : 'none',
'transitionOut' : 'none'
});
$("#flash").fancybox({
'padding' : 0,
'autoScale' : false,
'transitionIn' : 'none',
'transitionOut' : 'none'
});
});
</script>
<div>
<h1><a id="login" href="#register-form" title="This is the title for register form">猛击我吧!</a></h1>
<div style="display:none">
<!-- Registration -->
<div id="register-form" style="width: 500px; height: 300px; margin:auto;">
<h1><a id="flash" href="http://www.adobe.com/jp/events/cs3_web_edition_tour/swfs/perform.swf">A Flash Demo</a></h1>
</div>
</div>
<!-- /Registration -->
</div>
美剧更新自动提醒服务已更新: http://jhnotifier.appspot.com/
源码host在: http://code.google.com/p/jhnotifier/
确切的说,这个应用目前只能叫做Easytv 更新通知服务,目前仅适合追美剧的有线通用户,当然追国产电视剧也是行的,程序的功能是美剧有更新时,主动发送邮件到你的邮箱实现通知。
现在支持多用户定制自己关心的电视剧,使用邮箱登陆上面的地址后,只要在Step2的表单里填入你关心的easytv剧集的URL地址即可体验,欢迎试用。
计划增加的功能:
1. 短信提醒
2. 支持任意用户,对关心的Easytv美剧提交自己的信息,收到更新通知 Done!
3. 增加其他的爬网页,发通知服务(例如美剧论坛BT种子更新,天气预报,或者是定制程度更高的应用)
本文描述如果使用GoogleAppEngine的数据存储服务,即如何在GAE上创建,删除,查询数据。重点例举四种类型主键的使用。本文是GAE 文档Creating, Getting and Deleting Data的扩展展开。
代码示例基于 appengine-java-sdk-1.3.3.1 +GAE Eclipse插件
不管是使用传统的关系型数据库还是使用NOSQL架构的数据存储,我们的应用程序存储实体对象时一般都需要主键,方便于之后的数据查询和更新。
JavaEE架构中使用JPA规范统一POJO在关系型数据库中的映射与持久化。因为JPA主要是为关系型数据库设计的,所以GAE仅支持部分JPA规范的内容,GAE文档里关于JPA的说明也比较简陋。
GAE文档里主要介绍基于JDO的数据存储,JDO规范的好处是它抽象级别更高,并不关心底层的数据库是何种实现,所以对于Google的BigTable架构比JPA适用,当前GAE仅实现了JDO规范里面的部分内容(基于开源的DataNucleus)。
GAE文档里的最简单例子,使用JDO存储数据的4个步骤是
Employee.java
import java.util.Date;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.IdentityType;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;
@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Employee {
@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
private Long id;
@Persistent
private String firstName;
@Persistent
private String lastName;
@Persistent
private Date hireDate;
public Person(String firstName, String lastName, Date hireDate) {
this.firstName = firstName;
this.lastName = lastName;
this.hireDate = hireDate;
}
// Accessors for the fields. JDO doesn't use these, but your application does.
public Long getId() {
return id;
}
public String getFirstName() {
return firstName;
}
// ... other accessors...
}
@PersistenceCapable(identityType = IdentityType.APPLICATION)标识该POJO能够通过JDO存储到数据存储区。
@Persistent标识字段为需要持久存储的
@PrimaryKey标识字段为主键
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)标识在已经被标识为主键的Long字段上标识该主键值自动填充
这两步GAE Eclipse插件都帮我们搞定了
PMF.java
import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManagerFactory;
public final class PMF {
private static final PersistenceManagerFactory pmfInstance =
JDOHelper.getPersistenceManagerFactory("transactions-optional");
private PMF() {}
public static PersistenceManagerFactory get() {
return pmfInstance;
}
}
import java.util.Date;
import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;
import Employee;
import PMF;
// ...
Employee employee = new Employee("Alfred", "Smith", new Date());
PersistenceManager pm = PMF.get().getPersistenceManager();
try {
pm.makePersistent(employee);
} finally {
pm.close();
}
JDO规范的查询格式和JPA的其实差不多
import java.util.List;
import Employee;
// ...
String query = "select from " + Employee.class.getName() + " where lastName == 'Smith'";
List<Employee> employees = (List<Employee>) pm.newQuery(query).execute();
上文中已经领略最简单的主键的例子,在GAE中所有实体都有一个唯一的标识唯一性的键值。根据文档:一个完整的键包含若干条信息,其中包括应用程序ID、类型和实体ID。
直接读这个描述的话可能不太直观,这三个元素一起才能标识你需要在GAE上存储的实体,其实和JavaEE里配置DataSource+JPA ORM Annotation是类似的,可以这么比较
应用程序ID:相当于关系型数据库的Schema
类型:相当于关系型数据库的表
实体ID:相当于关系型数据库的主键
实体的主键字段可以有四种类型,长整型,字符串(未编码字符串),编码字符串形式的主键,com.google.appengine.api.datastore.Key 前面两个相对简单常见,比较容易用,后面两个稍微有点绕。
UserLong.java POJO数据类
import java.util.Date;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.IdentityType;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;
@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class UserLong {
@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
private Long id;
@Persistent
private Date date = new Date();
@Persistent
private String name;
@Persistent
private String type;
public UserLong() {
}
public UserLong(String name, String type) {
super();
this.name = name;
this.type = type;
}
// ...
单元测试类TestUserWithLongId,对使用长整型字段为主键的对象的创建,查询,删除
import java.util.List;
import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
import com.google.appengine.tools.development.testing.LocalTaskQueueTestConfig;
import com.haojii.jdo.utils.PMF;
public class TestUserWithLongId {
private static final LocalServiceTestHelper helper = new LocalServiceTestHelper(
new LocalTaskQueueTestConfig(),
new LocalDatastoreServiceTestConfig());
@BeforeClass
public static void setUpBeforeClass() throws Exception {
helper.setUp();
}
@AfterClass
public static void tearDownAfterClass() throws Exception {
helper.tearDown();
}
@Test
public void doTest() {
PersistenceManager pm = PMF.get().getPersistenceManager();
System.out.println("0 Create and persist UserLong with @PrimaryKey: Long id");
UserLong userLong = new UserLong("name1",UserLong.class.getSimpleName());
pm.makePersistent(userLong);
System.out.println("1 Query UserLong with getObjectById");
UserLong tmpUserLong = null;
Long id = userLong.getId();
tmpUserLong = pm.getObjectById(UserLong.class, id);
System.out.println("userLong==tmpUserLong?"+(userLong==tmpUserLong));
Assert.assertTrue(userLong == tmpUserLong);
System.out.println("2 Query UserLong with name1");
tmpUserLong = null;
Query query = pm.newQuery(UserLong.class);
query.setFilter("name == nameParam");
query.setOrdering("date desc");
query.declareParameters("String nameParam");
try {
List<UserLong> results = (List<UserLong>) query.execute("name1");
System.out.println("All UserLong size:"+results.size());
Assert.assertEquals(1, results.size());
if (results.iterator().hasNext()) {
for (UserLong e : results) {
tmpUserLong = e;
System.out.println(" "+e);
}
} else {
System.out.println("2 Query get empty result");
}
} finally {
query.closeAll();
}
Assert.assertTrue(tmpUserLong!=null);
if (tmpUserLong != null)
{
System.out.println("3 Update the searched result[0] UserLong with name1->NAME!");
System.out.println("userLong==tmpUserLong?"+(userLong==tmpUserLong));
Assert.assertTrue(userLong == tmpUserLong);
tmpUserLong.setName("NAME!");
pm.makePersistent(tmpUserLong);
}
System.out.println("4 Delete UserLong with name1");
System.out.println(" ->"+userLong);
pm.deletePersistent(userLong);
query = pm.newQuery(UserLong.class);
int size = ((List<UserLong>) query.execute()).size();
System.out.println("All UserLong size:"+size);
Assert.assertEquals(0, size);
}
}
2.1.1: LocalServiceTestHelper是为GAE本地单元测试提供的辅助类,这里不再赘述,若不明白看一下前面的link的参考文档。
2.1.2: 创建持久化对象pm.makePersistent(entityObject);
2.1.3: 删除持久化对象pm.deletePersistent(entityObject);
2.1.4: 查询长整型主键的对象
方法一,pm.getObjectById(UserLong.class, id)
第一个参数是实体对象的类型,第二个参数是能标识实体的主键这里是长整型的id
方法二,使用JDO查询,可设置查询条件,Query query = pm.newQuery(UserLong.class);
这个查询返回的是实体的列表集合
UserUnEncodedString.java
@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class UserUnEncodedString {
@PrimaryKey
private String name;
@Persistent
private String type;
@Persistent
private Date date = new Date();
public UserUnEncodedString() {
}
public UserUnEncodedString(String name, String type) {
super();
this.name = name;
this.type = type;
}
TestUserWithUnEncodedStringId.java
@Test
public void doTest() {
PersistenceManager pm = PMF.get().getPersistenceManager();
System.out.println("0 Create and persist UserUnEncodedString with @PrimaryKey: String name");
UserUnEncodedString userUnEncodedString = new UserUnEncodedString("key_name1",UserUnEncodedString.class.getSimpleName());
pm.makePersistent(userUnEncodedString);
System.out.println("1 Query UserUnEncodedString with getObjectById");
UserUnEncodedString tmpUserUnEncodedString = null;
String key = userUnEncodedString.getName();
tmpUserUnEncodedString = pm.getObjectById(UserUnEncodedString.class, key);
System.out.println("UserUnEncodedString==tmpUserUnEncodedString?"+(userUnEncodedString==tmpUserUnEncodedString));
Assert.assertTrue(userUnEncodedString==tmpUserUnEncodedString);
System.out.println("2 Query UserUnEncodedString with key_name1");
tmpUserUnEncodedString = null;
Query query = pm.newQuery(UserUnEncodedString.class);
query.setFilter("name == nameParam");
query.setOrdering("date desc");
query.declareParameters("String nameParam");
try {
List<UserUnEncodedString> results = (List<UserUnEncodedString>) query.execute("key_name1");
System.out.println("All UserUnEncodedString size:"+results.size());
Assert.assertEquals(1, results.size());
if (results.iterator().hasNext()) {
for (UserUnEncodedString e : results) {
tmpUserUnEncodedString = e;
System.out.println(" "+e);
}
} else {
System.out.println("2 Query get empty result");
}
} finally {
query.closeAll();
}
Assert.assertTrue(tmpUserUnEncodedString != null);
if (tmpUserUnEncodedString != null)
{
System.out.println("3 Update the searched result[0] UserUnEncodedString with key_name1->KEY_NAME!");
System.out.println("UserUnEncodedString==tmpUserUnEncodedString?"+(userUnEncodedString==tmpUserUnEncodedString));
Assert.assertTrue(userUnEncodedString==tmpUserUnEncodedString);
tmpUserUnEncodedString.setName("KEY_NAME!");
pm.makePersistent(tmpUserUnEncodedString);
}
System.out.println("4 Delete UserUnEncodedString with key_name1");
System.out.println(" ->"+userUnEncodedString);
pm.deletePersistent(userUnEncodedString);
query = pm.newQuery(UserUnEncodedString.class);
int size = ((List<UserUnEncodedString>) query.execute()).size();
System.out.println("All UserUnEncodedString size:"+size);
Assert.assertEquals(0, size);
}
2.2.1 这里也没有特别的地方,值得稍微说一下的是我们使用未编码的字符串作为主键,则需要我们自己保证主键的键值是唯一的,否则makePersistent方法就是更新对象,而不是创建。这个方法pm.getObjectById(UserUnEncodedString.class, key);接受未编码的字符串为第二个参数用于查询。
UserKey.java
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class UserKey {
@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
private Key key;
@Persistent
private Date date = new Date();
@Persistent
private String name;
@Persistent
private String type;
public UserKey() {
}
public UserKey(String name, String type) {
super();
this.key = KeyFactory.createKey(UserKey.class.getSimpleName(), name);
this.name = name;
this.type = type;
}
2.3.1 首先看这个POJO数据类,主键标识在下面这个字段上Key,参考GAE文档,它的键值包括父实体组(如果有)的键以及应用程序分配的字符串ID或系统生成的数字ID。要创建带应用程序分配的字符串ID的对象,请创建带有该ID的键值并将字段设置为该值。要创建带系统分配的数字ID的对象,请将键字段留为null。
这里我们选择使用程序自己分配字符串ID的Key,使用KeyFactory.createKey(String kind, String name)方法创建Key实例,第一个参数即上文提到的主键组成成分之一类型,第二个参数是标识这个主键的任意唯一的字符串,我的理解是和未编码字符串主键基本上一致。
TestUserWithKeyId.java
@Test
public void doTest() {
PersistenceManager pm = PMF.get().getPersistenceManager();
System.out.println("0 Create and persist UserLong with @PrimaryKey: Key");
UserKey user = new UserKey("name1",UserKey.class.getSimpleName());
pm.makePersistent(user);
System.out.println("1 Query UserKey with getObjectById");
UserKey tmpUser = null;
Key key = user.getKey();
tmpUser = pm.getObjectById(UserKey.class, key);
System.out.println("(1)user==tmpUser ? "+(user==tmpUser));
Assert.assertTrue(user==tmpUser);
Key createdKey = KeyFactory.createKey(UserKey.class.getSimpleName(), user.getName());
tmpUser = pm.getObjectById(UserKey.class, createdKey);
System.out.println("(2)user==tmpUser ? "+(user==tmpUser));
Assert.assertTrue(user==tmpUser);
System.out.println("2 Query UserKey with name1");
tmpUser = null;
Query query = pm.newQuery(UserKey.class);
query.setFilter("name == nameParam");
query.setOrdering("date desc");
query.declareParameters("String nameParam");
try {
List<UserKey> results = (List<UserKey>) query.execute("name1");
System.out.println("All UserKey size:"+results.size());
Assert.assertEquals(1, results.size());
if (results.iterator().hasNext()) {
for (UserKey e : results) {
tmpUser = e;
System.out.println(" "+e);
}
} else {
System.out.println("2 Query get empty result");
}
} finally {
query.closeAll();
}
Assert.assertTrue(tmpUser != null);
if (tmpUser != null)
{
System.out.println("3 Update the searched result[0] UserKey with name1->NAME!");
System.out.println("user==tmpUser?"+(user==tmpUser));
Assert.assertTrue(user==tmpUser);
tmpUser.setName("NAME!");
pm.makePersistent(tmpUser);
}
System.out.println("4 Delete UserKey with name1");
System.out.println(" ->"+user);
pm.deletePersistent(user);
query = pm.newQuery(UserKey.class);
int size = ((List<UserKey>) query.execute()).size();
System.out.println("All UserKey size:"+size);
Assert.assertEquals(0, size);
}
2.3.2 测试用例基本上和前面的一致,这里稍微值得说一下的是使用自己创建的key进行查询Key createdKey = KeyFactory.createKey(UserKey.class.getSimpleName(), user.getName());pm.getObjectById(UserKey.class, createdKey);本质上和上一种未编码字符串是一样的。
UserEncodedString.java
@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class UserEncodedString {
@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
@Extension(vendorName="datanucleus", key="gae.encoded-pk", value="true")
private String encodedKey;
@Extension(vendorName="datanucleus", key="gae.pk-name", value="true")
private String keyName;
//"gae.pk-name" 字段可以在保存对象前设置为键名称。保存对象时,将使用包括键名称的完整键填充编码键字段。其类型必须为 String。
@Persistent
private Date date = new Date();
@Persistent
private String name;
@Persistent
private String type;
public UserEncodedString() {
}
public UserEncodedString(String keyName, String name, String type) {
super();
this.encodedKey = KeyFactory.keyToString(KeyFactory.createKey(UserEncodedString.class.getSimpleName(), keyName));
this.keyName = keyName;
this.name = name;
this.type = type;
}
2.4.1 这个POJO数据存储类,更绕了一点点弯路,可以和上面的一种使用的Key作为主键的POJO数据类稍微对比一下看,然后就不会觉得太复杂了。
正如注释中所写
@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
@Extension(vendorName=”datanucleus”, key=”gae.encoded-pk”, value=”true”)
private String encodedKey;
@Extension(vendorName=”datanucleus”, key=”gae.pk-name”, value=”true”)
private String keyName;
//”gae.pk-name” 字段可以在保存对象前设置为键名称。保存对象时,将使用包括键名称的完整键填充编码键字段。其类型必须为 String。
this.encodedKey = KeyFactory.keyToString(KeyFactory.createKey(UserEncodedString.class.getSimpleName(), keyName));
keyName字段只是用来产生使用程序自己分配字符串ID的Key的,根据这个字符串ID产生Key之后,将Key的编码字符串格式保存于encodedKey当中。
我的理解是可以encodedKey的作用==Key的作用,只是为了某些情况下使用方便。
TestUserWithEncodedStringId.java
@Test
public void doTest() {
PersistenceManager pm = PMF.get().getPersistenceManager();
System.out.println("0 Create and persist UserLong with @PrimaryKey: String encodedKey");
UserEncodedString userEncodedString = new UserEncodedString("name1","name1",UserEncodedString.class.getSimpleName());
pm.makePersistent(userEncodedString);
System.out.println("1 Query UserEncodedString with getObjectById, using KeyFactory.stringToKey(encodedKey)");
UserEncodedString tmpUserEncodedString = null;
String encodedKey = userEncodedString.getEncodedKey();
System.out.println(userEncodedString);
Key key = KeyFactory.stringToKey(encodedKey);
System.out.println("KeyFactory.stringToKey(encodedKey)="+key);
tmpUserEncodedString = pm.getObjectById(UserEncodedString.class, key);
System.out.println("UserEncodedString==tmpUserEncodedString?"+(userEncodedString==tmpUserEncodedString));
Assert.assertTrue(userEncodedString==tmpUserEncodedString);
System.out.println("1(1) Query UserEncodedString with getObjectById, using KeyFactory.createKey(UserEncodedString.class.getSimpleName(), userEncodedString.getKeyName())");
key = KeyFactory.createKey(UserEncodedString.class.getSimpleName(), userEncodedString.getKeyName());
tmpUserEncodedString = pm.getObjectById(UserEncodedString.class, KeyFactory.keyToString(key));
System.out.println(" (1)UserEncodedString==tmpUserEncodedString?"+(userEncodedString==tmpUserEncodedString));
Assert.assertTrue(userEncodedString==tmpUserEncodedString);
System.out.println("1(2) Query UserEncodedString with getObjectById, using userEncodedString.getEncodedKey():"+userEncodedString.getEncodedKey());
tmpUserEncodedString = pm.getObjectById(UserEncodedString.class, userEncodedString.getEncodedKey());
System.out.println(" (2)UserEncodedString==tmpUserEncodedString?"+(userEncodedString==tmpUserEncodedString));
Assert.assertTrue(userEncodedString==tmpUserEncodedString);
System.out.println("2 Query UserEncodedString with name1");
tmpUserEncodedString = null;
Query query = pm.newQuery(UserEncodedString.class);
query.setFilter("name == nameParam");
query.setOrdering("date desc");
query.declareParameters("String nameParam");
try {
List<UserEncodedString> results = (List<UserEncodedString>) query.execute("name1");
System.out.println("All UserEncodedString size:"+results.size());
Assert.assertEquals(1, results.size());
if (results.iterator().hasNext()) {
for (UserEncodedString e : results) {
tmpUserEncodedString = e;
System.out.println(" "+e);
}
} else {
System.out.println("2 Query get empty result");
}
} finally {
query.closeAll();
}
Assert.assertTrue(tmpUserEncodedString != null);
if (tmpUserEncodedString != null)
{
System.out.println("3 Update the searched result[0] UserEncodedString with name1->NAME!");
System.out.println("UserEncodedString==tmpUserEncodedString?"+(userEncodedString==tmpUserEncodedString));
Assert.assertTrue(userEncodedString==tmpUserEncodedString);
tmpUserEncodedString.setName("NAME!");
pm.makePersistent(tmpUserEncodedString);
}
System.out.println("4 Delete UserEncodedString with name1");
System.out.println(" ->"+userEncodedString);
pm.deletePersistent(userEncodedString);
query = pm.newQuery(UserEncodedString.class);
int size = ((List<UserEncodedString>) query.execute()).size();
System.out.println("All UserEncodedString size:"+size);
Assert.assertEquals(0, size);
}
2.4.2 测试用例还是和前面的一致,这里要说的就是下面三个使用getObjectById的查询结果是一致的。
pm.getObjectById(UserEncodedString.class, userEncodedString.getEncodedKey());
第一个使用Key的编码字符串格式直接查询
String encodedKey = userEncodedString.getEncodedKey();
Key key = KeyFactory.stringToKey(encodedKey);
pm.getObjectById(UserEncodedString.class, key);
第二个使用KeyFactory.stringToKey方法根据key查询
key = KeyFactory.createKey(UserEncodedString.class.getSimpleName(), userEncodedString.getKeyName());
pm.getObjectById(UserEncodedString.class, KeyFactory.keyToString(key));
第三个使用程序自己分配字符串ID生成的Key来查询
本文介绍了使用GAE存取数据的基本方法,主要展开并演示了使用4种主键的数据对象存取方法,属于GoogleAppEngine的入门级别文章。
对象关系,事务等相关内容也是构建GAE应用必备的,然而GAE实例代码中并没有很多现成的例子,我瞄到过GAE DataNucleus插件中有些单元测试代码可以参考,因为没有强烈的需求,我还没有深入研究,目前我也只是处于简单的1对1,1对N关系搭起来能用的阶段。