RedashでJDBCドライバーを使う方法

Redashは標準で数多くのデータソースに対応しています。
さらに、対応していないデータソースがあったとしても、Pythonでquery_runnerを書くことによって新たなデータソースを追加することができます。
対象のデータソースに接続するためのPythonライブラリがあればquery_runnerを素直に書くことができますが、一部のDBはJDBCドライバーしか提供されていないことがあります。
JDBCドライバーはJVM言語からしか呼び出すことができないために、そのままredashのquery_runnerに組み込むことができません。
そこで一工夫をして、JDBCドライバーをredashに組み込んでみましたので、この記事ではその方法を紹介いたします。

# 使用するライブラリ
今回はPythonからJDBCドライバーを呼び出すためのライブラリである。JayDeBeApiを使用します。
https://github.com/baztian/jaydebeapi

このライブラリはCPython環境下ではJPypeを使いPythonとJavaの間の橋渡しを行います。
https://github.com/jpype-project/jpype

PythonからJavaを呼び出すためのライブラリにはPy4Jがあり、Qiitaの紹介記事もありますが、それと比較してJPypeはJavaのプロセスを立ち上げる必要がないために動作が軽い気がします。

https://www.py4j.org/
https://qiita.com/riverwell/items/e90cbbfdac439e6e9d30

# redashにJDBCドライバーを組み込む
今回はredash v5.0.2にJDBCドライバーを組み込みます。
組み込むJDBCドライバーはMySQLのもので、以下のURLからダウンロードします。
https://dev.mysql.com/downloads/connector/j/5.1.html

redashは標準でMySQLに対応しているので、無理にJDBCドライバーを組み込む必要は全くありませんが、広く親しまれているDBなので説明のために使用します。

## jdbc.py
次にquery_runnerを書きます。
query_runnerの書き方は以下の記事によくまとまっています。
https://discuss.redash.io/t/creating-a-new-query-runner-data-source-in-redash/347

上記の記事を参考にして、以下のquery_runnerを作成しました。

```python
import json
import logging
import os
import jaydebeapi

from redash.query_runner import *
from redash.settings import parse_boolean
from redash.utils import JSONEncoder

logger = logging.getLogger(__name__)
types_map = {
    jaydebeapi.STRING: TYPE_STRING,
    jaydebeapi.TEXT: TYPE_STRING,
    jaydebeapi.NUMBER: TYPE_INTEGER,
    jaydebeapi.FLOAT: TYPE_FLOAT,
    jaydebeapi.DECIMAL: TYPE_FLOAT,
    jaydebeapi.DATE: TYPE_DATE,
    jaydebeapi.DATETIME: TYPE_DATETIME,
    jaydebeapi.ROWID: TYPE_STRING,
}

class Jdbc(BaseSQLQueryRunner):
    noop_query = "SELECT 1"

    @classmethod
    def configuration_schema(cls):
        schema = {
            'type': 'object',
            'properties': {
                'host': {
                    'type': 'string',
                },
                'port': {
                    'type': 'number',
                    'default': 3306,
                },
                'user': {
                    'type': 'string'
                },
                'password': {
                    'type': 'string',
                },
                'database': {
                    'type': 'string',
                },
            },
            "order": ['host', 'port', 'user', 'password', 'database'],
            'required': ['host', 'port', 'user', 'database'],
            'secret': ['password']
        }

        return schema

    @classmethod
    def name(cls):
        return "jdbc"

    @classmethod
    def enabled(cls):
        try:
            import jaydebeapi
        except ImportError:
            return False

        return True

    def _get_tables(self, schema):
        query = """
        SELECT col.table_schema,
               col.table_name,
               col.column_name
        FROM `information_schema`.`columns` col
        WHERE col.table_schema NOT IN ('information_schema', 'performance_schema', 'mysql');
        """

        results, error = self.run_query(query, None)
        if error is not None:
            raise Exception("Failed getting schema.")

        results = json.loads(results)

        for row in results['rows']:
            if row['TABLE_SCHEMA'] != self.configuration['database']:
                table_name = u'{}.{}'.format(row['TABLE_SCHEMA'], row['TABLE_NAME'])
            else:
                table_name = row['TABLE_NAME']

            if table_name not in schema:
                schema[table_name] = {'name': table_name, 'columns': []}

            schema[table_name]['columns'].append(row['COLUMN_NAME'])

        return schema.values()

    def run_query(self, query, user):
        import jaydebeapi

        connection = None
        try:
            host = self.configuration.get('host', '')
            port = self.configuration.get('port', '')
            user = self.configuration.get('user', '')
            password = self.configuration.get('password', '')
            database = self.configuration.get('database', '')

            jclassname = 'com.mysql.jdbc.Driver'
            url = 'jdbc:mysql://{}:{}/{}'.format(host, port, database)
            driver_args = {'user': user, 'password': password}
            jar_path = '/app/redash/query_runner/mysql-connector-java-5.1.47.jar'
            connection = jaydebeapi.connect(jclassname, url, driver_args, jar_path)

            cursor = connection.cursor()
            logger.info("JDBC running query: %s", query)
            cursor.execute(query)

            data = cursor.fetchall()

            if cursor.description is not None:
                columns = self.fetch_columns([(i[0], types_map.get(i[1], None)) for i in cursor.description])
                rows = [dict(zip((c['name'] for c in columns), row)) for row in data]

                data = {'columns': columns, 'rows': rows}
                json_data = json.dumps(data, cls=JSONEncoder)
                error = None
            else:
                json_data = None
                error = "No data was returned."

            cursor.close()
        except jaydebeapi.Error as e:
            json_data = None
            error = str(e.message)
        except KeyboardInterrupt:
            cursor.close()
            error = "Query cancelled by user."
            json_data = None
        finally:
            if connection:
                connection.close()

        return json_data, error

register(Jdbc)
```

