![11806bb4b0bf05a0b6cccdde4b83835e.png](https://i-blog.csdnimg.cn/blog_migrate/400354957d87979b421c50effd9175ed.jpeg)
一、模型(Model)
这么深度的定制开发,很可能需要新增一些模型(Model)以及对应的数据库表(table),这就会产生数据升级的需求。Flask Migrate可以实现数据库结构与现有模型的对比,并生成对应的升级、回退脚本(基于SQLAlchemy和Alembic)。
老王:Superset中的Flask Migrate
--这是2017年整理的Flask Migrate的步骤。
Tip:在初始化开发环境时,应该跳过load example这一步,避免样例数据集干扰Migrate工具的判断。在开发环境上,如果已经初始化过了,可以把Superset的数据库备份一下,重新初始化。
![95e68d582733fb5b9aa705ae1f92b19a.png](https://i-blog.csdnimg.cn/blog_migrate/60e3bf9542a35786414162b2f9f71f0f.jpeg)
停止Superset服务,再用mv命令重命名superset.db,执行初始化操作:
fabmanager create-admin -app superset
superset db upgrade
superset init
![88794b97f8a0695b458ebdc7db1bddeb.png](https://i-blog.csdnimg.cn/blog_migrate/cf2d49aa0c7a53f5f2bf9eb73704cf5d.jpeg)
![b148e35fd69d158efd72fe95893895f8.png](https://i-blog.csdnimg.cn/blog_migrate/9b2b2080583ee593ae88fc6a5b5e7dfe.jpeg)
初始化之后,用flask run命令启动服务:
/home/superset_master/bin/flask run -h 10.1.1.234 -p 8088 --with-threads --reload
登陆之后的前端是这样的,没有example中的数据、图表了:
![b0b1dbcd068b28dd1a47bd5a9bcf2306.png](https://i-blog.csdnimg.cn/blog_migrate/5f7aceb157b9e814b23af1183a127fe7.jpeg)
![1ccf23b74c8d346409d374135dea529b.png](https://i-blog.csdnimg.cn/blog_migrate/8ab850ae6c6a66745b2cc0e9888292df.jpeg)
增加业务模型:
首先是“数据源”,需求中的这个概念和Superset的数据库非常接近(Superset主数据库中的dbs表),不同在于,在Superset仅仅作为数据提供者的场景中,用户管理功能由外部提供,拿到的数据库设计,“数据源”表中没有“系统用户”相关的字段。
而dbs表中的有两个外键,创建人、修改人都关联到系统用户(ab_user)表:
FOREIGN KEY(created_by_fk) REFERENCES ab_user (id),
FOREIGN KEY(changed_by_fk) REFERENCES ab_user (id)
暂时决定单独创建一个独立的模型,用于保存“数据源”信息。同时,该模型需要一个拼接sqlalchemy_uri的函数【参考1】,以便和现有的连接数据库的代码兼容(dbs表中有sqlalchemy_uri字段)。而且,如果以后不使用Superset做数据提供者了,“数据源”表中不保存ab_user的信息,也更方便移植。(TODO:“数据源”这样的表怎么绕过Superset的权限体系呢?等通过前端来访问时再回来补充。)【2019-05-13补充:获取table字段信息时,发现新建一个“数据源”类会带来大量的代码复制,看起来还是要沿用Superset现有的Database,或者从Database继承】。
dbs的定义在superset/models/core.py中。
class Database(Model, AuditMixinNullable, ImportMixin):
"""An ORM object that stores Database related information"""
除了字段定义之外,还有大量的成员函数、成员变量,比如viz.py中用到的get_db函数,就是Database的成员函数。在创建模型这个阶段,这些函数、变量可以先不处理。
from superset.models.helpers import AuditMixinNullable, ImportMixin
AuditMixinNullable和ImportMixin在 superset/models/helpers.py中定义,AuditMixinNullable里面用到了ab_user表中的变量,貌似在记录哪个用户创建了表,对“数据源”表来说,应该不需要从AuditMixinNullable继承。
ImportMixin貌似和导入有关,暂时没看到有副作用,先保留。
在superset/models/core.py中增加Datasource的定义:
class Datasource(Model, ImportMixin):
"""database and other sources"""
__tablename__ = 'datasource'
type = 'table'
id = Column(Integer, primary_key=True)
name = Column(String(250), unique=True)
type = Column(String(40))
host = Column(String(200))
port = Column(Integer)
username = Column(String(50))
password = Column(String(50))
created = Column(DateTime)
changed = Column(DateTime)
执行superset db migrate:
![ecfdf4b876284bbf7d649d628d01f41f.png](https://i-blog.csdnimg.cn/blog_migrate/785cbfade139ceb15d57ae68968466bd.jpeg)
可以看到生成了新的数据库处理文件。
该文件的主体是upgrade和downgrade两个函数,这两个函数可以手工调整(在我的测试环境上,是必须手工调整,只保留建表和drop表的语句。问题应该出在superset db migrate命令检测到了值为NULL的字段,并增加了对应的alter column命令,参见上图的"Detected NULL on column ***"):
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('datasource',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=250), nullable=True),
sa.Column('type', sa.String(length=40), nullable=True),
sa.Column('host', sa.String(length=200), nullable=True),
sa.Column('port', sa.Integer(), nullable=True),
sa.Column('username', sa.String(length=50), nullable=True),
sa.Column('password', sa.String(length=50), nullable=True),
sa.Column('created', sa.DateTime(), nullable=True),
sa.Column('changed', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('datasource')
# ### end Alembic commands ###
执行superset db upgrade命令:
(superset_master) [root@localhost superset]# superset db upgrade
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume transactional DDL.
INFO [alembic.runtime.migration] Running upgrade 80aa3f04bc82 -> 7d8d5967328e, empty message
可以在sqlite中看到增加了对应的表:
(superset_master) [root@localhost superset]# sqlite3 ~/.superset/superset.db
SQLite version 3.6.20
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .schema datasource
CREATE TABLE datasource (
id INTEGER NOT NULL,
name VARCHAR(250),
type VARCHAR(40),
host VARCHAR(200),
port INTEGER,
username VARCHAR(50),
password VARCHAR(50),
created DATETIME,
changed DATETIME,
PRIMARY KEY (id),
UNIQUE (name)
);
sqlite>
又仔细看了数据库设计,datasource的名字写错了,需要加上前缀,执行db downgrade:
(superset_master) [root@localhost superset]# superset db downgrade
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume transactional DDL.
INFO [alembic.runtime.migration] Running downgrade 7d8d5967328e -> 80aa3f04bc82, empty message
datasource表不见了:
![a50bdbd280fa34243498937faa6fc732.png](https://i-blog.csdnimg.cn/blog_migrate/62fc13e3a42e168bbda2836666890522.jpeg)
然后,修改core.py,去掉7d8d5967328e.py,重新执行db migrate、db upgrade。输出略。
多表关联
Superset自身的模型,在实现多表关联时,会单独建一张表,保存两张单表的关联信息。
比如Dashboard和Slice。Slice可以被加入到某个Dashboard中,这样就需要保存slice和dashboard之间的对应关系。
实现方式如下:
dashboard_slices = Table(
'dashboard_slices', metadata,
Column('id', Integer, primary_key=True),
Column('dashboard_id', Integer, ForeignKey('dashboards.id')),
Column('slice_id', Integer, ForeignKey('slices.id')),
)
...
class Dashboard(Model, AuditMixinNullable, ImportMixin):
"""The dashboard object!"""
__tablename__ = 'dashboards'
id = Column(Integer, primary_key=True)
dashboard_title = Column(String(500))
position_json = Column(utils.MediumText())
description = Column(Text)
css = Column(Text)
json_metadata = Column(Text)
slug = Column(String(255), unique=True)
slices = relationship(
'Slice', secondary=dashboard_slices, backref='dashboards')
owners = relationship(security_manager.user_model, secondary=dashboard_user)
但是老王拿到的数据库设计不是这样的,以上述Dashboard和Slice的例子来说,是只有Slice表和Dashboard表,没有关联的dashboard_slices表。Superset采用这样的结构,有什么优点么?
在stackoverflow上找了两个多表关联的例子,并没有采用这样的结构,所有的关联,仍然是在实体表内部处理:
flask-sqlalchemy multiple relationship types between two tables
SQLAlchemy relationships across multiple tables
为了避免查询数据的时候不能沿用Superset现有的流程,还是仿照现有的方式增加一张关联表。
05-14更新:找到一篇介绍添加一张关系表做关联的文章:Flask - SQLAlchemy Self Referential Relationship - CodeOmitted
按文章里的术语,这张额外的表,叫“Association table”。
5月16号补充:老王:SQLAlchemy:指向自身的关联(翻译)
5月23号补充:association table用在多对多的场景中,比如说,一个dashboard可以包含多个slice,而slice又可以归属于多个dashboard,这样,就没法在两侧(dashboard和slice表)中通过保存**id的方式体现这种关联关系,只好加一张association tables。
一对一的关系,就需要在两侧分别增加指向对方的foreignkey字段,而一对多关系,就是在一的一方不保存对方的id,仅有一个relationship变量,在多的一方保存一的Id。
二、URL
第三方的前端有自己的URL规范,需要让Superset支持新的URL,主要的修改点应该在superset/views目录下。
如何验证新的URL呢?毕竟在还没有验证新的鉴权机制,应该不能离开Superset前端的登陆,似乎可以从菜单上想办法。加一个新菜单项(或者还需要对应的视图),用于向服务器发新的URL。
CSDN有篇文章,展示了通过“去掉@has_access_api”,绕过权限控制的方法:
https://blog.csdn.net/qq273681448/article/details/75050513
superset/views/core.py里面,有【Import Dashboard】的注册代码:
appbuilder.add_link(
'Import Dashboards',
label=__('Import Dashboards'),
href='/superset/import_dashboards',
icon='fa-cloud-upload',
category='Manage',
category_label=__('Manage'),
category_icon='fa-wrench')
看起来,可以直接在菜单里面写URL(href参数)。
这是点【Import Dashboard】菜单时,后端的日志:
![15a28aae1ed1a479dac7c4bd288cc57f.png](https://i-blog.csdnimg.cn/blog_migrate/00c9491aaee5ff6659965496972ee817.png)
以数据源为例,增加数据源的URL是/datasource/save,在superset/views/core.py中增加新菜单项(测试过程中发现,datasource这个名字会和Superset已有的数据源冲突,故改成xdatasource):
appbuilder.add_link(
'Verify New URL',
label=__('Verify New URL(add datasource)'),
href='/xdatasource/save',
icon='fa-cloud-upload',
category='Manage',
category_label=__('Manage'),
category_icon='fa-wrench')
登陆后的界面:
![32cb67975c3d702d1c401fedd5774dbc.png](https://i-blog.csdnimg.cn/blog_migrate/e4b0797b6cc74f42236a9abff465419a.jpeg)
点击菜单后可以看到后端日志中有/datasource/save,返回值为405:
![f43ac84da25954160a9898d8f2b9c2d4.png](https://i-blog.csdnimg.cn/blog_migrate/3971e9ba63647c571847774c884becec.png)
![ecb8b889dec7e5693ab3a271c3280f5c.png](https://i-blog.csdnimg.cn/blog_migrate/5c04f754d1c5955057a4ce60c3caf9d6.jpeg)
至少说明add_link的方法是可行的。
还有一个需要解决的问题,http请求的前缀该怎么设置(老王不是专门的前端开发,不清楚标准的说法是什么)。
在flask_appbuilder的baseview.py里面,默认是用class的名字转换成小写,作为route_base:
class BaseView(object):
...
def create_blueprint(self, appbuilder,
endpoint=None,
static_folder=None):
...
if self.route_base is None:
self.route_base = '/' + self.__class__.__name__.lower()
如果要把xdatasource作为router_base,对应的View应该从BaseView继承(实际是从SupersetBaseView继承)。
class XDatasource(BaseSupersetView): # noqa
@expose('/save', methods=['GET', 'POST'])
def add(self):
logging.debug('Enter DatasourceView::add')
return self.json_response('inside DatasourceView::add')
可以看到流程走通了:
![9b6626078a366f542bb2fe25a36cba2c.png](https://i-blog.csdnimg.cn/blog_migrate/e31dbe0eabcc8e10669ce141bf716f1a.png)
05-13更新:
可以通过设置route_base成员变量改变默认的URL根路径:
class Datasource(BaseSupersetView): # noqa
route_base = '/datasource'
三、增加数据库
datasource是后端业务流程的基础。可以拆分成两个任务:增加一条记录、连接到数据库(借鉴test connection)。
增加一条记录:
在superset/views/core.py中,Superset类的成员函数save_slice功能类似:
from superset import (
app, appbuilder, cache, conf, db, results_backend,
security_manager, sql_lab, viz)
slc = models.Slice(owners=[g.user] if g.user else [])
def save_slice(self, slc):
session = db.session()
msg = _('Chart [{}] has been saved').format(slc.slice_name)
session.add(slc)
session.commit()
flash(msg, 'info')
看起来,是通过SQLAlchemy的session来增加记录。db是从superset引入的,应该可以直接用,slc是初始化了一个模型那边定义的Slice对象。先在代码里构造一条记录,加一条测试数据试试。
@expose('/save', methods=['GET', 'POST'])
def add(self):
logging.debug('Enter DatasourceView::add')
datasource = models.CiDODatasource()
datasource.id = '1'
datasource.name = 'test_1'
datasource.type = 'mysql'
datasource.host = '10.1.1.234'
datasource.port = 5000
datasource.username = 'test_user'
datasource.password = 'test_password'
datasource.created = datetime.now()
datasource.changed = datetime.now()
self.save_datasource(datasource)
return self.json_response('inside DatasourceView::add')
def save_datasource(self, datasource):
logging.debug('Enter save_datasource')
session = db.session()
msg = _('Datasource [{}] has been saved').format(datasource.name)
session.add(datasource)
session.commit()
logging.debug('save_datasource:after commit')
flash(msg, 'info')
执行成功,不过flash的内容没有显示到页面上(TODO:了解下flash的用法)。
执行成功之后,用sqlite看看是否增加了记录:
sqlite> select * from tbl_app_datasource;
1|test_1|mysql|10.1.1.234|5000|test_user|test_password|2019-05-10 14:41:59.876512|2019-05-10 14:41:59.876543
再通过通过URL传参增加一条记录:
appbuilder.add_link(
'Verify New URL',
label=__('Verify New URL(add datasource)'),
href='/xdatasource/save?id=2&name=test_2&type=psql&host=10.1.1.235&port=5001&username=test_user_2&password=test_password_2',
icon='fa-cloud-upload',
category='Manage',
category_label=__('Manage'),
category_icon='fa-wrench')
...
def add(self):
logging.debug('Enter DatasourceView::add')
logging.debug(request.args)
datasource = models.CiDODatasource()
datasource.id = int(request.args.get('id'))
datasource.name = request.args.get('name')
datasource.type = request.args.get('type')
datasource.host = request.args.get('host')
datasource.port = int(request.args.get('port'))
datasource.username = request.args.get('username')
datasource.password = request.args.get('password')
datasource.created = datetime.now()
datasource.changed = datetime.now()
self.save_datasource(datasource)
return self.json_response('inside DatasourceView::add')
def save_datasource(self, datasource):
logging.debug('Enter save_datasource')
session = db.session()
msg = _('CiDODatasource [{}] has been saved').format(datasource.name)
session.add(datasource)
session.commit()
logging.debug('save_datasource:after commit')
flash(msg, 'info')
执行成功后,用sqlite看看是否增加了记录:
sqlite> select * from tbl_app_datasource;
1|test_1|mysql|10.1.1.234|5000|test_user|test_password|2019-05-10 14:41:59.876512|2019-05-10 14:41:59.876543
2|test_2|psql|10.1.1.235|5001|test_user_2|test_password_2|2019-05-10 15:02:42.741239|2019-05-10 15:02:42.741269
四、获取数据中的表的字段信息
这个在SQLLab或者增加数据源时都有,是Superset自带的功能,现在需要移植到新增的View和Model里面。
先来看看现有的元数据是怎么获取的。
现有的获取元数据的功能在superset/models/core.py中,是Database类的成员函数。
def get_columns(self, table_name, schema=None):
return self.inspector.get_columns(table_name, schema)
inspector是Database类的成员变量:
@property
def inspector(self):
engine = self.get_sqla_engine()
return sqla.inspect(engine)
然后,get_sqla_engine()又是Database的成员函数,而且代码量还比较大。
如果要引入同样的机制,就需要复制这一系列函数,显然不利于维护代码。看起来要修改Database,以适应业务需求,而不是重新定义一个“数据源”类,或者业务的数据源类从Database继承。
在业务的数据源类里面,增加上述函数,增加一个新的菜单项用于验证。
另,获取到的columns需要经过处理才能转换成json:
payload_columns = []
for col in columns:
dtype = ''
try:
dtype = '{}'.format(col['type'])
except Exception:
dtype = col['type'].__class__.__name__
pass
payload_columns.append({
'name': col['name'],
'type': dtype.split('(')[0] if '(' in dtype else dtype,
'longType': dtype,
})
return self.json_response(payload_columns)
五、跨数据库关联两张表
需要借助pandas来实现跨库的两表关联。
本来打算从数据库到数据集都重新建立模型,在第四部分,获取表的字段信息时,发现如果重建模型会引入大量重复代码,那最容易想到的办法就是从models.Database类派生一个类出来,在子类里面增加业务所需接口,对于View,同样的思路应该也适用。
具体的业务逻辑就不写了,只写一下怎么利用pandas关联两张表,把一张表的数据加载到df_1,另一张表的数据加载到df_2,再利用pandas的join函数关联两个df。
这篇文章到这里就告一段落了,大致上可以看成一个技术原型阶段的总结。
感谢阅读。
2019-05-16 10:50
老王于长沙绿地广场
参考:
1、schema_uri:
http://www.pythondoc.com/flask-sqlalchemy/config.htmlwww.pythondoc.commysql://username:password@server/db
sqlite:root/.superset/superset.db
看起来,格式为数据库类型,数据用户名,密码,服务器,数据库。
2019-06-05更新:
superset/config.py
有个公共设置,看说明,是不需要鉴权的,改成Admin之后,用curl测了一下,不需要登陆就可以访问url:
# Uncomment to setup Public role name, no authentication needed
# AUTH_ROLE_PUBLIC = 'Public'
(superset_033) [root@localhost superset]# curl http://10.1.1.234:8088/datasource/list
[]
能想到的第一个修改点,是登陆。
github上有相关的讨论:
Widgets or embed dashboards · Issue #264 · apache/incubator-superset
给的建议是增加public的权限。
有一个定制authentication的文档:
https://medium.com/@sairamkrish/apache-superset-custom-authentication-and-integrate-with-other-micro-services-8217956273c1
这篇文章给的方法是重载CUSTOM_SECURITY_MANAGER ,重定向request。
![700f05da2ddcdd465993fc8337fc588c.png](https://i-blog.csdnimg.cn/blog_migrate/9b8341e8439a08994f49d8c54ccc9fda.jpeg)
参考: