この記事はGMOペパボ Advent Calendar 2017の13日目の記事です。

最近まで取り組んでいたこととして、10年以上ペパボを支えてきたサービスの一部Webアプリケーションにおいて、PEAR::DBの使用をやめて、PDOの使用に変更するというものがありました。

この記事では、取り組みの動機や、どのようなアプローチを採ったのか、また、そこから得られた知見などを紹介します。おそらく、相応に老舗であるPHPアプリケーションでしか、PEAR::DBと向き合う機会はないと思われますので、万人向けの記事ではないことを予めお断りしておきます。

動機と背景

まずはじめに、PEAR::DBとPDOについて、簡単に触れたうえで、今回の取り組みの動機と背景について整理します。

PEAR::DBとは

PEAR::DBとは、PEARで提供されているデータベース抽象化のためのライブラリです。PEARのサイトの説明にあるとおり、主に以下のような機能を提供します。

  • データアクセスの抽象化
  • OO-StyleのクエリAPI
  • etc…

2002年にバージョン1.2がパッケージとしてはじめてリリースされたようです。2002年といえば、まだPHP4の時代です。もしかしたら若い人は聞いたことすらないかもしれませんね。現時点の最新バージョンは、1.9.2となっていますが、だいぶ前に以下のとおりのステータスとなっています。

This package has been superseded, but is still maintained for bugs and security fixes. Use MDB2 instead.

まだメンテナンスはされていますが、バグ修正とセキュリティフィクス限定とのことですので、新機能の追加や性能向上などは今後行われることはありません。

PDOとは

PDOとは、PHP Data Objectsの略称で、PHPの標準拡張モジュールです。公式サイトの説明にあるとおり、提供する主な機能は以下のとおりです。

PDO は、データアクセスの抽象化レイヤを提供します。 つまり、使用しているデータベースが何であるかにかかわらず、同じ 関数を使用してクエリの発行やデータの取得が行えるということです。

こちらも、PEAR::DBと同じくデータアクセスレイヤの抽象化についての機能を提供しています。

動機と背景

さて、今回の取り組みは上述したPEAR::DBをPDOに置き換えるという取り組みです。その動機と背景については、以下のとおりです。

まず、PEAR::DBを使い続けるデメリットとして以下が挙げられます。

  • 新機能の追加や性能向上などのフィーチャーに取り組まれることが今後ない。
  • どこかのタイミングでメンテナンスが終了する可能性がある。(これは、PEAR::DB特有のものではないが。)
  • MySQLを利用している場合に限る話になるが、PEAR::DBでMySQL APIとしてmysqlを選択している場合、そのままではPHP7系にはできない。(PHP5.5.0で非推奨。PHP7で削除済み。)PEAR::DBが使用するMySQL APIをmysqliに変更する必要がある。

次に、PDOを使うメリットとしては以下が挙げられます。

  • PHPの標準拡張モジュールであるため、メンテナンスについてはあまり心配することがない。
  • 新機能追加や機能向上の恩恵を得られる可能性が高い。
  • PEAR::DBのようにPHPで抽象化の実装が書かれているわけではなく、C言語で実装されているので速い。
  • 当然PHP7にもバンドルされているので、PHP7へのバージョンアップ時にPDOの部分についてはとくに障壁は生まない。

以上のように、PEAR::DBのデメリットを排し、PDOのメリットを享受するというのが動機の主体となります。

PHP7系へのバージョンアップが主目的である場合は、今回のような取り組みは必須ではありません。すでに述べたように、障壁となるのはmysqlというAPIの利用のみなので、これをmysqliの利用に変えれば済みます。ただし、その場合、PEAR::DBの他のデメリットについての課題は依然として残り続けます。

今回の取り組みでは、PHP7へのバージョンアップも当然視野には入れていたとはいえ、PEAR::DBの他のデメリットについても断ち切るためにPDOへのリプレースを選択しました。

アプローチ

この項では、PEAR::DBをPDOにリプレースするにあたってどのようなアプローチを採ったかを説明します。

SQLは変更しない

