🔥

laravel-doctrineを使ってレガシーなoracleデータベースに立ち向う

7 min read

モチベーション

FWの力を借りてちょっとした社内ツールをささっと作りたかったのですが既存データベースには下記のような問題がありました。

  • テーブル名、カラム名が日本語なのでFWの規約に則っていない
  • モデリングが破綻していてテーブル構成が超複雑
  • そもそもがoracleなので主要なORMが対応していない

laravel-doctrineというライブラリを使うことで上記の問題があってもlaravelを使って無事ツールを作ることが出来たので紹介しようと思います。

laravel及びdoctrineについての解説はしません

laravel-doctrineとは

Active Recorde patternと対を成すData Mapper patternによる実装のORMであるdoctrineをlaravelでも利用できるようにするものです。laravel8.*にも対応しています。

http://laraveldoctrine.org/

今回は扱いませんでしたがmigrationにも対応しているようです。

laravel-doctrineのインストール

laravelプロジェクトにインストールします。

composer require "laravel-doctrine/orm"

laravel-doctrineの設定

config/app.phpにサービスプロバイダを追記します。

// After updating composer, add the ServiceProvider to the providers array in config/app.php

LaravelDoctrine\ORM\DoctrineServiceProvider::class,

必要であればconfig/app.phpにファサードを登録します。

// Optionally you can register the EntityManager, Registry and/or Doctrine facade:

'EntityManager' => LaravelDoctrine\ORM\Facades\EntityManager::class,
'Registry'      => LaravelDoctrine\ORM\Facades\Registry::class,
'Doctrine'      => LaravelDoctrine\ORM\Facades\Doctrine::class,

以下のコマンドでconfig/doctrine.phpを生成します。

php artisan vendor:publish --tag="config"

oracleの日付型の設定をしておく

データベースにoracleを使う場合はconfig/doctrine.phpを編集してDoctrine\DBAL\Event\Listeners\OracleSessionInitを追加します。

/*
|--------------------------------------------------------------------------
| Doctrine events
|--------------------------------------------------------------------------
|
| The listener array expects the key to be a Doctrine event
| e.g. Doctrine\ORM\Events::onFlush
|
*/
'events'        => [
    'listeners'   => [],
    'subscribers' => ['Doctrine\DBAL\Event\Listeners\OracleSessionInit'] 
],

これが無いと日付を扱う際に以下のエラーが出ます。

ORA-01861: リテラルが書式文字列と一致しません

yajiraのインストール

laravelでoci8を使えるようにするためのライブラリを入れます。

composer require yajra/laravel-oci8

config/app.phpに追記

// After updating composer, add the ServiceProvider to the providers array in config/app.php
'providers' => [
    // ...
    Yajra\Oci8\Oci8ServiceProvider::class,
],

config/oracle.phpを生成

php artisan vendor:publish --tag=oracle

書きながら気がついたのですがlaravel-doctrineを使う場合はYajraを使わなくても良い気がします(未検証)。

config/oracle.phpについて

oracle.phpの中身は以下のようになっており、.envから設定値を取得する仕組みの為、得に編集する必要はありませんが、tnsnameでデータベースを指定する場合は注意が必要です。

<?php

return [
    'oracle' => [
        'driver'         => 'oracle',
        'tns'            => env('DB_TNS', ''), // ここに設定された値はdoctrineでは読み込まれない
        'host'           => env('DB_HOST', ''),
        'port'           => env('DB_PORT', '1521'),
        'database'       => env('DB_DATABASE', ''), // tnsnameを使用する場合はこちらに設定する
        //以下略
    ],
];

tnsの設定を何度見直しても接続できず試行錯誤した結果判明したことなので何故そうなってしまうのかは不明です。

既存のDDLからmapping定義を生成する

既存のデータベースのマッピング定義を手作業で作成するのは超苦痛な作業なのでリバースエンジニアリングをしていきます。laravel-doctrineにはそういった機能は無いのでdoctrineの機能を直で使っていきます。
まずはcli-config.phpを作成してドキュメントルートに配置します。

<?php
// cli-config.php
use Doctrine\ORM\Tools\Console\ConsoleRunner;
use Doctrine\ORM\Tools\Setup;
use Doctrine\ORM\EntityManager;

require_once "vendor/autoload.php";

// Create a simple "default" Doctrine ORM configuration for Annotations
$isDevMode = true;
$proxyDir = null;
$cache = null;
$useSimpleAnnotationReader = false;
$config = Setup::createAnnotationMetadataConfiguration(array(__DIR__ . "/app/Db/"), $isDevMode, $proxyDir, $cache, $useSimpleAnnotationReader);

