KEIS BLOGは株式会社ケイズ・ソフトウェアが運営しています。

KEIS BLOG

Introduction to Antlr! Pt.5


shimoda01
こんにちは、下田です。頑なに焼き肉に行っているのですが、行く度に経験値が上がるので、自分の中でココは良かったココは微妙だったと言った判断が出来るようになってきました。焼肉好きで良く行っている割に焼肉なんて大して変わらないと思っていたんですけどね。この写真は先日、群馬へ遠征して焼肉食べたときの写真です。最初、この皿が来たとき牛脂サービスしてくれたのかと思いましたが、よく見たら肉だったという残念な目に合いました。ちなみにプライスレンジが「うしごろ」と同じなのに肉、サービス共に「うしごろ」の足元も及ばないこともあり、いま見ても怒りが沸々と湧いてきます。

はい、話がそれました。今回も前回と同様に Antlr の話を進めたいと思います。ちなみに今回が Antlr シリーズ最終回です。

では、まず前回の状態から、 Java のクラスをコピーして用意しましょう。前回はこんな感じで終わっていたと思います。

package a;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;

import a.antlr.MyMySQLLexer;
import a.antlr.MyMySQLParser;
import a.antlr.MyMySQLParser.Create_tableContext;
import a.b.MyAntlrColumn;

