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を作成しました。
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も作成します。
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"]
JayDeBeApi==1.1.1
JPype1==0.6.3
redashの起動
ここまできたら、redashを起動することができます。
なお、追加したquery_runnerをredashに読み込ませるために、 REDASH_ADDITIONAL_QUERY_RUNNERS
環境変数に redash.query_runner.jdbc
を付けて起動する必要があります。
redashを起動し、DataSource画面を見るとJDBCというデータソースが追加されていることが確認できます。
現時点ではデータソースを表すアイコン画像がないせいでちょっと残念な見た目になってしまっています。
この部分に画像を入れるためにはclient/app/assets/images/db-logos
に jdbc.png
という画像を配置し、再度コンテナのbuildを行えばOKです。
クエリを実行
JDBCドライバーをquery_runnerとして登録できたので、クエリを実行してみます。
まとめ
JayDeBeApiを使用することによって、redashでJDBCドライバーを使用することができました。
これでredashのデータソースのバリエーションが広がり、素敵なBIライフを送ることができるようになりました。