えいのうにっき

a-knowの日記です

blobstoreにデータオブジェクトを、アプリケーションから書きこむ。ついでに圧縮も。

なによりもまずは、

新年あけましておめでとうございます。 今年もどうぞよろしくお願いします。

CDiT for Web(on GAE) の作り直しを

現在やっておりまして。作り直しに伴う機能増強・問題解決はもちろん、今まではJDOで行なっていたデータ周りの処理も今回を契機にSlim3を適用させようと考えておりまして、全体では結構な大幅改修となりそうではあるんですが。

ぼくのアプリで抱えていた、解決すべき問題のひとつに、扱うデータが少し大きめなことがありました。iTunesでの再生回数(全曲!)が記録された情報になるんですが、「datastoreには1MB以上のサイズのデータは登録できない」という制約にちょいちょい引っかかってまして。 で、今までは「登録対象データにzip圧縮を掛け」たり、「登録対象データを1MB未満のデータに分割し、それぞれを登録」したりしてました。

一方で、datastoreには登録が難しい、巨大なデータの登録向けのblobstoreなんですが、軽く見たかんじ、webページからのリクエスト(アップロード)という形でしかデータの登録ができなさそうだったので、「アプリの中で発生する巨大なデータを登録する」用途には使えないかな〜と見てたのですが、あるタイミングで、Googleの公式ドキュメントに『Writing Files to the Blobstore (Experimental)』という記述があるのを見つけまして。

App Engine allows you to programmatically create blobstore blobs, providing a file-like API that you can use to read and write to blobs. Some common uses of this functionality include exporting data and generating reports or any function that involves generating large binary data objects. You begin by creating a new (empty) blobstore file using the createNewBlobFile() method. This method creates a writable blobstore file that you can open using the Files API's open() method. This is a low-level API. You can use the high-level mapreduce library to create blobstore files based on datastore data. The following sample shows how to create a new blobstore file and manipulate it using the File API:

  // Get a file service
  FileService fileService = FileServiceFactory.getFileService();

  // Create a new Blob file with mime-type "text/plain"
  AppEngineFile file = fileService.createNewBlobFile("text/plain");

  // Open a channel to write to it
  boolean lock = false;
  FileWriteChannel writeChannel = fileService.openWriteChannel(file, lock);

  // Different standard Java ways of writing to the channel
  // are possible. Here we use a PrintWriter:
  PrintWriter out = new PrintWriter(Channels.newWriter(writeChannel, "UTF8"));
  out.println("The woods are lovely dark and deep.");
  out.println("But I have promises to keep.");

  // Close without finalizing and save the file path for writing later
  out.close();
  String path = file.getFullPath();

  // Write more to the file in a separate request:
  file = new AppEngineFile(path);

  // This time lock because we intend to finalize
  lock = true;
  writeChannel = fileService.openWriteChannel(file, lock);

  // This time we write to the channel using standard Java
  writeChannel.write(ByteBuffer.wrap
            ("And miles to go before I sleep.".getBytes()));

  // Now finalize
  writeChannel.closeFinally();

  // Later, read from the file using the file API
  lock = false; // Let other people read at the same time
  FileReadChannel readChannel = fileService.openReadChannel(file, false);

  // Again, different standard Java ways of reading from the channel.
  BufferedReader reader =
          new BufferedReader(Channels.newReader(readChannel, "UTF8"));  
       String line = reader.readLine();
  // line = "The woods are lovely dark and deep."

  readChannel.close();

  // Now read from the file using the Blobstore API
  BlobKey blobKey = fileService.getBlobKey(file);
  BlobstoreService blobStoreService = BlobstoreServiceFactory.getBlobstoreService();
  String segment = new String(blobStoreService.fetchData(blobKey, 30, 40));