まず、基本ポリシーとして、SQL文は原則として一切変更しないこととしました。理由としては、すべてのSQL文についてのテストが整備されていたわけでもなかったため、SQL文に変更を加えてしまうとリプレースとは本質的に異なる部分のテストが必要になるからです。(当然といえば当然ですが。)やむを得ず、SQL文に触る必要があったケースについては後述します。

対象の状況

ここで、リプレース対象のPHPアプリケーションについて、その状況をざっと確認しておきます。

  • PHP5.6(PHP4系からのバージョンアップを経ている)
  • データベースにはMySQLを利用
  • PEAR::DBが利用するAPIは、mysql(mysqliではない)
  • Web Application Framework(以下、フレームワーク)は、ZendFramework 1系を参考にして作られた内製フレームワーク
  • フレームワーク利用部分はMVC構成になっている
  • フレームワークを使わない素のPHP部分(非MVC部分)も相当量存在(機能数ベースで1/3~1/2程度)

10年以上生き抜いてきたサービスなので、いろいろな経緯や事情により、上記のような状況となっていました。

MVC部分への対処

MVC部分については、当然ですが基本的にクラスベースのコードになっています。データアクセスまわりに関しては、DAO(Data Access Object)を用いたデザインになっており、以下のように複数のDAOの系統があるような状態でした。

実際のデータアクセスは、Dao_AbstractクラスとDao_Managerクラスによって行われています。Dao_Abstractクラスは、継承ツリーの基点になっており、テーブルごとにサブクラスを定義するという設計になっていました。一方のDao_Managerクラスは、主に2系統あるクラスグループから継承ではなく集約を用いてデータアクセスを委譲されるという設計でした。

既存のデザインを眺めた結果、以下のアプローチを採ることにしました。

  • Dao_Baseというデータアクセスのための基底クラスを新しく用意する。
  • Dao_BaseクラスはデータアクセスにPDOを利用する。
  • Dao_BaseクラスはDao_Abstractクラスのインタフェースと互換する。
  • Dao_BaseクラスはDao_Managerクラスのインタフェースと互換する。
  • Dao_Abstractクラスを継承しているクラス群のスーパークラスをDao_Baseクラスに切り替える。
  • Dao_Managerクラスのインスタンスをコンポジションで保持している箇所を、Dao_Baseクラスのインスタンスを保持するよう切り替える。

理由は、以下のようなメリットがあると考えたからでした。

  • コードの変更箇所が少なく、また変更内容がほぼ同じになるため、作業自体は容易になる。
  • とくに継承ツリーに対しては、スーパークラスを段階的に切り替えて行けるため、影響範囲を最小にしつつ、その影響を見極めながら確実にプロダクションコードにPDOへのリプレースという進捗を与えられる。
  • 想定外の事象が発生して切り戻しが必要になった場合でも、切り戻されるのは基本的にそのときのリリースのみであり、リリース済みで問題のなかったものまで切り戻されたりしない。(進捗が無に帰さない。)

対応後のクラスの関係は以下のようになりました。

「そもそものクラス設計をあるべき形にすべきだ」という見方もあるかもしれませんが、今回の取り組みで解決したい問題とは本質的に異なる問題領域になるため、見送っています。

非MVC部分への対処

MVC部分については、クラスベースであることもあり、ユニットテストで品質を担保しながら、リファクタリングしていくという王道パターンで進めることができました。しかし、非MVC部分ではそのような進め方はできません。ほとんどのコードがクラスベースではなく、HTMLにPHPコードを埋め込んだ形のコードです。とはいえ、PEAR::DBを利用してのデータベース接続を行う部分や、その接続を用いてPEAR::DBのクエリインタフェースを利用する部分などは、グローバル関数として一元化はされている状態でした。

上記のような状態であったため、こちらでは以下のようなアプローチを採ることにしました。

  • PEAR::DBの接続オブジェクトを返していたグローバル関数を変更し、Dao_Baseクラスのインスタンスを返すようにした。
  • Dao_BaseクラスのインタフェースをPEAR::DBの接続オブジェクトのインタフェースにも互換させた。
  • PEAR::DBの接続オブジェクトのメソッドであるqueryが返す結果オブジェクトのインタフェースを互換させるために、Dao_Base_Queryクラスを追加し、Dao_Base::queryメソッドにそのインスタンスを返させるようにした。

