バックアップ、リカバリ、データ移行にバルクローダーを使用する

Bulkloaderは、Googleサーバー上のストレージとの間でデータをダウンロードするためのGoogle App Engineのインターフェースです。 Bulkloaderは、アプリケーションデータのバックアップ/リカバリ/移行に使用すると便利ですが、ドキュメントと使用例は悲惨なほど小さく、複雑なアプリケーションではさまざまな問題やバグに遭遇します。 私自身は、SDKのソースコードを掘り下げて、バグを読んで、仕事のラウンドを書いて、かなり長い間、さまざまな情報源を掘り続けてきました。 そして今、私は詳細な記事の形でいくつかの果物を提示する準備ができています。



記事は非常に大きいので注意してください。



App Engineアプリケーションの作成の詳細については特に説明しません。このトピックは繰り返し取り上げられており、ロシア語を含む海で例を見つけることができます。 ただし、合成例は通常あまり認識されていないため、個人のブログエンジンである「本物の」アプリケーションを検討します。このトピックは誰にでもよく知られ、理解できるものです。 バックアップファイルの形式として、プレーンXMLを選択します。



ドキュメントの改定もここにはありません。



用語


ロシア語を話す確立された用語はないので、「Kind」を「class」または「type」と呼んで、自分自身にある程度の自由を与えました。 エンティティはエンティティのままです。



インポート=デシリアライゼーション=リカバリ。 エクスポート=シリアル化=バックアップ。



準備する


以下では、ローカルUNIX / LinuxマシンでGAE SDKバージョン1.4.1以降を使用します。Windowsでは、すべてがほぼ同じです。 Googleサーバー上のアプリケーションを使用する場合、特定のニュアンスがありますが、公式ドキュメントでそれらについて読むことができます。ここでは、ローカルサーバーのみを使用します。



メインのGAE SDKプログラム(appcfg.py、dev_appserver.py)は、コンソールで実行できる必要があります(たとえば、SDKパスは適切な環境変数に書き込まれます)。



バルクローダーとは


BulkloaderはPythonフレームワークであり、それを使用するには、構成ファイルだけでなくコードも記述する必要があります。 ただし、フレームワークを習得すると、App Engine内のサーバーからデータを保存および復元するための非常に強力なメカニズムが得られます。



ローカルマシンにデータを保存する形式を選択すると、bulkloaderは特定のルール(インポートおよびエクスポート)に従ってデータを変換します。 記事の最後に、bulkloaderの詳細を確認できるリンクがあります。



Bulkloaderが機能するためには、アプリケーションはまずAPIへのアクセスポイントを有効にする必要があります。 したがって、remote_apiを有効にします。これには、アプリケーション構成(app.yaml)でセクションを追加します(存在しない場合)



 ビルトイン:
 -remote_api:オン 


このセクションには、http:// servername / _ah / remote_apiのAPIへのアクセスポイントが含まれます。デフォルト設定のローカルサーバーの場合は、http:// localhost:8080 / _ah / remote_apiになります。



データスキーマ


アプリケーションデータスキーマから始めましょう。 ブログでは、記事(記事)、コメント(ArticleComment)、レンダリングされた記事(RenderedArticle)のすべてが明確です。 コメントはツリーで表示されます。 htmlでレンダリングされた記事は、別​​のストレージエンティティに格納されます。



エンティティクラスは、次のように相互に参照します。



記事→RenderedArticle(記事内のレンダリングされた記事へのリンク)

ArticleComment→Article(コメントからの記事へのリンク)

ArticleComment→ArticleComment(親コメントへのリンク)



class RenderedArticle(db.Model): html_body = db.TextProperty(required=True) class Article(db.Model): shortcut = db.StringProperty(required=True) title = db.StringProperty(required=True) body = db.TextProperty(required=True) html_preview = db.TextProperty() rendered_html = db.ReferenceProperty(RenderedArticle) published = db.DateTimeProperty(auto_now_add=True) updated = db.DateTimeProperty(auto_now_add=True) tags = db.StringListProperty() is_commentable = db.BooleanProperty() is_draft = db.BooleanProperty() class ArticleComment(db.Model): parent_comment = db.SelfReferenceProperty() name = db.StringProperty() email = db.StringProperty() homepage = db.StringProperty() body = db.TextProperty(required=True) html_body = db.TextProperty(required=True) published = db.DateTimeProperty(auto_now_add=True) article = db.ReferenceProperty(Article) ip_address = db.StringProperty() is_approved = db.BooleanProperty(default=False) is_subscribed = db.BooleanProperty(default=False)
      
      







