Flask-APScheduler定时任务查询操作数据库(多文件/模块)

开篇引用一篇博主的话:“由于百度这些搜出来的关于flask-apscheduler的文章都是异步加减乘除的,我加个锤子哦,没有一点实在一点的文章”

确实,这就是我这几天针对这个问题在网上查了那么多文章资料后的内心真实写照!
该文章传送门:flask-apscheduler框架异步任务访问数据库的问题

PS:我在这篇博客中传送的文章都是真正有利于此问题解决的文章


好,言归正传,我们先说问题:

用Flask-APScheduler写了个定时器,执行时报错:RuntimeError: No application found. Either work inside a view function or push an application context.

问描述就是没有找到app。

原因也很简单,操作db需要app,而定时器在后台运行实际上是找不到app的,需要push一个app context给它,让它在上下文里面工作:

with app.app_context():
    # put db operate code here like this:
    list_Users= db.session.query(Users).all()

举一反三:线程Thread也是一样的,相关操作需要放在上下文里面的

写到这,Flask-APScheduler的用法这边也记录下吧:

Flask-APScheduler用法介绍

1、安装包Flask-APScheduler

pipenv install Flask-APScheduler
# 或者如果你用pip也一样
# pip install Flask-APScheduler

2、配置任务JOBS

# flaskdemo/config.py
class APSchedulerJobConfig(object):
    JOBS = [
        {
            'id': 'autosubimit',
            'func': 'job_func',
            'args': None,
            'trigger': {
                'type': 'cron', # 类型
                'day_of_week': "0-6",	# 可定义具体哪几天要执行
                'hour': '22',	# 小时数
                'minute': '0'
            }
        },{
            'id': 'job_3',
            'func': 'exchange_an',
            'args': '',
            'trigger': 'interval',
            'seconds': 5	#  每隔5秒执行一次
        },{
        	'id': 'job_test',
        	'func': 'test_apscheduler',
        	'args': None,
        	'next_run_time': datetime.datetime.now() + datetime.timedelta(seconds=10)
        }
    ]

上述代码中几个参数:

  1. id:自定义ID
  2. func:即你要定时执行的函数,书写规则是 ‘目录 : 函数名’,从config文件所在的目录算起
  3. args: 要传入的参数
  4. trigger:任务类型,或者理解为定时器开启的方式,有三种:date表示具体的一次性任务,interval表示循环任务,cron表示定时任务,这里需要注意下如果没有指定trigger,则默认为date类型,所以如果是一次性任务,可以不用写种类,如上述job_3

3、启动任务

# flaskdemo/main.py
from flask import Flask
from flask_apscheduler import APScheduler

app=Flask(__name__)
app.config.from_object(APSchedulerJobConfig)

if __name__ == "__main__":
	scheduler=APScheduler()
	scheduler.init_app(app)
	scheduler.start()

	app.run(debug=True)

4、编写要定时执行的函数

好了,现在可以开始写具体的业务函数了,这里需要注意的就是上文提到的问题:在定时执行的函数func的实现过程中,需要注意Flask 的 Context 机制,比如在func中需要基于Flask-SQLAlchemy访问数据库,需要这么处理:

# flaskdemo/main.py
def job_func():
	with app.app_context():
    	# put db operate code here like this:
    	list_Users= db.session.query(Users).all()

这里插一嘴,不是一脚,我觉得很nice的一个优化,在stackoverflow中看到一篇问答中提到:“You probably want an app context for all jobs. You can subclass the extension and override run_job.”,你可以继承APScheduler类:

from flask_apscheduler import APScheduler as _BaseAPScheduler

class APScheduler(_BaseAPScheduler):
    def run_job(self, id, jobstore=None):
        with self.app.app_context():
            super().run_job(id=id, jobstore=jobstore)
            # super(APScheduler, self) in Python 2

传送门:Querying model in Flask-APScheduler job raises app context RuntimeError

好,划重点:以上正常可用的前提是job_func和scheduler对象在同一个模块内,即可以获取到app对象,才有app_context,即便你

from flask import current_app
app = current_app._get_current_object()
with app.app_context():
	pass

也是报错!因为该JOB模块相当于是后台单独异步运行的,调用flask的current_app是无效的,捕获不到主进程app。

多模块(文件)解决方案1:重新create app

那么我们首先想到的就是没有app,那么我们就重新建一个app,通过create_app工厂模式等重新生成一个flask app对象,包括db对象,自己打包一条龙,类似后台管理用flask_script写manage.py文件一样,没有就自己造嘛!这里可以参考这位兄弟的文章:flask-apscheduler的app_context问题
写得比网上大多数文章靠谱多了!这里贴一下他的代码:

from app import create_app

def shcduler_job1():
        app = create_app(args)
    with app.app_context:
        do the job

他在文章内还提供了定时任务的固定app,来避免创建大量app和db占用资源的情况,很有用的功能,大家参考,定时任务少的就没有必要global app。

多模块(文件)解决方案2:apscheduler单独导入

好,这里重点讲下我的解决方案,摸爬滚打出来的解决方案:把apscheduler对象单独做一个模块包,在main中初始化,在job模块中再调用,这样就可以带有初始化后端apscheduler对象,在其上下文中操作数据库model即可,废话不多说,看代码:

  1. 老规矩,先看项目结构图
    在这里插入图片描述
  2. common.init.py 生成apscheduler对象