configuration_schemaを除くほとんどの処理はどのJDBCドライバを使用しても共通して使えるかと思います。

## 各種ファイルの配置
上記の手順で作成した `jdbc.py` 及び、JDBCドライバーのjarファイルを `redash/query_runner` に配置します。


## Dockerfile
最後に、Redashを動作させるためのDockerコンテナを作ります。
JPypeを動作させるためにはJREが必要なので、redash標準のDockerfileにパッチを当てます。
また、jaydebeapiのインストールを行うためのrequirements.txtも作成します。

```dockerfile

FROM redash/base:latest

# JREのインストールを追加
RUN apt-get update && apt-get install -y openjdk-8-jre

COPY redash/requirements.txt redash/requirements_dev.txt redash/requirements_all_ds.txt requirements_jaydebeapi.txt ./
RUN pip install -r requirements.txt -r requirements_dev.txt -r requirements_all_ds.txt -r requirements_jaydebeapi.txt

COPY . ./
RUN npm install && npm run build && rm -rf node_modules
RUN chown -R redash /app
USER redash

ENTRYPOINT ["/app/bin/docker-entrypoint"]
CMD ["server"]
```

```plaintext:requirements.txt
JayDeBeApi==1.1.1
JPype1==0.6.3
```

# redashの起動
ここまできたら、redashを起動することができます。
なお、追加したquery_runnerをredashに読み込ませるために、 `REDASH_ADDITIONAL_QUERY_RUNNERS` 環境変数に　`redash.query_runner.jdbc` を付けて起動する必要があります。

redashを起動し、DataSource画面を見るとJDBCというデータソースが追加されていることが確認できます。

<img width="1124" alt="スクリーンショット 2018-11-29 14.28.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/74551/110e2750-5cd2-ac45-8821-f526ac8cc307.png">

現時点ではデータソースを表すアイコン画像がないせいでちょっと残念な見た目になってしまっています。
この部分に画像を入れるためには`client/app/assets/images/db-logos`に　`jdbc.png` という画像を配置し、再度コンテナのbuildを行えばOKです。

## クエリを実行
JDBCドライバーをquery_runnerとして登録できたので、クエリを実行してみます。

<img width="791" alt="スクリーンショット 2018-11-29 14.30.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/74551/ca1b72c4-0576-2d22-f401-5dc36faba2ad.png">


# まとめ
JayDeBeApiを使用することによって、redashでJDBCドライバーを使用することができました。
これでredashのデータソースのバリエーションが広がり、素敵なBIライフを送ることができるようになりました。