モデルは、2種類のリンク、日付、文字列、ブール値、リストなど、さまざまな種類のデータが使用されていることを示しています。 今後、リストとリンクで最大の問題が発生したことに注意してください。



バルクローダーの確認


データベースに入力し、APIを介してBulkloaderの動作を確認します。



  appcfg.py download_data --email = doestmatter -A wereword --url = http:// localhost:8080 / _ah / remote_api --kind = Article --filename = out.dat 




-Aパラメーターでアプリケーションの名前、-emailパラメーターの任意の行(ローカルサーバーでは関係ありません)、-kindパラメーター-ダウンロードするエンティティクラス(download_data引数を参照)で指定します。 コマンドが実行された後(パスワード要求でEnterを押すだけ)、指定されたエンティティクラス(out.dat)のバックアップとさまざまなログ(bulkloader- *の形式のファイル)を持つファイルが現在のディレクトリに表示されます。 デフォルトでは、バックアップにはSQLITE3形式が使用されます。SQLITE3ビューアーで結果ファイル(out.dat)を開いて学習できます。 その構造は実際の使用(たとえば、移行)にはほとんど役に立たないため、バルクローダーの構成(およびその他の関連ファイル)を書き続けて、データが私たちにとってより便利な形式でエクスポートされるようにします。



bulkloaderの構成ファイルの作成


SDKの現在のバージョンは、CSVとXMLの2つのデータエクスポート/インポート形式をサポートしています。2番目の形式を使用します。 設定ファイルは使い慣れたYAMLファイルであり、ストレージからデータをエクスポート/インポートするときにデータ変換がどのように実行されるかを説明します。 公式ドキュメントには、アプリケーションから基本設定を生成する方法が記載されていますが、最初から作成します。 このファイルをconfig.yamlと呼びます。通常、アプリケーションツリーに別のバックアップディレクトリを作成し、必要なものをすべてそこに配置します。実際にはメインアプリケーションと交差しません。



最初に-python_preambleセクションで-エクスポート/インポートプロセスに必要なPythonモジュールが定義されています。 モジュールの「紳士のセット」を次に示します。base64およびreは標準のPythonモジュール、googleです* SDKのモジュールであり、helpersは現在のディレクトリにある独自のモジュールhelpers.pyファイルです。 helpers.pyには、データをインポート/エクスポートするためのさまざまな回避策やその他の便利な機能がありますが、最初はその名前の空のファイルを作成し、後でコードを追加します。



 python_preamble: - import: base64 - import: re - import: google.appengine.ext.bulkload.transform - import: google.appengine.ext.bulkload.bulkloader_wizard - import: google.appengine.ext.db - import: google.appengine.api.datastore - import: google.appengine.api.users - import: helpers
      
      







構成の次のセクションはトランスフォーマーであり、エンティティの「コンバーター」をローカルバックアップ形式で、またはその逆について説明します。 ここでは、必要なエンティティクラスのすべてのフィールドを記述する必要があります。 各エンティティクラスはkindと呼ばれる個別のセクションで説明されています。ここにそのようなセクションの最も簡単な例があり、Articleクラスのコンバータについて説明します。



 transformers: - kind: Article connector: simplexml #   connector_options: #   xpath_to_nodes: "/blog/Articles/Article" # XPath,          style: element_centric #    XML,    — - property_map: - property: __key__ external_name: key export_transform: transform.key_id_or_name_as_string
      
      





短いメモ、XPathサポートは非​​常に弱く、「/ AAA / BBB / CCC」という形式の式のみを実際に使用できます。



次に、作成したばかりの構成(オプション--config)を使用してサーバーからデータをダウンロードします。



  appcfg.py download_data --email = doestmatter -A wereword --url = http:// localhost:8080 / _ah / remote_api --kind = Article --config = test.yaml --filename = Article.xml 




