読者です 読者をやめる 読者になる 読者になる

元フリーエンジニアライフ

Ruby on Rails とか MovableType とかAWSやってるフリーランスウェブエンジニアの記録でした。現在は法人成りしてIT社長。

MT::Objectでは精度仕様・桁数を指定したfloatは使えない

Locationで位置情報を保存後、保存直後に再構築したページでは正しい位置を指すが、再度編集画面を開くと、指定した位置がズレている、という不具合が見つかりました。

調べてみると、

8122 Geary Boulevard, San Francisco, CA 94121

Latitude 37.77938700000001 Longitude -122.506663

が保存後

Latitude 37.7794 Longitude -122.507

になっていました。

Floatの精度仕様と桁数

このような場合、DDLで必要な精度仕様と桁数を指定します。

float(桁数,小数点以下の桁数)

ググってると緯度、経度を扱う場合小数点以下6桁ぐらいあればよいようなので、

float(9,6)

というようになりますね。

MTの型指定ではどうやるの

MTでは独自オブジェクトやプラグインで追加するカラムの指定は、このように行います。

YAML

object_types:
    modeltype:
        colname: 型タイプ オプション

Perl

__PACKAGE__->install_properties ({
    column_defs => {
        'colname' => '型タイプ オプション',

参考:

文字列の場合長さを指定できるようになっており、このように指定します。

hoge: string(100)

この方式に従うと、floatではこうでしょうか?

lat: float(9,6)

float(9,6) で指定できそうでできない

試してみましたが、だめでした。普通に指定なしで作られました。

ソースを追ってみると

  1. カラム追加DDLを組み立てるために、MT::ObjectDriver::DDL->add_column_sql
  2. float(桁数,小数点以下の桁数)のようなSQLを組み立てるために、MT::ObjectDriver::DDL->column_sql
  3. モデルのカラム定義を取得するためにMT::Object->column_def
  4. MT::Object->__parse_defstring(100) not nullのような文字列から{'type' => 'string', 'size' => 1, 'not_null' => 1}のような連想配列のリファレンスを作る
  5. 連想配列のリファレンスをMT::ObjectDriver::DDL->type2dbに渡して、varchar(100)のような型指定を得る(MySQLの場合はサブクラス MT::ObjectDriver::DDL::mysqltype2dbが呼ばれます。)

といった流れになっています。

float(9,6)で指定できないのは、

  1. __parse_def(9,6)が無視される
  2. MT::ObjectDriver::DDL->type2dbfloat固定で桁数指定などを行わないことになっている

という2つの仕様によるもののようです。

float(9,6) で指定できるようにするには

以下のようにMT_DIR/lib以下の各ファイルを変更することで対応できました。

  1. __parse_def{'type' => 'float', 'precision' => 10, 'scale' => 6}のように桁数と精度を連想配列に入れるようにする

     if ( $def =~ s/\((\d+),(\d+)\)// ) {
         $def{precision} = $1;
         $def{scale} = $2;
     }
    
  2. MT::ObjectDriver::DDL::mysql->type2db連想配列に精度が含まれるときはfloat(桁数,小数点以下の桁数)のような型指定を返すようにする。

     elsif ( $type eq 'float' ) {
         if ( $def->{precision} && $def->{scale} ) {
             return 'float(' . $def->{precision} . ',' . $def->{scale} . ')';
         } else {
             return 'float';
         }
     }
    

    また、DBに作成済みのカラム情報をパースするため、MT::ObjectDriver::DDL::mysql->column_defsにも少し追加。

     my ($precision, $scale) = ( $coltype =~ m/float\((\d+),(\d+)\)/i ? ( $1, $2 ) : ( undef, undef ) );
     $coltype = $ddl->db2type($coltype);
    
     if ( ( $coltype eq 'float' ) && $precision && $scale ) {
         $defs->{$colname}{precision} = $precision;
         $defs->{$colname}{scale} = $scale;
     }
    

その他、MySQL以外への対応が必要ならそれぞれ用のサブクラスへの変更が必要でしょう。

コアの書き換えが必要なんて現実的ではない

ので、結局文字列型にして範囲計算などは暗黙の型キャストを期待することにしました。ちょっとモヤモヤ。

object_types:
    entry:
        lat: string(20) indexed
        lng: string(20) indexed