单独使用_单独使用Superset后端

11806bb4b0bf05a0b6cccdde4b83835e.png

一、模型(Model)

这么深度的定制开发,很可能需要新增一些模型(Model)以及对应的数据库表(table),这就会产生数据升级的需求。Flask Migrate可以实现数据库结构与现有模型的对比,并生成对应的升级、回退脚本(基于SQLAlchemy和Alembic)。

老王:Superset中的Flask Migrate

--这是2017年整理的Flask Migrate的步骤。

Tip:在初始化开发环境时,应该跳过load example这一步,避免样例数据集干扰Migrate工具的判断。在开发环境上,如果已经初始化过了,可以把Superset的数据库备份一下,重新初始化。

95e68d582733fb5b9aa705ae1f92b19a.png

停止Superset服务,再用mv命令重命名superset.db,执行初始化操作:

fabmanager create-admin -app superset

superset db upgrade

superset init

88794b97f8a0695b458ebdc7db1bddeb.png

b148e35fd69d158efd72fe95893895f8.png

初始化之后,用flask run命令启动服务:

/home/superset_master/bin/flask run -h 10.1.1.234 -p 8088 --with-threads --reload

登陆之后的前端是这样的,没有example中的数据、图表了:

b0b1dbcd068b28dd1a47bd5a9bcf2306.png

1ccf23b74c8d346409d374135dea529b.png

增加业务模型:

首先是“数据源”,需求中的这个概念和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

可以看到生成了新的数据库处理文件。

该文件的主体是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

然后,修改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

以数据源为例,增加数据源的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

点击菜单后可以看到后端日志中有/datasource/save,返回值为405:

f43ac84da25954160a9898d8f2b9c2d4.png

ecb8b889dec7e5693ab3a271c3280f5c.png

至少说明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

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.html​www.pythondoc.com

mysql://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

参考:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值