from flask_apscheduler import APScheduler # as _BaseAPScheduler
scheduler = APScheduler()
  1. config.py中编写配置
class APSchedulerJobConfig(object):
    JOBS = [
        {
            'id': 'autosubimit',
            'func': 'flaskdemo.apschedulerjob:auto_submit_planlist', # 路径:job函数名
            'args': None,
            'trigger': {
                'type': 'cron',
                'hour': '18',
                'minute': '32'
            }
        }
    ]
    SCHEDULER_API_ENABLED = True
    SQLALCHEMY_ECHO = True
  1. main.py主程序中初始化
from flaskdemo.common import scheduler	# 先导入生成的sheduler对象
from flaskdemo.config import APSchedulerJobConfig
...

app.config.from_object(APSchedulerJobConfig) # 导入配置

if __name__ == "__main__":
	...
    if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':	# 解决FLASK DEBUG模式定时任务执行两次
        scheduler.init_app(app)
        scheduler.start()
    ... 
  1. apschedulerjob.py JOB函数中使用上下文
from flaskdemo.common import scheduler	# 很关键的一步,导入初始化过的sheduler对象

def auto_submit_planlist():
	with scheduler.app.app_context():	# 这个sheduler是带有app及其上下文的
		# put db operate code here like this:
    	list_Users= db.session.query(Users).all()	# 终于可以查询了,激动!!!
    	

大功告成!

这里再补充说明两点:
(1)第5步一定是要导入scheduler,这个是带有app及其上下文的,如果直接用current_app替代scheduler.app使用上下文,则会报错:

RuntimeError: Working outside of application context.

This typically means that you attempted to use functionality that needed
to interface with the current application object in some way. To solve
this, set up an application context with app.app_context().  See the
documentation for more information.

(2)在开篇提到的那个博主,他因为要不到app,所以在继承了Apscherduler类后,在新类中添加了个get_app函数,如下所示:

from flask_apscheduler import APScheduler as _BaseAPScheduler

class APScheduler(_BaseAPScheduler):
	def get_app(self):
		if self.app:
			return self.app
		else:
			return current_app
    def run_job(self, id, jobstore=None):
        with self.app.app_context():
            super().run_job(id=id, jobstore=jobstore)
            # super(APScheduler, self) in Python 2

如此之后,再在job函数里面调用这个新类生成的apscheduler对象:

def test():
	with scheduler.app.app_context():
		task = Task.query.all()

从仅有代码里我分析他继承和生成apscheduler对象的代码应该是在主进程了,否则他调不到app或者current_app(他回复我在apscheduler.py中需要导入current_app),然后再在job模块导入这个对象,这个模式其实和我的方式几乎一样,但是我是可以直接用在主进程中初始化的apscheduler对象,所以我并不是太懂(因为没看到所有代码和目录结构)为何还要继承类手动去写get_app,我猜想他可能没有在主进程中初始化(init)apscheduler对象故而直接在job模块函数中调用而调不到,所以需要手动写。
(3)当DEBUG模式调用app.run()的时候,用到了Werkzeug库,它会生成一个子进程,当代码有变动的时候它会自动重启,所以会Flask会执行两次定时任务。可以在run()里加入参数 use_reloader=False,就会取消这个功能,当然,在以后的代码改动后也不会自动更新了。

app.run(debug=True, use_reloader=False)

或者也可以查看WERKZEUG_RUN_MAIN环境变量, 默认情况下,调用子程序时,它会被设置为True。

# 解决FLASK DEBUG模式定时任务执行两次
if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':	
    scheduler.init_app(app)
    scheduler.start()

传送门:
python(flask/gunicorn)+apscheduler定时邮件重发两次的问题
Flask-为什么会启动两次

其他方案1:动态创建job(未试验)

也是网上看到的文章,看起来他似乎解决了这个问题,但因为我没用Redis,所以我并没有按照他的做法试验,但大致思路是不写配置文件,而是作为参数主动apscheduler.add_job()传入,而且他可以用current_app,但看起来他遇到了同样的问题也解决了,所以这里也传送一下供参考:Flask-APScheduler 爬坑指南

“APScheduler 的任务执行其实和线程池差不多,后台启动多个线程(当然数量是可以配置的)处理添加的job,但是 flask 的应用上下文只在当前线程可见,所以直接在线程函数中执app.config[‘DB_PORT’]会报错。正确的处理方法是将当前的应用上下文传递给线程函数 func”。如下所示:

app.apscheduler.add_job(uuid.uuid1().get_hex(), func, args=[app,...], ...)

看起来很简单,但我实际上跑起来IDE报错:flask app is not iterable,所以我没搞明白怎么做。SO,还是解决方案2比较靠谱,也不用重建app。

其他方案2:在JOB模块中生成apscheduler再在主程序调用初始化(未试验)

在寻找解决方案时咨询也遇到此问题的网友@pipi荨 他的解决方案:“从task中生成scheduler实例,然后在主函数中引用,相当于和视图一样,把实例注册到主函数中就可以了”
看起来也是合理的,后面有时间再试下。

走过的弯路+1,Mark!

发布了25 篇原创文章 · 获赞 24 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/arnolan/article/details/84936075