そして、2つのオブジェクトに関するデータを含む結果のXMLを取得します。



 <?xml version="1.0"?> <blog> <Articles> <Article> <key>6</key> </Article> <Article> <key>8</key> </Article> </Articles> </blog>
      
      







XMLには、transformersセクションの構成ファイルで説明したフィールドのみが含まれることに注意してください。 私たちの場合、これはレコードキーにすぎません。 export_transformパラメーターで、このフィールドに特定のコンバーター、transform.key_id_or_name_as_stringを指定しました。 これはgoogle.appengine.ext.bulkload.transformモジュールの関数です。 別のタイプのフィールドでは、他のコンバーター関数が使用され、Pythonの通常のラムダ式はそのようなコンバーターとして機能できます。



そして今、Articleエンティティークラスを記述する設定全体:



 - kind: Article connector: simplexml connector_options: xpath_to_nodes: "/blog/Articles/Article" style: element_centric property_map: - property: __key__ external_name: key export_transform: transform.key_id_or_name_as_string - property: rendered_html external_name: rendered-html export_transform: transform.key_id_or_name_as_string # deep key! It's required here! import_transform: transform.create_deep_key(('Article', 'key'), ('RenderedArticle', transform.CURRENT_PROPERTY)) - property: shortcut external_name: shortcut - property: body external_name: body - property: title external_name: title - property: html_preview external_name: html-preview - property: published external_name: published export_transform: transform.export_date_time('%Y-%m-%dT%H:%M') import_transform: transform.import_date_time('%Y-%m-%dT%H:%M') - property: updated external_name: updated export_transform: transform.export_date_time('%Y-%m-%dT%H:%M') import_transform: transform.import_date_time('%Y-%m-%dT%H:%M') - property: tags external_name: tags import_transform: "lambda x: x is not None and len(x) > 0 and eval(x) or []" - property: is_commentable external_name: is-commentable import_transform: transform.regexp_bool('^True$') - property: is_draft external_name: is-draft import_transform: transform.regexp_bool('^True$')
      
      







詳細に分析しましょう。 オブジェクトの各フィールドには、このフィールドのデータ変換ルールを記述するプロパティパラメーターが指定されます。



external_nameパラメーターは、XMLファイル内の対応する要素の名前を設定します。



パラメーターimport_transformは、データをインポートするための関数であり、データをバックアップからフィールドの目的のデータ型に変換します。 これは逆シリアル化であると想定できます。



export_transformパラメーターで、フィールドをバックアップに書き込まれるテキストに変換する機能、データのシリアル化。



単純型(たとえば、String)の場合、インポート関数とエクスポート関数の明示的な説明は必要ありません;標準が使用されますが、これで十分です。 他のタイプについては個別に説明します。



render_htmlフィールドから始めましょう。まず、別のクラスのオブジェクト(この場合はRenderedArticle)への参照です。次に、RenderedArticleクラスのこのオブジェクトはArticleクラスの対応するオブジェクトのです。 したがって、逆シリアル化中に、オブジェクトへの有効な参照を「構築」する必要があります。これは、標準のtransform.create_deep_keyメソッドを使用して2つのフィールドの値から行われます。



  - property: rendered_html external_name: rendered-html export_transform: transform.key_id_or_name_as_string # deep key! It's required here! import_transform: transform.create_deep_key(('Article', 'key'), ('RenderedArticle', transform.CURRENT_PROPERTY))
      
      







import / export_transformパラメーターには、1つの引数を取り、1つの値を返す関数につながる式が含まれている必要があることに注意してください。 また、上記の例では、特定の引数を使用した関数呼び出しがあります。この関数は一種のデコレーターであり、データ変換用に既に準備された関数を返します。 transform.create_deep_keyは、入力として複数の2要素タプルを受け入れます。各タプルは、オブジェクトのリレーションのチェーンの1レベルを反映し、タプル自体にはエンティティクラスの名前と(XMLファイルからの)要素の名前が含まれます。 これらのフィールドから、キー値が生成されます。



この場合、チェーンは2つのオブジェクトで構成され、値transform.CURRENT_PROPERTYを使用して、リレーションシップチェーンから現在のオブジェクトのフィールド名を取り除きます。 原則として、transform.CURRENT_PROPERTYの代わりに、rendered_htmlを記述することは完全に可能です。



