Django启动时只执行一次代码的方法?

253
我正在编写一个 Django 中间件类,希望只在启动时执行一次,以初始化其他任意代码。我遵循了 sdolan 在 这里 发布的非常好的解决方案,但是 "Hello" 消息会在终端输出两次。例如:
from django.core.exceptions import MiddlewareNotUsed
from django.conf import settings

class StartupMiddleware(object):
    def __init__(self):
        print "Hello world"
        raise MiddlewareNotUsed('Startup complete')

在我的Django设置文件中,我已经将该类包含在MIDDLEWARE_CLASSES列表中。

但是当我使用runserver运行Django并请求页面时,在终端中会出现以下内容:

Django version 1.3, using settings 'config.server'
Development server is running at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
Hello world
[22/Jul/2011 15:54:36] "GET / HTTP/1.1" 200 698
Hello world
[22/Jul/2011 15:54:36] "GET /static/css/base.css HTTP/1.1" 200 0

有什么想法为什么会打印两次“Hello world”?谢谢。


1
只是出于好奇,您是否弄清楚为什么 init.py 中的代码会执行两次? - Mutant
11
在运行服务器时,它只会执行两次……这是因为runserver首先加载应用程序以检查它们,然后才启动服务器。即使在自动重新加载runserver时,代码也只会被执行一次。 - Pykler
3
哇,我一直在这里...所以再次感谢您的评论@Pykler,那正是我想知道的。 - WesternGun
10个回答

348

更新:Django 1.7现在有一个用于此目的的钩子

文件:myapp/apps.py

from django.apps import AppConfig
class MyAppConfig(AppConfig):
    name = 'myapp'
    verbose_name = "My Application"
    def ready(self):
        pass # startup code here

文件:myapp/__init__.py

default_app_config = 'myapp.apps.MyAppConfig'

针对 Django < 1.7

第一个答案似乎不再适用了,urls.py 在首次请求时加载。

最近有效的方法是将启动代码放在任何一个已安装的应用程序init.py 中,例如myapp/__init__.py

def startup():
    pass # load a big thing

startup()

使用 ./manage.py runserver 时会执行两次...这是因为runserver有一些技巧来先验证模型等等...正常部署或者即使是当runserver自动重新加载时,这只会执行一次。


6
我认为这会被每个加载项目的进程执行。所以,在任何部署情况下,我想不出为什么这不能完美地工作。这对于管理命令确实有效。+1 - Skylar Saveland
3
@Patrick 是的,你可以,你的启动函数可以长这样 https://gist.github.com/pykler/024334b23f18d66937f2 - Pykler
14
文档说明这不是进行任何数据库交互的地方,这使得它不适合许多代码。这段代码应该放在哪里? - Mark
3
当我尝试这个时,我在Django 1.10上遇到了django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet.的错误。这个答案现在已经过时了吗? - user2361174
3
如果您的脚本运行了两次,可以查看这个答案:https://dev59.com/_V4b5IYBdhLWcg3w9Fur#28504072 - Braden Holt
显示剩余23条评论

142

更新自Pykler下面的答案:Django 1.7现在具有用于此目的的钩子


不要用这种方式。

您不需要“中间件”来进行一次性启动操作。

您需要在顶层urls.py中执行代码。该模块被导入并执行一次。

urls.py

from django.confs.urls.defaults import *
from my_app import one_time_startup

urlpatterns = ...

one_time_startup()

2
@Andrei:管理命令是完全不同的问题。在所有管理命令之前进行特殊的一次性启动的想法很难理解。你需要提供一些具体的东西。也许可以在另一个问题中提出。 - S.Lott
11
urls.py 代码只在第一次请求时执行(猜测这回答了 @SteveK 的问题)(Django 1.5)。 - lajarre
4
在我的情况下,针对每个 worker,这将执行一次,总共会执行3次。 - Raphael
11
这个回答已经过时了,你应该使用 Pykler 给出的答案。 - Mark Chackerian
1
@MarkChackerian,如果涉及到数据库访问,Pykler的答案是无效的。 - Florent
显示剩余10条评论

47
这个问题在Entry point hook for Django projects博客文章中有很好的回答,该方法适用于Django版本>= 1.4。