public class Main4 {
    public static void main(String[] args) {
        try {
            List<String> readAllLines = Files.readAllLines(Paths.get("src/test/resources/test1.txt"));
            String sql = readAllLines.get(0);
            MyMySQLLexer lexer = new MyMySQLLexer(new ANTLRInputStream(sql.toLowerCase()));
            CommonTokenStream tokenStream = new CommonTokenStream(lexer);
            MyMySQLParser parser = new MyMySQLParser(tokenStream);
            Create_tableContext tableContext = parser.create_table();
            for (MyAntlrColumn c : tableContext.columns) {
                if (c != null) {
                    System.out.println(c);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

そして、私の別の記事ですが「JavaPoetで簡単コード生成!」を利用して自動的に Pojo (MVC で言う Model) を生成したいと思います。
まずは gradle の依存関係に JavaPoet を追加します。

compile 'com.squareup:javapoet:1.3.0'

そして、前回の Antlr の場合、 create_table は カラム定義の List を返す状態となっており、テーブル名が分からなくなってしまうため、 テーブル定義を受け取るクラスと、それを生成する create_table の定義に直します。

package a.b;

import java.util.List;

public class MyAntlrTable {
    private String tableName;
    private List<a.b.MyAntlrColumn> columns;

    public MyAntlrTable(String tableName, List<MyAntlrColumn> columns) {
        super();
        this.tableName = tableName;
        this.columns = columns;
    }

    public String getTableName() {
        return tableName;
    }

    public void setTableName(String tableName) {
        this.tableName = tableName;
    }

    public List<a.b.MyAntlrColumn> getColumns() {
        return columns;
    }

    public void setColumns(List<a.b.MyAntlrColumn> columns) {
        this.columns = columns;
    }

    @Override
    public String toString() {
        return "MyAntlrTable [tableName=" + tableName + ", columns=" + columns + "]";
    }
}

このクラスは単純に、テーブル名と今まで受け取っていたカラム定義の List を受け取るだけのクラスです。以下の create_table 定義で、そのクラスを利用してテーブル名とカラム定義を返すように変更します。

create_table returns[a.b.MyAntlrTable tableDef]:
  CREATE TABLE tn=tbl_name LPAREN cd=create_definition (COMMA cds+=create_definition)* RPAREN SEMICOLON
  {
    List<a.b.MyAntlrColumn> columns = new ArrayList<a.b.MyAntlrColumn>();
    columns.add($cd.column);
    for (Create_definitionContext c : $cds) {
      columns.add(c.column);
    }
    $tableDef = new a.b.MyAntlrTable($tn.text, columns);
  }
  ;

では、以前から使っているテスト用の DML を試しにパースして表示してみます。以前の DML はこんな感じでした。

create table analytics
(a_id int(25) not null auto_increment,
 a_blog varchar(200) default null,
 a_user_id int(25) default null,
 a_ip varchar(30) default null,
 a_time varchar(20) default null,
 a_cockie varchar(100) default null,
 a_rq_p_id int(25) default null,
 a_request_url varchar(100) default null,
 primary key (a_id),
 key a_blog (a_blog),
 key a_blog_2 (a_blog),
 key a_user_id (a_user_id),
 key a_rq_p_id (a_rq_p_id));

これを取り込んで単純に出力する Java プログラムを作ります。

package a;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;

import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;

import a.antlr.MyMySQLLexer;
import a.antlr.MyMySQLParser;
import a.antlr.MyMySQLParser.Create_tableContext;

public class Main4 {
    public static void main(String[] args) {
        try {
            List<String> readAllLines = Files.readAllLines(Paths.get("src/test/resources/test1.txt"));
            String sql = readAllLines.stream().collect(Collectors.joining(" "));
            MyMySQLLexer lexer = new MyMySQLLexer(new ANTLRInputStream(sql.toLowerCase()));
            CommonTokenStream tokenStream = new CommonTokenStream(lexer);
            MyMySQLParser parser = new MyMySQLParser(tokenStream);
            Create_tableContext tableContext = parser.create_table();
            System.out.println(tableContext.tableDef);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

ここらへんは、以前のパートでも説明したので説明は割愛しますが、出力結果を見るとそれっぽい感じで出てきています!

MyAntlrTable [tableName=analytics, columns=[MyAntlrColumn [name=a_id, columnOption=MyAntlrColumnOption [type=class java.lang.Integer, option=[notnull, auto_increment]]], MyAntlrColumn [name=a_blog, columnOption=MyAntlrColumnOption [type=class java.lang.String, option=[defaultnull]]], MyAntlrColumn [name=a_user_id, columnOption=MyAntlrColumnOption [type=class java.lang.Integer, option=[defaultnull]]], MyAntlrColumn [name=a_ip, columnOption=MyAntlrColumnOption [type=class java.lang.String, option=[defaultnull]]], MyAntlrColumn [name=a_time, columnOption=MyAntlrColumnOption [type=class java.lang.String, option=[defaultnull]]], MyAntlrColumn [name=a_cockie, columnOption=MyAntlrColumnOption [type=class java.lang.String, option=[defaultnull]]], MyAntlrColumn [name=a_rq_p_id, columnOption=MyAntlrColumnOption [type=class java.lang.Integer, option=[defaultnull]]], MyAntlrColumn [name=a_request_url, columnOption=MyAntlrColumnOption [type=class java.lang.String, option=[defaultnull]]], null, null, null, null, null]]

では、これで準備が整ったので、上記で生成した MyAntlrTable のインスタンスを利用して、 Pojo を生成します。と言っても、 JavaPoet の記事で書いたことをやるだけですね。流れは以下のようになります。

package a;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;

import javax.lang.model.element.Modifier;

import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;

import a.antlr.MyMySQLLexer;
import a.antlr.MyMySQLParser;
import a.antlr.MyMySQLParser.Create_tableContext;
import a.b.MyAntlrColumn;
import a.b.MyAntlrTable;

import com.google.common.base.CaseFormat;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeSpec.Builder;

public class Main5 {
    public static void main(String[] args) {
        try {
            List<String> readAllLines = Files.readAllLines(Paths.get("src/test/resources/test1.txt"));
            String sql = readAllLines.stream().collect(Collectors.joining(" "));
            MyMySQLLexer lexer = new MyMySQLLexer(new ANTLRInputStream(sql.toLowerCase()));
            CommonTokenStream tokenStream = new CommonTokenStream(lexer);
            MyMySQLParser parser = new MyMySQLParser(tokenStream);
            Create_tableContext tableContext = parser.create_table();
            generatePojo(tableContext.tableDef);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    private static void generatePojo(MyAntlrTable t) {

        String name = t.getTableName();
        name = Character.toUpperCase(name.charAt(0)) + name.substring(1);
        Builder typeBuilder = TypeSpec.classBuilder(name).addModifiers(Modifier.PUBLIC);
        for (MyAntlrColumn c : t.getColumns()) {
            if (c != null) {
                addMember(typeBuilder, c);
            }
        }
        JavaFile src = JavaFile.builder("a.b1", typeBuilder.build()).build();
        System.out.println(src);
    }

    private static void addMember(Builder typeBuilder, MyAntlrColumn c) {
        Class<?> type = c.getColumnOption().getType();
        String name = c.getName();
        name = CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, name);
        typeBuilder.addField(type, name, Modifier.PRIVATE);
        addGetterAndSetter(typeBuilder, name, type);
    }

    private static void addGetterAndSetter(Builder typeBuilder, String name, Class<?> type) {
        typeBuilder.addMethod(MethodSpec.methodBuilder("get" + Character.toUpperCase(name.charAt(0)) + name.substring(1))
                .addModifiers(Modifier.PUBLIC)
                .returns(type)
                .addStatement("return this.$L", name)
                .build());

        typeBuilder.addMethod(MethodSpec.methodBuilder("set" + Character.toUpperCase(name.charAt(0)) + name.substring(1))
                .addModifiers(Modifier.PUBLIC)
                .returns(void.class)
                .addParameter(type, name)
                .addStatement("this.$L = $L", name, name)
                .build());
    }
}

今回新しいことは特にありませんね。MySQL の DML を取り込んでパースし、情報を集めた Bean から、Model(Pojo) を生成しています。
生成されるのは、メンバー、メンバーの Getter/Setter です。実際に実行した結果は以下のようになります。

package a.b1;

import java.lang.Integer;
import java.lang.String;

public class Analytics {
  private Integer aId;

  private String aBlog;

  private Integer aUserId;

  private String aIp;

  private String aTime;

  private String aCockie;

  private Integer aRqPId;

  private String aRequestUrl;

  public Integer getAId() {
    return this.aId;
  }

  public void setAId(Integer aId) {
    this.aId = aId;
  }

  public String getABlog() {
    return this.aBlog;
  }

  public void setABlog(String aBlog) {
    this.aBlog = aBlog;
  }

  public Integer getAUserId() {
    return this.aUserId;
  }

  public void setAUserId(Integer aUserId) {
    this.aUserId = aUserId;
  }

  public String getAIp() {
    return this.aIp;
  }

  public void setAIp(String aIp) {
    this.aIp = aIp;
  }

  public String getATime() {
    return this.aTime;
  }

  public void setATime(String aTime) {
    this.aTime = aTime;
  }

  public String getACockie() {
    return this.aCockie;
  }

  public void setACockie(String aCockie) {
    this.aCockie = aCockie;
  }

  public Integer getARqPId() {
    return this.aRqPId;
  }

  public void setARqPId(Integer aRqPId) {
    this.aRqPId = aRqPId;
  }

  public String getARequestUrl() {
    return this.aRequestUrl;
  }

  public void setARequestUrl(String aRequestUrl) {
    this.aRequestUrl = aRequestUrl;
  }
}

非常に straightforward な Pojo です。DML に定義した情報は全てこちらの生成ロジックに持ち込めるため、 RDB 特有のキー制約や Null 制約、フォーマットの制約なども Annotation などを利用して情報を付加することが出来ます。こういった部分を自動化することには大きなメリットがあります。一つ目は、 RDB にクエリを投げる前に自前に Java レベルで制約をチェックすることが出来ます。通常のアプリケーション開発では、 一つの処理が開始されて最終的に RDB にデータを登録することがゴール地点です。ですので、最後の最後に制約エラーで失敗すると、それまでにやってきた処理が全部無駄になります。また、 DB は別のサーバーにある場合が多く、ネットワークを介しての通信となるため、処理速度が Java のレベルと比べると expensive な operation となるため、パフォーマンスの観点から極力アクセスを減らすことがセオリーとなっています。二つ目は、開発者の大切な時間を使って愚直なコードを書く量を減らすことにより、より開発者が他の問題に取り組めるため productive になります。三つ目は、開発中、頻繁に更新される DB schema と sync 出来ることです。もしこれを手動でやっている場合、誰かが定期的に DB と実際のコードに差異がないか確認しなければなりません。いざ本番にリリースしようとしたらカラムの型が違っていた、と言った問題も未然に防げます。開発者は、多数の問題と戦っている上で、こういった ‘つまらない’ (未然に防げる問題) を減らすことで、これもまた productivity を高めることに繋がります。開発者からすればつまならい問題というのはたくさんあります。しかし、開発者でなければそういった問題を解決できない、というものが多いのも IT の厄介なところです。

これで、 Antlr の説明は終了ですが、これを更に発展させていけば、もっと複雑な Data Access Object なども自動生成になりますので、是非試してみてください。以上で終わりますが、全5記事による説明で Antlr で何が出来て、どういった領域に適用できるのか何となく分かっていただけたら嬉しいです。是非皆さんも、 Antlr を使って productivity を高めてください。

【関連記事】
Splunkに株価を取り込んでみた ft. Fujikawa
Introduction to Antlr!
Introduction to Antlr! Pt.2
JavaPoetで簡単コード生成!
Introduction to Antlr! Pt.3
モニタリングルーター Sensu
マイクロフレームワーク Spark
Introduction to Antlr! Pt.4