$conn = array(
    'driver' => 'oci8', // oracleなのでoci8を設定
    'user' => "<user>",
    'password' => "<password>",
    'dbname' => "<tnsname>", // tnsnames.oraがあればtnsnameを入れるだけでOK
    'charset' => 'AL32UTF8',
);

// obtaining the entity manager
$entityManager = EntityManager::create($conn, $config);
$config->setFilterSchemaAssetsExpression('/ホゲマスタ/'); //ここに正規表現で生成したいテーブルを指定できます

return ConsoleRunner::createHelperSet($entityManager);

setFilterSchemaAssetsExpression()メソッドは現在deprecatedになっているのでdoctrine v3からは使えなくなります。

以下のコマンドを実行することで/app配下にmapping定義[1]が生成されます。

php ./vendor/bin/doctrine orm:convert-mapping --from-database annotation --namespace='Db\Entities' ./app

mapping定義からentityを生成する

mapping定義のままでは使えないので以下のコマンドでgetterとsetterを生やしentityを作成します。

php ./vendor/bin/doctrine orm:generate-entities ./app/ --generate-annotations=true --no-backup --filter="ホゲマスタ"

カスタムリポジトリを作成する

doctrineは上手く使えばカスケードなどの便利な機能で複数テーブルから成るドメインを簡単に操作することが出来るのですが、ちゃんとモデリングが考えられたDBでないとその技が使えないのでこういったケースでは生クエリを書くことが多くなると思います。そこで本来はビジネスロジックの記述場所となるカスタムリポジトリを生成し、生クエリの置き場所として利用しました。ついでにクラス名が日本語なのが気持ち悪いので適切な名前にします。

以下のようにアノテーションにrepositoryClassを設定します。

<?php

namespace Db\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
* hogehogetable
*
* @ORM\Table(name="ホゲマスタ")
* @ORM\Entity(repositoryClass="Db\Repositories\Hoge")
*/
class ホゲマスタ
{

アノテーションの記述を行った上で以下のコマンドを実行することでカスタムリポジトリ[2]が生成されます。

php ./vendor/bin/doctrine orm:generate-repositories ./app/ --filter="ホゲマスタ"

DBに対して行いたい処理はこの中にメソッドを定義して記述していくことになります。

ハマりポイント

カラム名に日本語が含まれているとクエリビルダが使えない

諦めて生クエリを書きました。

カラム名が長い時oracleではエラーになる

doctrine v3から修正されるそうです。諦めて生クエリを書きました。

https://github.com/doctrine/orm/issues/4610

生成したmapping定義に設定されるsequenceがおかしい

おそらくですが、既存のテーブルからmapping定義を生成する際にDDLだけでなくレコードの中身を確認しているようです。しかしカラム名が日本語だからかアノテーションに以下のようなおかしな文字が含まれてしまいエラーが出るのでentityを生成したら確認しておきましょう。

/**
 * @var string
 *
 * @ORM\Column(name="ホゲコード", type="string", length=4, nullable=false, options={"comment"="ホゲコード"})
 * @ORM\Id
 * @ORM\GeneratedValue(strategy="SEQUENCE")
 * @ORM\SequenceGenerator(sequenceName="ホゲマスタ_ホゲコー?", allocationSize=1, initialValue=1)
 */
private $ホゲコード;

以下のように修正してかわします。

/**
 * @var string
 *
 * @ORM\Column(name="ホゲコード", type="string", length=4, nullable=false, options={"comment"="ホゲコード"})
 * @ORM\Id
 * @ORM\GeneratedValue(strategy="NONE")
 */
private $ホゲコード;

複数のDB接続を使い分けたい

ドキュメントにはDoctrine\Common\Persistence\ManagerRegistryを使えと書いてあるのですが情報が古いです。

http://www.laraveldoctrine.org/docs/1.4/orm/multiple-connections

Doctrine\Persistence\ManagerRegistryを使います。

use Doctrine\Persistence\ManagerRegistry;

class ExampleController extends Controller
{
    protected $em;

    public function __construct(ManagerRegistry $em)
    {
        $this->em = $em->getManager('otherConnection');
    }
}

まとめ

いろいろとハマりましたが最終的には無事、作りたかった社内ツールを完成させることができました。
結局生クエリめっちゃ書いてるやんって話なのですが、それでもフレームワークの力を使ってある程度は楽に開発が出来ましたし、Data Mapper patternによって綺麗にレイヤ分けすることが出来ました。

かなりニッチな内容でしたが、誰かの役に立てば幸いです。

脚注
  1. この例だとapp/Db/Entities/ホゲマスタ.phpが生成されます。 ↩︎

  2. この例だとapp/Db/Repositories/Hoge.phpが生成されます。 ↩︎

Discussion

ログインするとコメントできます