基本上,你可以使用<project>/wsgi.py 来实现,它仅会在服务器启动时运行一次,而不是在运行命令或导入特定模块时运行。

import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings")

# Run startup code!
....

from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()

再次添加注释以确认此方法仅执行一次代码。不需要任何锁定机制。 - ATOzTOA
这里添加的脚本似乎在测试框架启动时没有被执行。 - Lewis Z
这个答案结束了长达两天半的寻找解决方案,而那些简单地不起作用。 - NMunro
5
请注意,此代码会在第一个网站请求时执行,而不是在启动Apache时执行。 - user984003

27

根据@Pykler的建议,在Django 1.7+中,您应该使用他回答中解释的钩子,但是如果您想要仅在调用运行服务器时调用函数(而不是在调用迁移、迁移、shell等时调用),并且希望避免AppRegistryNotReady异常,则需要执行以下操作:

文件:myapp/apps.py

import sys
from django.apps import AppConfig

class MyAppConfig(AppConfig):
    name = 'my_app'

    def ready(self):
        if 'runserver' not in sys.argv:
            return True
        # you must import your modules here 
        # to avoid AppRegistryNotReady exception 
        from .models import MyModel 
        # startup code here

14
这个在生产模式下运行吗?据我所知,在生产模式下,不会启动“runserver”。 - nerdoc
谢谢!我在我的应用程序中使用了高级Python调度程序,并且我不想在运行manage.py命令时运行调度程序。 - lukik
您需要在某个时候运行ready()吗? - Florent

21

如果有帮助的话,除了pykler的回答之外,"--noreload"选项可以防止runserver在启动时执行两次命令:

python manage.py runserver --noreload

但是这个命令不会在其他代码更改后重新加载runserver。