日付のあるフィールドにも特別なアプローチが必要ですが、ここではすべてが簡単です。SDKの関数ジェネレーターを使用し、引数で日付/時刻の書式設定テンプレートを指定します。



  - property: published external_name: published export_transform: transform.export_date_time('%Y-%m-%dT%H:%M') import_transform: transform.import_date_time('%Y-%m-%dT%H:%M')
      
      







文字列のリストを持つフィールド、ここではシリアル化に標準的な方法が使用されるため、何も書く必要はありませんが、インポートには特別なアプローチが必要です。



  - property: tags external_name: tags import_transform: "lambda x: x is not None and len(x) > 0 and eval(x) or []"
      
      





エクスポート(シリアル化)すると、文字列のリストは次の種類の要素に変換されます。



 <tags>[u'x2', u'another string']</tags>
      
      







ただし、空の文字列リストは空の文字列に変換されます。



 <tags></tags>
      
      





また、標準コンバーターを使用してインポートする場合、空のフィールドはNone値に変換されます。これは明らかに有効なリストではなく、アプリケーションでこのフィールドを読み取ろうとすると問題が発生します。 したがって、正しい(比較的)変換を実行するラムダ式を使用します。 ただし、 SDKのバグが原因で、フィールドタイプバリデーターにエラーがあるためこれはあまり役に立ちません。



ブール値フィールドを使用する場合、逆シリアル化用の単純なコンバーターも使用します。



  - property: is_commentable external_name: is-commentable import_transform: transform.regexp_bool('^True$')
      
      





標準エクスポートでは、ブール値は「True」および「False」文字列に変換されますが、インポート時にはさらに一般的な方法を使用します。「True」文字列のみがTrueに変換され、残りはすべてFalseに変換されます。



Articleクラスのインポートされたオブジェクトを含む結果のXMLファイルは、次のようになります。



 <?xml version="1.0"?> <blog> <Articles> <Article> <body>aaa bbb ccc</body> <updated>2011-01-20T08:19</updated> <key>6</key> <is-draft>False</is-draft> <title>this is new article</title> <html-preview><p>aaa bbb ccc</p></html-preview> <tags></tags> <shortcut>short-cut-1295418565</shortcut> <rendered-html>7</rendered-html> <published>2011-01-19T06:29</published> <is-commentable>True</is-commentable> </Article> <Article> <body>ff gg hh</body> <updated>2011-01-19T06:30</updated> <key>8</key> <is-draft>False</is-draft> <title>another article</title> <html-preview><p>ff gg hh</p></html-preview> <tags>[u'x2']</tags> <shortcut>short-cut-1295418590</shortcut> <rendered-html>9</rendered-html> <published>2011-01-19T06:29</published> <is-commentable>True</is-commentable> </Article> </Articles> </blog>
      
      







オブジェクト関係チェーンの使用


オブジェクト間の関係または依存関係は、エンティティクラスのオブジェクトを作成するときに親引数を使用して構築されます。 新しいオブジェクトは、親で指定されたものと同じエンティティのグループに分類されます。 このアプローチにより、たとえばトランザクションを使用してデータの整合性を維持できます。 インポートおよびエクスポート中の関係のチェーンは、特別な方法で処理する必要があります。 また、以下で考慮するいくつかのニュアンスがあります。



したがって、Articleエンティティクラスがあり、このタイプのオブジェクトは記事であり、マークアップ言語の記事のソースコード、小さなプレビュー、その他の公式情報が含まれています。 また、htmlコードでレンダリングされた記事のテキストは、RenderedArticleクラスの別のオブジェクトに保存されます。 App Engineが採用するオブジェクトの全体サイズの制限を回避するために、レンダリングされたテキストを個別のエンティティクラスに分離しました。実際、ArticleオブジェクトとRenderedArticleオブジェクトは1対1の関係で動作します。 RenderedArticleオブジェクトは、Articleオブジェクトと同じエンティティグループに作成されます。



RenderedArticleエンティティクラスのconfig.yaml configの一部を次に示します



 - kind: RenderedArticle connector: simplexml connector_options: xpath_to_nodes: "/blog/RenderedArticles/Article" style: element_centric property_map: - property: __key__ external_name: key export: - external_name: ParentArticle export_transform: transform.key_id_or_name_as_string_n(0) - external_name: key export_transform: transform.key_id_or_name_as_string_n(1) import_transform: transform.create_deep_key(('Article', 'ParentArticle'), ('RenderedArticle', transform.CURRENT_PROPERTY)) - property: html_body external_name: html-body
      
      