こちらを見るかぎりでは、blobstore上にフツーのファイルを生成できちゃいそうな感じですよね。Experimentalっていう但し書きが気になりますが。。。 ぼくのアプリケーション内で巨大になるのは、再生回数情報を記録したArrayListとHashMap。Javaのオブジェクトはシリアライズすることでファイルに書き出せるので、それと同じ要領でやればできるかなーと思い、やってみたらローカルテスト環境では出来たっぽいので、ここにもそれをメモしておこうと思います(今までの名残から、圧縮も掛けてます)。なんかまずいところとか考慮漏れなどがあれば、ぜひご指摘下さい!(ぼくがやろうとしていることを他の方でされている方がいないかどうか、軽く調べてみたのですが、見つかりませんでした。。) production環境下ではまだ試していないので、そのあたりで予期せぬ現象がおきるかもしれません。

まず、blobstoreへの書き出し部分。

    public static BlobKey registBlob(Object o) throws IOException{

        //データをバイト配列に変換(圧縮も実施)
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ZipOutputStream zip_os = new ZipOutputStream(baos);

        zip_os.putNextEntry(new ZipEntry("zipped_entry"));

        ObjectOutputStream oos = new ObjectOutputStream(zip_os);
        oos.reset();
        oos.writeObject(o);
        oos.flush();
        zip_os.closeEntry();
        zip_os.close();
        baos.close();

        byte[] bytes = baos.toByteArray();

        // Get a file service
        FileService fileService = FileServiceFactory.getFileService();

        // Create a new Blob file with mime-type "application/octet-stream"
        AppEngineFile file = fileService.createNewBlobFile("application/octet-stream");

        // Open a channel to write to it
        boolean lock = true;
        FileWriteChannel writeChannel = fileService.openWriteChannel(file, lock);

        // This time we write to the channel using standard Java
        writeChannel.write(ByteBuffer.wrap(bytes));

        // Now finalize
        writeChannel.closeFinally();

        return fileService.getBlobKey(file);
    }

一部、公式ドキュメントのサンプルをそのまま使ってますので、コメントもそのまま残っちゃってます。 mime-typeを"application/octet-stream"にするのがポイントでしょうか。これにより、渡されたオブジェクトをシリアライズしてzip圧縮を掛けたものを、blobstoreにバイナリデータファイルとして書き出せています。と思います。 fileService.getBlobKey(file); で書きだしたファイルを一意に識別するキーが得られるので(まぁファイル名みたいなもんですよね)、これを別エンティティの1プロパティとして持っておけば、それを元に読み出せばいいだけ、だと思ってます。

それではその読み出し部分ですが、以下になります。ぼくがやってみた動作確認の関係で、テストコードになっちゃってますが。

    @Test
    public void registBlobtest() throws Exception {
        ArrayList<String> list = new ArrayList<>();
        HashMap<String, String> map = new HashMap<>();

        //setup
        list.add("1");
        list.add("2");
        list.add("3");

        map.put("A", "value1");
        map.put("B", "value2");
        map.put("C", "value3");


        //regist
        BlobKey list_blobkey = UtilityMethods.registBlob(list);
        BlobKey map_blobkey = UtilityMethods.registBlob(map);


        //read_list
        InputStream is = new BlobstoreInputStream(list_blobkey);

        ObjectInputStream ois = null;
        ArrayList<String> readListData = null;

        ZipInputStream zip_in = new ZipInputStream(is);
        zip_in.getNextEntry();
        ois = new ObjectInputStream(zip_in);

        readListData = (ArrayList<String>) ois.readObject();

        //check
        assertThat(readListData, is(notNullValue()));
        assertEquals("1", readListData.get(0));
        assertEquals("2", readListData.get(1));
        assertEquals("3", readListData.get(2));


        //read_map
        is = new BlobstoreInputStream(map_blobkey);

        ois = null;
        HashMap<String, String> readMapData = null;

        zip_in = new ZipInputStream(is);
        zip_in.getNextEntry();
        ois = new ObjectInputStream(zip_in);

        readMapData = (HashMap<String, String>) ois.readObject();

        //check
        assertThat(readMapData, is(notNullValue()));
        assertEquals("value1", readMapData.get("A"));
        assertEquals("value2", readMapData.get("B"));
        assertEquals("value3", readMapData.get("C"));
    }

2つのオブジェクトを一緒くたに読みだしてみようとしています。汚くてすみません。ですが、一応テストは通っています。

blobstoreからの読み出しについては、以下を参考にさせて頂きました。

今年もよろしくお願いします。