【Android/Java】ユーザーデータの後方互換性を保つ

注意

Androidでのjava.io.Serializableを使ったサーバーを介さない機密性のないユーザーデータの保持に関する記事です。記事作者の主観が多分に含まれています。

広告

前提

こんな人向け

  • 開発環境はAndroid Studio、開発言語はJava。
  • Androidアプリの開発言語でしかJavaに触れたことがない。
  • Serializableインターフェースを使ってファイルなどにオブジェクトを保存できる。
  • クラス内容を変更した後で過去のデータをデシリアライズするとデータ消えるんですけど!

調べたサイト集(知らなかったことが割とあったので見ておいたほうがいいかも)

このリストは後に追加される可能性があります。

筆者環境

  • Android Studio
  • Java 1.8

前方互換性については考慮しない。サーバーサイドが絡むなら仕様によっては考えなければならない(素人の戯言ではあるが私だったら例外を投げる)が、サーバーなどアプリ外部にシリアライズしたデータを渡さず、アプリ内でデータの動きが完結している場合はユーザーがアプリをダウングレードでもしない限りありえない(はず)ので、その場合に限り前方互換性は無くていいのではないかと考えた。

新しく追加したフィールドをデシリアライズ時に取得できなければ代替値を入れたかっただけなのに

Android StudioではSerializableインターフェースを継承しても何も起こらないが、EclipseというIDEでは「serialVersionUIDを定義しろ」と警告される。

serialVersionUIDはlong型のstatic finalな定数で、デシリアライズ時にこの値を比較して異なればInvalidClassExceptionという例外を投げデシリアライズを終了する。

serialVersionUIDを定義しなかった場合、クラスの構造を基にこの値が計算される。そのため、Serializableを継承しただけでは、クラス内容を変更するとデシリアライズが例外を投げて終了するため、更新前のデータが受け継がれない。

デシリアライズ時にserialVersionUIDが異なっていると、同じクラスではないと判断してデシリアライズ処理を破棄してしまう。serialVersionUIDが同じなら同じクラスだと判断して最後まで処理してくれる。こちらはフィールドを増やしたりするだけなら読み込んでも初期化はされないし、writeObjectメソッドやreadObjectメソッドを変更するといった「互換性のない変更」をしない限り、シリアライズは最後まで完了します。

ところが全く変更する気のないserialVersionUIDを定義したとしても、後方互換性のためにreadObjectメソッドを編集すると「互換性のない変更」となってしまう。どちらにしろ追加したデータを読めないならバグの種にしかならないので意味がない。というか、読み込めなかった場合の代替値がセットできればいいというだけなんだけど。

互換性のない変更」をするとInvalidClassExceptionが投げられる。反対に「互換性のある変更」もあるが、詳細はこちらの記事公式ドキュメントで解説されているので一度見ておいたほうがいいかもしれない。(丸投げ)

とある記事では基本的にserialVersionUIDを定義しない方が良いと言ったり、また別の記事では定義した方が良いとなっていたり。ケースバイケースと言えばそれまでなんだけど……何も分かってない状態だと混乱の原因でしかない。だって具体的な方法とか書いてないし……。

つまり、ユーザーデータを保持するクラスを定義して、その内容を変更するアップデートをしてしまうと、InvalidClassExceptionが投げられてデータを復元できない。つまりアップデートによってデータが飛んでしまうわけで。そうなると当然ユーザーは怒るよね? 俺だって怒る。「二度と使うかこんなもの」って。開発者がユーザーのデータを消しにかかるようなものだし。

OutputとかInputとかあるStreamを継承したクラスを定義すれば後方互換性を持つことも可能らしいが、具体的な方法についての情報は何も分からなかった。(無能)

で、結局どうすればいいの?

別のクラスを作る

どうしてもSerializableを使う必要があるとか、保存するデータがセーブデータなど比較的少数(1つから5つほど)のものである場合は、別のクラスを作成して自分でデータを移行する処理を書く、という手もある。自分でも強引だなとは思う。

前のクラスのデータが存在すれば、今のクラスにデータを移行する処理を自分で書く。packageとか増やせば同名のクラスが存在できるし、package名に気を付けて運用する必要があるけどバージョンに応じたクラス名を考えなくていいし、ただ一つに定まったクラス名で運用できるメリットがある。

ただし、言い換えればそれはユーザーデータの保持するクラスを変更しなければならない度にクラスが増え、ユーザーがアプリを更新しない限りそれらすべてのクラスをサポートする必要があるということ。まあ、それほど頻繁にユーザーデータ関連を変更、あるいは更新するようなことはない、よね?

原則何かしらの変更は追加によって行う使わないものは非推奨にするというルールであれば特に問題なくデータの受け渡しができそう。

ただ何度も言うが記事冒頭でも書いた通り、この方法はネットを介して配布するようなデータには向いていないと思う。なぜなら、アップデート前のプログラムアップデート後のデータが渡される可能性があるからだ。アプリのバージョンを調べて強制アップデートさせるのも手だが、どちらにしろネットに繋ぐのならデータベースを使うほうがいいはず。

この情報は未検証で不正確なので、もし必要な時が来てそこで検証まで終えたら情報を更新するつもりです

SQLiteを使う

例えるならブックマークやアプリ内で使うテキスト・写真などのメディアのような明確にリストする必要があるデータ、ユーザーの意思によって増え続ける可能性があるデータの保持はSQLiteを使う、という手もある。目安としては100件以上。

SQLite含め大抵のDBは何かしらのDBそのものの変更に対するMigrationがサポートされているようなので、規模の大きいデータを扱う場合や度々扱うデータに変更がある場合でも後方互換性を失うことはなさそう。

SQLiteをそのまま扱う場合、SQLを扱ったことがないとSerializableを意地でも使う方法と比べて学習然り諸々のコストは高くなる

公式ドキュメントではRoomライブラリが推奨されているので、特に理由がない限りそちらを使うといいと思う。

まとめ

「なんでどこにも情報が無いんだよこれぇ!」と思いながら情報収集しました。

というか調べていると度々言われているのが、基本的に後方互換性を持たせるのはあまり良しとされないんですよね。セキュリティがどうのと。それは分かるんだけども、でも後方互換性を持たせなければデータに変更があった場合、ユーザーのデータごと切り捨てなければならないということになるし、じゃあ推奨されないのは分かったから代替手段はなんなのかとくれば、そんなものはどこにも書いてない、と。頭おかしなるで

一番良いのは扱うデータに変更がないことらしいです。そういうことじゃなくてさぁ……

……後方互換性を持たせなければならないデータの実装って、実際みんなはどうしてるんだろうか。もしかしたら、ユーザーデータは大抵DBで扱うからそういうの気にしないとかそういうことなんだろうか。


私が思いつく限りではこれが限界でした。ので、

他にいい方法があればコメントにて教えてください。

参考記事

コメント

タイトルとURLをコピーしました