上記の例でデータのエクスポートがどのように記述されているかに注目してください。 まず、オブジェクトの1つのキーフィールドがバックアップ内の2つの要素に変換されます。 次に、インポート時に、キーフィールドはParentArticleとkeyの2つの要素の値から「アセンブル」されます。 transform.key_id_or_name_as_string_n(0)コードは、キーフィールドでの実行の結果として、複合キーの指定されたコンポーネントを返す関数を返します。



この構成に基づいて生成されたXMLは次のようになります。



 <?xml version="1.0"?> <blog> <RenderedArticles> <Article> <ParentArticle>6</ParentArticle> <html-body><p>aaa bbb ccc</p></html-body> <key>7</key> </Article> <Article> <ParentArticle>8</ParentArticle> <html-body><p>ff gg hh</p></html-body> <key>9</key> </Article> </RenderedArticles> </blog>
      
      







ここで、ArticleCommentクラスのオブジェクトのエクスポートとインポートを検討します。コメントはツリーである、つまり、コメントには「親」コメントを含めることができ、さらに各コメントには親投稿へのリンクがあります。



 - kind: ArticleComment connector: simplexml connector_options: xpath_to_nodes: "/blog/Comments/Comment" style: element_centric property_map: - property: __key__ external_name: key export_transform: transform.key_id_or_name_as_string import_transform: transform.create_deep_key(('Article', 'article'), ('ArticleComment', transform.CURRENT_PROPERTY)) - property: parent_comment external_name: parent-comment export_transform: transform.key_id_or_name_as_string import_transform: helpers.create_deep_key(('Article', 'article'), ('ArticleComment', transform.CURRENT_PROPERTY)) - property: article external_name: article export_transform: transform.key_id_or_name_as_string import_transform: transform.create_foreign_key('Article') - property: name external_name: name - property: body external_name: body
      
      







一見、すべてがシンプルに見えますが、ある時点でコンバータの「デフォルト」動作が壊れます。 parent_commentフィールドはNoneであり、トップレベルのコメントを示すことに注意してください。 インポートプロセス中にtransform.create_deep_keyメソッドを使用すると、値Noneでエラーが発生します。



  BadArgumentError:引数4として整数IDまたは文字列名が必要です。 なし(NoneType)を受け取りました。 


このエラーについてもバグを作成しましたが、これまでのところ、開発者からの反応はありません。 このバグを回避するには、transforms.create_deep_keyメソッドの代わりにhelpers.pyファイルを使用します。 回避策は非常に簡単です。値がNoneでない場合にのみキーを生成します。



 def create_deep_key(*path_info): f = transform.create_deep_key(*path_info) def create_deep_key_lambda(value, bulkload_state): if value is None: return None return f(value, bulkload_state) return create_deep_key_lambda
      
      





コメントで、誰かが興味を持っているなら、この関数で何が起こっているのかをもっと伝えることができます。



したがって、オプションでオブジェクトへの参照が正しく復元されます。



現在、記事フィールドを操作しています。このフィールドには、コメントが属する記事へのリンクが含まれています。 オブジェクトへの参照を復元するには、transform.create_foreign_keyメソッドを使用します。transform.create_deep_keyメソッドと同様に機能しますが、リレーションシップチェーンは考慮されません。 ここで、オブジェクトへのリンクが空の場合、潜在的なバグに注意を喚起したいと思います。リカバリ中に、上記のいくつかの段落とまったく同じエラーが発生します。



おわりに


Bulkloaderを使用することはすでに可能ですが、非常に慎重です。 すべての変更が変更ログに含まれるわけではないため、SDKの各リリース後にアナウンスを常に監視し、ドキュメントを注意深く読む必要があります。 また、バイナリデータの操作の概要も残されましたが、ここではすべてが簡単です。



  - property: data external_name: data export_transform: base64.b64encode import_transform: transform.blobproperty_from_base64
      
      







次回は、GAE-python-djangoアプリケーションのローカライズ機能について説明します。



参照資料



All Articles