1
谢谢,这解决了我的问题!我希望在部署时不会发生这种情况。 - Gabo
3
作为替代方案,您可以检查 os.environ.get('RUN_MAIN') 的内容,以便仅在主进程中执行您的代码(请参阅 https://dev59.com/_V4b5IYBdhLWcg3w9Fur#28504072)。 - bdoering
是的,这个加上pykler的答案对我也起作用了,因为它防止了多次调用ready(self),同时仍然能够只启动一次。干杯! - DarkCygnus
Django的runserver默认会启动两个具有不同pid号码的进程。使用--noreload参数可以让它只启动一个进程。 - Eugene Gr. Philippov

17

标准解决方案

使用Django 3.1+,您可以编写此代码以在启动时仅执行一次方法。与其他问题的不同之处在于检查主启动进程(默认情况下,runserver会启动2个进程,一个作为快速代码重载的观察者):

import os 
from django.apps import AppConfig

class MyAppConfig(AppConfig):
    name = 'app_name'

    def ready(self):
        if os.environ.get('RUN_MAIN'):
            print("STARTUP AND EXECUTE HERE ONCE.")
            # call here your code

另一种解决方案是避免执行环境检查,但调用--noreload以强制仅使用一个进程。

替代方法

要回答的第一个问题是为什么我们需要执行代码一次:通常我们需要初始化一些服务、数据库中的数据或其他一次性操作。90%的情况下都是一些数据库初始化或作业队列。

使用AppConfig.ready()方法的方法不可靠,在生产中不总是可重现,并且不能保证只执行一次(但至少执行一次不同)。为了有一个相当可预测并且确切地执行一次的东西,最好的方法是开发一个Django BaseCommand并从启动脚本中调用它。

例如,我们可以在您的"myapp"中编写文件" app / management / commands / init_tasks.py":

from django.core.management.base import BaseCommand
from project.apps.myapp.tasks import scheduler
from project import logger, initialize_database_data

class Command(BaseCommand):
    help = "Init scheduler or do some staff in the database."

    def handle(self, *args, **options):
        scheduler.reload_jobs()
        initialize_database_data()
        logger.info("Inited")

最后,我们可以有一个启动脚本 "Start.bat"(在示例中为Windows批处理文件),以设置完整的应用程序启动:

start /b python manage.py qcluster
start /b python manage.py runserver 0.0.0.0:8000
start /b python manage.py init_tasks

2
BaseCommand对我来说最好用!我认为它应该是被接受的答案。 - Kasra Najafi
2
太好了!我认为这应该是被接受的答案。 - mahyar
1
在Linux上也能很好地工作。只需确保将management/commands/init_tasks.py放在应用程序内,而不是基础包中即可。 - Onyr

13

请注意,您不能在AppConfig.ready函数内可靠地连接到数据库或与模型交互(请参见文档中的警告)。

如果您需要在启动代码中与数据库交互,一种可能的方法是使用connection_created信号,在连接到数据库时执行初始化代码。

from django.dispatch import receiver
from django.db.backends.signals import connection_created

@receiver(connection_created)
def my_receiver(connection, **kwargs):
    with connection.cursor() as cursor:
        # do something to the database

显然,这个解决方案是针对每个数据库连接运行代码一次,而不是每个项目启动运行一次。因此,您需要为CONN_MAX_AGE设置一个合理的值,以便在每个请求上不会重新运行初始化代码。还要注意,开发服务器忽略CONN_MAX_AGE,因此在开发中每个请求都会运行一次代码。

99%的情况下这是一个坏主意 - 数据库初始化代码应该放在迁移中 - 但有一些用例你不能避免晚初始化,上述注意事项是可以接受的。


5
如果您需要在启动代码中访问数据库,这是一个不错的解决方案。一个简单的方法使它只运行一次是让 my_receiver 函数从 connection_created 信号中断开连接,具体做法是在 my_receiver 函数中添加以下内容:connection_created.disconnect(my_receiver) - alan

2
如果想在运行服务器时只打印一次“hello world”,请将print ("hello world")放在StartupMiddleware类外部。
from django.core.exceptions import MiddlewareNotUsed
from django.conf import settings

class StartupMiddleware(object):
    def __init__(self):
        #print "Hello world"
        raise MiddlewareNotUsed('Startup complete')

print "Hello world"

4
嗨,Oscar!在SO上,我们更喜欢答案包含英文解释,而不仅仅是代码。你能简要说明一下你的代码如何修复问题以及为什么吗? - Max von Hippel

0

我使用了这里的被接受的解决方案,它检查是否作为服务器运行,而不是执行其他managy.py命令,例如migrate

apps.py:

from .tasks import tasks

class myAppConfig(AppConfig):
    ...

    def ready(self, *args, **kwargs):
        is_manage_py = any(arg.casefold().endswith("manage.py") for arg in sys.argv)
        is_runserver = any(arg.casefold() == "runserver" for arg in sys.argv)

        if (is_manage_py and is_runserver) or (not is_manage_py):
            tasks.is_running_as_server = True

既然在开发模式下,即使不使用参数--noreload,它仍会被执行两次,因此我添加了一个标志以在作为服务器运行时触发,并将我的启动代码放在urls.py中,该文件仅被调用一次。

tasks.py:

class tasks():
    is_running_as_server = False

    def runtask(msg):
        print(msg)

urls.py:

from . import tasks

task1 = tasks.tasks()

if task1.is_running_as_server:
    task1.runtask('This should print once and only when running as a server')

因此,总结一下,我正在利用AppConfig中的read()函数来读取参数并知道代码如何执行。但是,在开发模式下,ready()函数会运行两次,一次为服务器提供服务,另一次为在代码更改时重新加载服务器,而urls.py仅在服务器上执行一次。因此,在我的解决方案中,我将这两个组合起来,以便仅在将代码作为服务器执行时运行我的任务一次。

0
在我的情况下,我使用Django来托管一个网站,并使用Heroku。我在Heroku上使用1个dyno(就像1个容器),而这个dyno创建了两个workers。 我想在上面运行一个discord机器人。我尝试了此页面上的所有方法,但它们都无效。
因为这是一个部署,所以不应该使用manage.py。相反,它使用gunicorn,我不知道如何添加--noreload参数。 每个worker只运行一次wsgi.py,因此每个代码将运行两次。而且两个workers的本地环境是相同的。
但我注意到一件事,每次Heroku部署时,它都使用相同的pid worker。所以我只需要...
if not sys.argv[1] in ["makemigrations", "migrate"]: # Prevent execute in some manage command
    if os.getpid() == 1: # You should check which pid Heroku will use and choose one.
        code_I_want_excute_once_only()

我不确定pid在未来是否会改变,希望它能永远保持不变。如果您有更好的方法来检查是哪个工作进程,请告诉我。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接