以上のようなアプローチにより、アプリケーション全体に散らばっている無数のデータアクセスコードを変更することなく、実際のデータアクセスの実装をすげ替えることができます。また、Dao_BaseクラスやDao_Base_Queryクラスのユニットテストを追加するにあたり、クライアントコードをそのままテストケースとしてテストコード化することにより、自然と品質保証の精度が高いテストを整備することが出来ました。

知見

この項では、今回の取り組みの中で得られたいくつかの知見について紹介します。

シリアライズ

PDOインスタンスは、そのままではシリアライズできません。何も考慮せずに、PDOインスタンスを含むオブジェクトをシリアライズしようとすると、以下のような例外が発生します。

‘PDOException’ with message ‘You cannot serialize or unserialize PDO instances’

シリアライズできるようにするためには、PDOインスタンスを含むオブジェクトにSerializableインタフェースを実装するか、__sleep__wakeupマジックメソッドを実装する必要があります。PHPマニュアルでも説明されています。

PEAR::DBのリファレンスを保持するオブジェクトの場合、この問題は発生しないため、PEAR::DBをPDOにリプレースした結果、はじめてこの問題が顕在化するケースがあり得ますので注意が必要です。

固有プレースホルダ

PEAR::DBには、プリペアドステートメントのプレースホルダとして、固有なものが存在します。

https://pear.php.net/package/DB/docs/1.9.2/DB/DB_common.html#methodprepare

  • ? scalar value (i.e. strings, integers). The system will automatically quote and escape the data.
  • ! value is inserted ‘as is’
  • & requires a file name. The file’s contents get inserted into the query (i.e. saving binary data in a db)

?は、PDOのプレースホルダとして使用可能ですが、その他は不可であるため注意が必要です。リプレース対象のコードにそれらが含まれている場合は、なんらかの対処を行わないと、意図したSQLが構築されず、エラーが発生することになります。

派生して得られたその他のバリュー

最後に、今回の取り組みで副産物的に得られたバリューについて、まとめて簡単に紹介します。

各所に散らばったデータアクセスを一元化できたため、今後のメンテナンスコストを下げられます。データアクセスについての変更が必要になった際に、あちらこちらを気にする必要がなくなりました。併せて、DAOについてはユニットテストを整備したため、本来あるべきデータアクセスのカプセル化と品質担保の実現ができており、変更に強くなっています。アプリケーション全体を洗う機会にもなったため、発見したデッドコードや不要コードなども積極的に削除することができました。こちらもコードのメンテナンスコスト削減に寄与するでしょう。(不要コードについての一考察については、昨年のエントリにもあります。)さらに、PEAR::DB経由ではなく、直接mysql APIの関数(mysql_*系の関数)を利用している箇所も存在したため、こちらもDAO経由でPDOを利用するようにリファクタリングしました。

まとめ

今回の取り組みについて振り返ると、結果としてユーザーに迷惑を掛けるような問題を引き起こさなかったという点が、まず第一に評価できる点と考えています。今回のような取り組みは、機能追加のような直接的で分かりやすいバリューをユーザーに届けるものではありませんが、開発スピードの向上やセキュリティリスクの低下、パフォーマンスの向上、プロダクトの品質向上など、間接的ですがボトムアップ的にサービスにバリューを与え、支えていく力になることは明白です。その反面、取り組みにおいて例えば障害などのユーザー影響を発生させてしまうと、ユーザー目線では当然バリューなどは影も形も見えず、そこにはただマイナスな事象があるのみになってしまいます。そうした中で、それこそユーザーに気づかれることなく取り組みの全工程を完遂できたことは、本来の意味でリファクタリングが成功したのだと言えるかもしれません。

冒頭に述べたとおり、PEAR::DBという話題の時点で、新しいPHPアプリケーションにはほぼ関係のない話になりますが、それでも老舗のPHPアプリケーションを運用している現場では、同じような状況は意外とあるのではなかろうかと考えています。そして、そういう現場において、この記事のどこか一部分でも参考になれば良いなと思った今年の年末でした。