Django源码分析-命令行源码解读(一)

1.Django测试环境搭建和调试

  • 下载django源码(自行在网上查看下载)
  • 在django源码的bin目录下django-admin.py文件是一个命令行工具,用于执行与Django项目相关的各种任务。如: 
  1. 创建django项目
    django-admin.py startproject projectname
    
  2. 创建django应用
    python manage.py startapp appname
    
  3. 执行数据库迁移
    python manage.py makemigrations
    python manage.py migrate
    
  •  可通过配置参数进行对脚本断点调试,以pycharm为例:
  • 设置完成后就可以进行断点调试啦

2.Django重要命令源码分析

2.1.startproject命令追踪

  • startproject命令的核心代码在django源码的core/management/commands/startproject.py中,核心函数就是handle函数。
    from django.core.checks.security.base import SECRET_KEY_INSECURE_PREFIX
    from django.core.management.templates import TemplateCommand
    
    from ..utils import get_random_secret_key
    
    
    class Command(TemplateCommand):
        help = (
            "Creates a Django project directory structure for the given project "
            "name in the current directory or optionally in the given directory."
        )
        missing_args_message = "You must provide a project name."
    
        def handle(self, **options):
            project_name = options.pop('name')
            target = options.pop('directory')
    
            # Create a random SECRET_KEY to put it in the main settings.
            options['secret_key'] = SECRET_KEY_INSECURE_PREFIX + get_random_secret_key()
    
            super().handle('project', project_name, target, **options)
    

    该handle函数的具体实现逻辑是在父类TemplateCommand的handle,位于django源码的core/management/commands/templates.py中。

  •  def handle(self, app_or_project, name, target=None, **options):
            ....
    
            # 创建项目目录
            if target is None:
                top_dir = os.path.join(os.getcwd(), name)
                try:
                    os.makedirs(top_dir)
                except FileExistsError:
                    raise CommandError("'%s' already exists" % top_dir)
                except OSError as e:
                    raise CommandError(e)
            else:
                if app_or_project == 'app':
                    self.validate_name(os.path.basename(target), 'directory')
                top_dir = os.path.abspath(os.path.expanduser(target))
                if not os.path.exists(top_dir):
                    raise CommandError("Destination directory '%s' does not "
                                       "exist, please create it first." % top_dir)
    
            extensions = tuple(handle_extensions(options['extensions']))
            extra_files = []
            for file in options['files']:
                extra_files.extend(map(lambda x: x.strip(), file.split(',')))
            if self.verbosity >= 2:
                self.stdout.write(
                    'Rendering %s template files with extensions: %s'
                    % (app_or_project, ', '.join(extensions))
                )
                self.stdout.write(
                    'Rendering %s template files with filenames: %s'
                    % (app_or_project, ', '.join(extra_files))
                )
            # 设置一些用于模板渲染的基本变量
            base_name = '%s_name' % app_or_project
            base_subdir = '%s_template' % app_or_project
            base_directory = '%s_directory' % app_or_project
            camel_case_name = 'camel_case_%s_name' % app_or_project
            camel_case_value = ''.join(x for x in name.title() if x != '_')
            # 创建一个 Context 对象,将各种信息传递给模板引擎
            context = Context({
                **options,
                base_name: name,
                base_directory: top_dir,
                camel_case_name: camel_case_value,
                'docs_version': get_docs_version(),
                'django_version': django.__version__,
            }, autoescape=False)
    
            # 配置一个虚拟的Django settings环境,以便在模板渲染时使用。
            if not settings.configured:
                settings.configure()
                django.setup()
            # 调用 handle_template 方法获取模板目录的路径
            template_dir = self.handle_template(options['template'],
                                                base_subdir)
            prefix_length = len(template_dir) + 1
            # 对模板目录进行遍历,逐个处理目录和文件,进行模板渲染或复制操作。
            for root, dirs, files in os.walk(template_dir):
    
                path_rest = root[prefix_length:]
                relative_dir = path_rest.replace(base_name, name)
                if relative_dir:
                    target_dir = os.path.join(top_dir, relative_dir)
                    os.makedirs(target_dir, exist_ok=True)
    
                for dirname in dirs[:]:
                    if dirname.startswith('.') or dirname == '__pycache__':
                        dirs.remove(dirname)
                # 在目标目录中创建相应的目录结构,并将模板文件进行渲染或复制到目标目录
                for filename in files:
                    if filename.endswith(('.pyo', '.pyc', '.py.class')):
                        # Ignore some files as they cause various breakages.
                        continue
                    old_path = os.path.join(root, filename)
                    new_path = os.path.join(
                        top_dir, relative_dir, filename.replace(base_name, name)
                    )
                    for old_suffix, new_suffix in self.rewrite_template_suffixes:
                        if new_path.endswith(old_suffix):
                            new_path = new_path[:-len(old_suffix)] + new_suffix
                            break  # Only rewrite once
    
                    if os.path.exists(new_path):
                        raise CommandError(
                            "%s already exists. Overlaying %s %s into an existing "
                            "directory won't replace conflicting files." % (
                                new_path, self.a_or_an, app_or_project,
                            )
                        )
    
                    # 选取python文件作为Django的模板文件 进行渲染或复制到目标目录
                    if new_path.endswith(extensions) or filename in extra_files:
                        with open(old_path, encoding='utf-8') as template_file:
                            content = template_file.read()
                        template = Engine().from_string(content)
                        content = template.render(context)
                        with open(new_path, 'w', encoding='utf-8') as new_file:
                            new_file.write(content)
                    else:
                        shutil.copyfile(old_path, new_path)
    
                    if self.verbosity >= 2:
                        self.stdout.write('Creating %s' % new_path)
                    try:
                         # 更新文件权限
                        shutil.copymode(old_path, new_path)
                        self.make_writeable(new_path)
                    except OSError:
                        self.stderr.write(
                            "Notice: Couldn't set permission bits on %s. You're "
                            "probably using an uncommon filesystem setup. No "
                            "problem." % new_path, self.style.NOTICE)
            # 如果存在需要清理的临时文件路径 (self.paths_to_remove),则进行清理。   
            if self.paths_to_remove:
                if self.verbosity >= 2:
                    self.stdout.write('Cleaning up temporary files.')
                for path_to_remove in self.paths_to_remove:
                    if os.path.isfile(path_to_remove):
                        os.remove(path_to_remove)
                    else:
                        shutil.rmtree(path_to_remove)

2.2 .shell命令追踪

  • django-admin shell命令用于启动一个交互式Python shell,并加载Django项目的环境.以便你可以在该环境中执行与项目相关的Python代码。
  • shell命令的核心代码在django源码的core/management/commands/shell.py中,核心函数就是handle方法。Command 类的 handle 方法负责创建并启动Python解释器,并将项目环境加载到解释器中。
    # 定义IPython、bpython和Python三种不同的交互式shell方法:
    def ipython(self, options):
        from IPython import start_ipython
        start_ipython(argv=[])
    
    def bpython(self, options):
        import bpython
        bpython.embed()
    
    def python(self, options):
        import code 
        # ... (后续代码中有关于启动Python交互式shell的实现)
    
    
    def handle(self, **options):
            # 判断是否有给定的命令需要执行
            if options['command']:
                exec(options['command'], globals())
                return
    
            # 判断是否有标准输入可以读取,并执行其内容
            if sys.platform != 'win32' and not sys.stdin.isatty() and select.select([sys.stdin], [], [], 0)[0]:
                exec(sys.stdin.read(), globals())
                return
            # 如果没有给定的命令,也没有标准输入,尝试依次使用可用的交互式shell
            available_shells = [options['interface']] if options['interface'] else self.shells
    
            for shell in available_shells:
                try:
                    return getattr(self, shell)(options)
                except ImportError:
                    pass
            raise CommandError("Couldn't import {} interface.".format(shell))

2.3.MigrationRecorder类详解

  • MigrationRecorder 类是 Django 中用于记录数据库迁移状态的工具类。它位于 django.db.migrations.recorder 模块中。该类的主要作用是用于处理在数据库中存储迁移记录。主要功能包括检查并创建 django_migrations 表,记录已应用的迁移以及删除所有迁移记录。
    from django.apps.registry import Apps
    from django.db import DatabaseError, models
    from django.utils.functional import classproperty
    from django.utils.timezone import now
    
    from .exceptions import MigrationSchemaMissing
    
    
    class MigrationRecorder:
         # _migration_class 用于存储 Migration 类的实例,该类是在需要时动态创建的。由于该类直接依赖于数据库表结构,需要在应用准备好后进行创建。
        _migration_class = None
    
        @classproperty
        # 通过 classproperty 装饰器将 Migration 类定义为类属性,以便在需要时延迟加载
        def Migration(cls):
            """
            Lazy load to avoid AppRegistryNotReady if installed apps import
            MigrationRecorder.
            """
            if cls._migration_class is None:
                class Migration(models.Model):
                    app = models.CharField(max_length=255) # 应用名称
                    name = models.CharField(max_length=255) # 迁移名称
                    applied = models.DateTimeField(default=now) # 应用时间
    
                    class Meta:
                        apps = Apps()
                        app_label = 'migrations'
                        db_table = 'django_migrations'
    
                    def __str__(self):
                        return 'Migration %s for %s' % (self.name, self.app)
    
                cls._migration_class = Migration
            return cls._migration_class
    
        def __init__(self, connection):
            self.connection = connection
    
        @property
        # 返回一个 Migration 查询集,用于对 django_migrations 表进行查询操作
        def migration_qs(self):
            return self.Migration.objects.using(self.connection.alias)
        
        # 检查 django_migrations 表是否存在。
        def has_table(self):
            """Return True if the django_migrations table exists."""
            with self.connection.cursor() as cursor:
                tables = self.connection.introspection.table_names(cursor)
            return self.Migration._meta.db_table in tables
        
        # 确保 django_migrations 表存在,并具有正确的模型结构。如果表不存在,则使用数据库模式编辑            
          器创建表。
        def ensure_schema(self):
            """Ensure the table exists and has the correct schema."""
            # If the table's there, that's fine - we've never changed its schema
            # in the codebase.
            if self.has_table():
                return
            # Make the table
            try:
                with self.connection.schema_editor() as editor:
                    editor.create_model(self.Migration)
            except DatabaseError as exc:
                raise MigrationSchemaMissing("Unable to create the django_migrations table (%s)" % exc)
    
        #  返回一个字典,将已应用的迁移的 (app_name, migration_name) 映射到相应的 Migration 实例
        def applied_migrations(self):
            """
            Return a dict mapping (app_name, migration_name) to Migration instances
            for all applied migrations.
            """
            if self.has_table():
                return {(migration.app, migration.name): migration for migration in self.migration_qs}
            else:
                # If the django_migrations table doesn't exist, then no migrations
                # are applied.
                return {}
        #  记录应用了指定迁移。
        def record_applied(self, app, name):
            """Record that a migration was applied."""
            self.ensure_schema()
            self.migration_qs.create(app=app, name=name)
        
        # 记录取消应用了指定迁移。
        def record_unapplied(self, app, name):
            """Record that a migration was unapplied."""
            self.ensure_schema()
            self.migration_qs.filter(app=app, name=name).delete()
        
        # 删除所有迁移记录,通常在测试迁移时使用。
        def flush(self):
            """Delete all migration records. Useful for testing migrations."""
            self.migration_qs.all().delete()
    

2.4.MigrationGraph类详解

MigrationGraph 类是 Django 中用于管理迁移依赖关系的类。它负责维护和查询整个迁移图,帮助 Django 确定迁移的执行顺序和依赖关系。它位于 django.db.migrations.graph模块中。

class MigrationGraph:
   
    def __init__(self):
        self.node_map = {} # 将节点标识映射到节点实例的字典。如:'k1':Node(k1)
        self.nodes = {} #  将节点标识映射到迁移实例的字典
    #  将指定迁移节点添加到图中。
    def add_node(self, key, migration):
        assert key not in self.node_map
        node = Node(key)
        self.node_map[key] = node
        self.nodes[key] = migration
    # 将虚拟节点添加到图中。
    def add_dummy_node(self, key, origin, error_message):
        node = DummyNode(key, origin, error_message)
        self.node_map[key] = node
        self.nodes[key] = None
    # 添加从 parent 节点到 child 节点的依赖关系。
    def add_dependency(self, migration, child, parent, skip_validation=False):
        """
        This may create dummy nodes if they don't yet exist. If
        `skip_validation=True`, validate_consistency() should be called
        afterwards.
        """
        if child not in self.nodes:
            error_message = (
                "Migration %s dependencies reference nonexistent"
                " child node %r" % (migration, child)
            )
            self.add_dummy_node(child, migration, error_message)
        if parent not in self.nodes:
            error_message = (
                "Migration %s dependencies reference nonexistent"
                " parent node %r" % (migration, parent)
            )
            self.add_dummy_node(parent, migration, error_message)
        self.node_map[child].add_parent(self.node_map[parent])
        self.node_map[parent].add_child(self.node_map[child])
        if not skip_validation:
            self.validate_consistency()
     # 移除被替换的节点,更新依赖关系。
    def remove_replaced_nodes(self, replacement, replaced):
        """
        Remove each of the `replaced` nodes (when they exist). Any
        dependencies that were referencing them are changed to reference the
        `replacement` node instead.
        """
        # Cast list of replaced keys to set to speed up lookup later.
        replaced = set(replaced)
        try:
            replacement_node = self.node_map[replacement]
        except KeyError as err:
            raise NodeNotFoundError(
                "Unable to find replacement node %r. It was either never added"
                " to the migration graph, or has been removed." % (replacement,),
                replacement
            ) from err
        for replaced_key in replaced:
            self.nodes.pop(replaced_key, None)
            replaced_node = self.node_map.pop(replaced_key, None)
            if replaced_node:
                for child in replaced_node.children:
                    child.parents.remove(replaced_node)
                    # We don't want to create dependencies between the replaced
                    # node and the replacement node as this would lead to
                    # self-referencing on the replacement node at a later iteration.
                    if child.key not in replaced:
                        replacement_node.add_child(child)
                        child.add_parent(replacement_node)
                for parent in replaced_node.parents:
                    parent.children.remove(replaced_node)
                    # Again, to avoid self-referencing.
                    if parent.key not in replaced:
                        replacement_node.add_parent(parent)
                        parent.add_child(replacement_node)
     # 移除替代节点,更新依赖关系。
    def remove_replacement_node(self, replacement, replaced):
        """
        The inverse operation to `remove_replaced_nodes`. Almost. Remove the
        replacement node `replacement` and remap its child nodes to `replaced`
        - the list of nodes it would have replaced. Don't remap its parent
        nodes as they are expected to be correct already.
        """
        self.nodes.pop(replacement, None)
        try:
            replacement_node = self.node_map.pop(replacement)
        except KeyError as err:
            raise NodeNotFoundError(
                "Unable to remove replacement node %r. It was either never added"
                " to the migration graph, or has been removed already." % (replacement,),
                replacement
            ) from err
        replaced_nodes = set()
        replaced_nodes_parents = set()
        for key in replaced:
            replaced_node = self.node_map.get(key)
            if replaced_node:
                replaced_nodes.add(replaced_node)
                replaced_nodes_parents |= replaced_node.parents
        # We're only interested in the latest replaced node, so filter out
        # replaced nodes that are parents of other replaced nodes.
        replaced_nodes -= replaced_nodes_parents
        for child in replacement_node.children:
            child.parents.remove(replacement_node)
            for replaced_node in replaced_nodes:
                replaced_node.add_child(child)
                child.add_parent(replaced_node)
        for parent in replacement_node.parents:
            parent.children.remove(replacement_node)
            # NOTE: There is no need to remap parent dependencies as we can
            # assume the replaced nodes already have the correct ancestry.
     # 确保图中没有虚拟节点
    def validate_consistency(self):
        """Ensure there are no dummy nodes remaining in the graph."""
        [n.raise_error() for n in self.node_map.values() if isinstance(n, DummyNode)]
    # 获取从根节点到目标节点的迁移计划。
    def forwards_plan(self, target):
        """
        Given a node, return a list of which previous nodes (dependencies) must
        be applied, ending with the node itself. This is the list you would
        follow if applying the migrations to a database.
        """
        if target not in self.nodes:
            raise NodeNotFoundError("Node %r not a valid node" % (target,), target)
        return self.iterative_dfs(self.node_map[target])
    # 获取从目标节点到根节点的迁移计划。
    def backwards_plan(self, target):
        """
        Given a node, return a list of which dependent nodes (dependencies)
        must be unapplied, ending with the node itself. This is the list you
        would follow if removing the migrations from a database.
        """
        if target not in self.nodes:
            raise NodeNotFoundError("Node %r not a valid node" % (target,), target)
        return self.iterative_dfs(self.node_map[target], forwards=False)
    # 迭代深度优先搜索,查找依赖关系。
    def iterative_dfs(self, start, forwards=True):
        """Iterative depth-first search for finding dependencies."""
        visited = []
        visited_set = set()
        stack = [(start, False)]
        while stack:
            node, processed = stack.pop()
            if node in visited_set:
                pass
            elif processed:
                visited_set.add(node)
                visited.append(node.key)
            else:
                stack.append((node, True))
                stack += [(n, False) for n in sorted(node.parents if forwards else node.children)]
        return visited
    # 获取所有根节点
    def root_nodes(self, app=None):
        """
        Return all root nodes - that is, nodes with no dependencies inside
        their app. These are the starting point for an app.
        """
        roots = set()
        for node in self.nodes:
            if all(key[0] != node[0] for key in self.node_map[node].parents) and (not app or app == node[0]):
                roots.add(node)
        return sorted(roots)
    # 获取所有叶节点
    def leaf_nodes(self, app=None):
        """
        Return all leaf nodes - that is, nodes with no dependents in their app.
        These are the "most current" version of an app's schema.
        Having more than one per app is technically an error, but one that
        gets handled further up, in the interactive command - it's usually the
        result of a VCS merge and needs some user input.
        """
        leaves = set()
        for node in self.nodes:
            if all(key[0] != node[0] for key in self.node_map[node].children) and (not app or app == node[0]):
                leaves.add(node)
        return sorted(leaves)
    # 检查图是否包含循环依赖
    def ensure_not_cyclic(self):
        # Algo from GvR:
        # https://neopythonic.blogspot.com/2009/01/detecting-cycles-in-directed-graph.html
        todo = set(self.nodes)
        while todo:
            node = todo.pop()
            stack = [node]
            while stack:
                top = stack[-1]
                for child in self.node_map[top].children:
                    # Use child.key instead of child to speed up the frequent
                    # hashing.
                    node = child.key
                    if node in stack:
                        cycle = stack[stack.index(node):]
                        raise CircularDependencyError(", ".join("%s.%s" % n for n in cycle))
                    if node in todo:
                        stack.append(node)
                        todo.remove(node)
                        break
                else:
                    node = stack.pop()

    def __str__(self):
        return 'Graph: %s nodes, %s edges' % self._nodes_and_edges()

    def __repr__(self):
        nodes, edges = self._nodes_and_edges()
        return '<%s: nodes=%s, edges=%s>' % (self.__class__.__name__, nodes, edges)

    def _nodes_and_edges(self):
        return len(self.nodes), sum(len(node.parents) for node in self.node_map.values())

    def _generate_plan(self, nodes, at_end):
        plan = []
        for node in nodes:
            for migration in self.forwards_plan(node):
                if migration not in plan and (at_end or migration not in nodes):
                    plan.append(migration)
        return plan
    # 生成迁移计划的 ProjectState。
    def make_state(self, nodes=None, at_end=True, real_apps=None):
        """
        Given a migration node or nodes, return a complete ProjectState for it.
        If at_end is False, return the state before the migration has run.
        If nodes is not provided, return the overall most current project state.
        """
        if nodes is None:
            nodes = list(self.leaf_nodes())
        if not nodes:
            return ProjectState()
        if not isinstance(nodes[0], tuple):
            nodes = [nodes]
        plan = self._generate_plan(nodes, at_end)
        project_state = ProjectState(real_apps=real_apps)
        for node in plan:
            project_state = self.nodes[node].mutate_state(project_state, preserve=False)
        return project_state

    def __contains__(self, node):
        return node in self.nodes


2.5.MigrationLoader类详解

MigrationLoader 类是 Django 中用于加载和管理数据库迁移的类。它允许Django了解项目中的所有迁移,以及它们之间的依赖关系。

import pkgutil
import sys
from importlib import import_module, reload

from django.apps import apps
from django.conf import settings
from django.db.migrations.graph import MigrationGraph
from django.db.migrations.recorder import MigrationRecorder

from .exceptions import (
    AmbiguityError, BadMigrationError, InconsistentMigrationHistory,
    NodeNotFoundError,
)

MIGRATIONS_MODULE_NAME = 'migrations'


class MigrationLoader:
    """
    Load migration files from disk and their status from the database.

    Migration files are expected to live in the "migrations" directory of
    an app. Their names are entirely unimportant from a code perspective,
    but will probably follow the 1234_name.py convention.

    On initialization, this class will scan those directories, and open and
    read the Python files, looking for a class called Migration, which should
    inherit from django.db.migrations.Migration. See
    django.db.migrations.migration for what that looks like.

    Some migrations will be marked as "replacing" another set of migrations.
    These are loaded into a separate set of migrations away from the main ones.
    If all the migrations they replace are either unapplied or missing from
    disk, then they are injected into the main set, replacing the named migrations.
    Any dependency pointers to the replaced migrations are re-pointed to the
    new migration.

    This does mean that this class MUST also talk to the database as well as
    to disk, but this is probably fine. We're already not just operating
    in memory.
    """

    def __init__(
        self, connection, load=True, ignore_no_migrations=False,
        replace_migrations=True,
    ):
        self.connection = connection
        self.disk_migrations = None
        self.applied_migrations = None
        self.ignore_no_migrations = ignore_no_migrations
        self.replace_migrations = replace_migrations
        if load:
            self.build_graph()
    # 确定指定应用程序标签的迁移模块的路径。
    @classmethod
    def migrations_module(cls, app_label):
        """
        Return the path to the migrations module for the specified app_label
        and a boolean indicating if the module is specified in
        settings.MIGRATION_MODULE.
        """
        if app_label in settings.MIGRATION_MODULES:
            return settings.MIGRATION_MODULES[app_label], True
        else:
            app_package_name = apps.get_app_config(app_label).name
            return '%s.%s' % (app_package_name, MIGRATIONS_MODULE_NAME), False
    # 从磁盘加载所有INSTALLED_APPS的迁移。
    def load_disk(self):
        """Load the migrations from all INSTALLED_APPS from disk."""
        self.disk_migrations = {}
        self.unmigrated_apps = set()
        self.migrated_apps = set()
        for app_config in apps.get_app_configs():
            # Get the migrations module directory
            module_name, explicit = self.migrations_module(app_config.label)
            if module_name is None:
                self.unmigrated_apps.add(app_config.label)
                continue
            was_loaded = module_name in sys.modules
            try:
                module = import_module(module_name)
            except ModuleNotFoundError as e:
                if (
                    (explicit and self.ignore_no_migrations) or
                    (not explicit and MIGRATIONS_MODULE_NAME in e.name.split('.'))
                ):
                    self.unmigrated_apps.add(app_config.label)
                    continue
                raise
            else:
                # Module is not a package (e.g. migrations.py).
                if not hasattr(module, '__path__'):
                    self.unmigrated_apps.add(app_config.label)
                    continue
                # Empty directories are namespaces. Namespace packages have no
                # __file__ and don't use a list for __path__. See
                # https://docs.python.org/3/reference/import.html#namespace-packages
                if (
                    getattr(module, '__file__', None) is None and
                    not isinstance(module.__path__, list)
                ):
                    self.unmigrated_apps.add(app_config.label)
                    continue
                # Force a reload if it's already loaded (tests need this)
                if was_loaded:
                    reload(module)
            self.migrated_apps.add(app_config.label)
            migration_names = {
                name for _, name, is_pkg in pkgutil.iter_modules(module.__path__)
                if not is_pkg and name[0] not in '_~'
            }
            # Load migrations
            for migration_name in migration_names:
                migration_path = '%s.%s' % (module_name, migration_name)
                try:
                    migration_module = import_module(migration_path)
                except ImportError as e:
                    if 'bad magic number' in str(e):
                        raise ImportError(
                            "Couldn't import %r as it appears to be a stale "
                            ".pyc file." % migration_path
                        ) from e
                    else:
                        raise
                if not hasattr(migration_module, "Migration"):
                    raise BadMigrationError(
                        "Migration %s in app %s has no Migration class" % (migration_name, app_config.label)
                    )
                self.disk_migrations[app_config.label, migration_name] = migration_module.Migration(
                    migration_name,
                    app_config.label,
                )
    # 通过其全名检索迁移。
    def get_migration(self, app_label, name_prefix):
        """Return the named migration or raise NodeNotFoundError."""
        return self.graph.nodes[app_label, name_prefix]
    # 通过名称前缀检索迁移
    def get_migration_by_prefix(self, app_label, name_prefix):
        """
        Return the migration(s) which match the given app label and name_prefix.
        """
        # Do the search
        results = []
        for migration_app_label, migration_name in self.disk_migrations:
            if migration_app_label == app_label and migration_name.startswith(name_prefix):
                results.append((migration_app_label, migration_name))
        if len(results) > 1:
            raise AmbiguityError(
                "There is more than one migration for '%s' with the prefix '%s'" % (app_label, name_prefix)
            )
        elif not results:
            raise KeyError("There no migrations for '%s' with the prefix '%s'" % (app_label, name_prefix))
        else:
            return self.disk_migrations[results[0]]
    # 检查并验证迁移依赖关系,处理__first__和__latest__等特殊情况。
    def check_key(self, key, current_app):
        if (key[1] != "__first__" and key[1] != "__latest__") or key in self.graph:
            return key
        # Special-case __first__, which means "the first migration" for
        # migrated apps, and is ignored for unmigrated apps. It allows
        # makemigrations to declare dependencies on apps before they even have
        # migrations.
        if key[0] == current_app:
            # Ignore __first__ references to the same app (#22325)
            return
        if key[0] in self.unmigrated_apps:
            # This app isn't migrated, but something depends on it.
            # The models will get auto-added into the state, though
            # so we're fine.
            return
        if key[0] in self.migrated_apps:
            try:
                if key[1] == "__first__":
                    return self.graph.root_nodes(key[0])[0]
                else:  # "__latest__"
                    return self.graph.leaf_nodes(key[0])[0]
            except IndexError:
                if self.ignore_no_migrations:
                    return None
                else:
                    raise ValueError("Dependency on app with no migrations: %s" % key[0])
        raise ValueError("Dependency on unknown app: %s" % key[0])
    # 向迁移图添加内部依赖关系
    def add_internal_dependencies(self, key, migration):
        """
        Internal dependencies need to be added first to ensure `__first__`
        dependencies find the correct root node.
        """
        for parent in migration.dependencies:
            # Ignore __first__ references to the same app.
            if parent[0] == key[0] and parent[1] != '__first__':
                self.graph.add_dependency(migration, key, parent, skip_validation=True)
    # 向迁移图添加外部依赖关系
    def add_external_dependencies(self, key, migration):
        for parent in migration.dependencies:
            # Skip internal dependencies
            if key[0] == parent[0]:
                continue
            parent = self.check_key(parent, key[0])
            if parent is not None:
                self.graph.add_dependency(migration, key, parent, skip_validation=True)
        for child in migration.run_before:
            child = self.check_key(child, key[0])
            if child is not None:
                self.graph.add_dependency(migration, child, key, skip_validation=True)
    # 使用磁盘和数据库数据构建迁移依赖关系图。
    def build_graph(self):
        """
        Build a migration dependency graph using both the disk and database.
        You'll need to rebuild the graph if you apply migrations. This isn't
        usually a problem as generally migration stuff runs in a one-shot process.
        """
        # Load disk data
        self.load_disk()
        # Load database data
        if self.connection is None:
            self.applied_migrations = {}
        else:
            recorder = MigrationRecorder(self.connection)
            self.applied_migrations = recorder.applied_migrations()
        # To start, populate the migration graph with nodes for ALL migrations
        # and their dependencies. Also make note of replacing migrations at this step.
        self.graph = MigrationGraph()
        self.replacements = {}
        for key, migration in self.disk_migrations.items():
            self.graph.add_node(key, migration)
            # Replacing migrations.
            if migration.replaces:
                self.replacements[key] = migration
        for key, migration in self.disk_migrations.items():
            # Internal (same app) dependencies.
            self.add_internal_dependencies(key, migration)
        # Add external dependencies now that the internal ones have been resolved.
        for key, migration in self.disk_migrations.items():
            self.add_external_dependencies(key, migration)
        # Carry out replacements where possible and if enabled.
        if self.replace_migrations:
            for key, migration in self.replacements.items():
                # Get applied status of each of this migration's replacement
                # targets.
                applied_statuses = [(target in self.applied_migrations) for target in migration.replaces]
                # The replacing migration is only marked as applied if all of
                # its replacement targets are.
                if all(applied_statuses):
                    self.applied_migrations[key] = migration
                else:
                    self.applied_migrations.pop(key, None)
                # A replacing migration can be used if either all or none of
                # its replacement targets have been applied.
                if all(applied_statuses) or (not any(applied_statuses)):
                    self.graph.remove_replaced_nodes(key, migration.replaces)
                else:
                    # This replacing migration cannot be used because it is
                    # partially applied. Remove it from the graph and remap
                    # dependencies to it (#25945).
                    self.graph.remove_replacement_node(key, migration.replaces)
        # Ensure the graph is consistent.
        try:
            self.graph.validate_consistency()
        except NodeNotFoundError as exc:
            # Check if the missing node could have been replaced by any squash
            # migration but wasn't because the squash migration was partially
            # applied before. In that case raise a more understandable exception
            # (#23556).
            # Get reverse replacements.
            reverse_replacements = {}
            for key, migration in self.replacements.items():
                for replaced in migration.replaces:
                    reverse_replacements.setdefault(replaced, set()).add(key)
            # Try to reraise exception with more detail.
            if exc.node in reverse_replacements:
                candidates = reverse_replacements.get(exc.node, set())
                is_replaced = any(candidate in self.graph.nodes for candidate in candidates)
                if not is_replaced:
                    tries = ', '.join('%s.%s' % c for c in candidates)
                    raise NodeNotFoundError(
                        "Migration {0} depends on nonexistent node ('{1}', '{2}'). "
                        "Django tried to replace migration {1}.{2} with any of [{3}] "
                        "but wasn't able to because some of the replaced migrations "
                        "are already applied.".format(
                            exc.origin, exc.node[0], exc.node[1], tries
                        ),
                        exc.node
                    ) from exc
            raise
        self.graph.ensure_not_cyclic()
    # 检查存在不一致的迁移历史,即应用的迁移具有未应用的依赖关系。
    def check_consistent_history(self, connection):
        """
        Raise InconsistentMigrationHistory if any applied migrations have
        unapplied dependencies.
        """
        recorder = MigrationRecorder(connection)
        applied = recorder.applied_migrations()
        for migration in applied:
            # If the migration is unknown, skip it.
            if migration not in self.graph.nodes:
                continue
            for parent in self.graph.node_map[migration].parents:
                if parent not in applied:
                    # Skip unapplied squashed migrations that have all of their
                    # `replaces` applied.
                    if parent in self.replacements:
                        if all(m in applied for m in self.replacements[parent].replaces):
                            continue
                    raise InconsistentMigrationHistory(
                        "Migration {}.{} is applied before its dependency "
                        "{}.{} on database '{}'.".format(
                            migration[0], migration[1], parent[0], parent[1],
                            connection.alias,
                        )
                    )
    # 检测加载的图中存在冲突的地方,即应用程序具有多个叶子迁移。
    def detect_conflicts(self):
        """
        Look through the loaded graph and detect any conflicts - apps
        with more than one leaf migration. Return a dict of the app labels
        that conflict with the migration names that conflict.
        """
        seen_apps = {}
        conflicting_apps = set()
        for app_label, migration_name in self.graph.leaf_nodes():
            if app_label in seen_apps:
                conflicting_apps.add(app_label)
            seen_apps.setdefault(app_label, set()).add(migration_name)
        return {app_label: sorted(seen_apps[app_label]) for app_label in conflicting_apps}
    # 返回表示加载的迁移最新状态的ProjectState对象。
    def project_state(self, nodes=None, at_end=True):
        """
        Return a ProjectState object representing the most recent state
        that the loaded migrations represent.

        See graph.make_state() for the meaning of "nodes" and "at_end".
        """
        return self.graph.make_state(nodes=nodes, at_end=at_end, real_apps=list(self.unmigrated_apps))
    # 接受迁移计划并返回表示计划的已收集SQL语句的列表
    def collect_sql(self, plan):
        """
        Take a migration plan and return a list of collected SQL statements
        that represent the best-efforts version of that plan.
        """
        statements = []
        state = None
        for migration, backwards in plan:
            with self.connection.schema_editor(collect_sql=True, atomic=migration.atomic) as schema_editor:
                if state is None:
                    state = self.project_state((migration.app_label, migration.name), at_end=False)
                if not backwards:
                    state = migration.apply(state, schema_editor, collect_sql=True)
                else:
                    state = migration.unapply(state, schema_editor, collect_sql=True)
            statements.extend(schema_editor.collected_sql)
        return statements

2.6.makemigrations命令追踪

  • makemigrations 是 Django 中用于生成数据库迁移文件的管理命令。核心代码在django源码的core/management/commands/makemigrations .py中。
      def handle(self, *app_labels, **options):
            self.verbosity = options['verbosity']  # 详细程度
            self.interactive = options['interactive']  # 交互模式
            self.dry_run = options['dry_run']  # 模拟运行
            self.merge = options['merge']  # 合并冲突
            self.empty = options['empty']  # 创建空迁移
            self.migration_name = options['name']  # 指定迁移名称
            if self.migration_name and not self.migration_name.isidentifier():
                raise CommandError('The migration name must be a valid Python identifier.')
            self.include_header = options['include_header']  # 包含头部信息
            check_changes = options['check_changes']  # 检查是否有更改
    
            # 检查用户指定的应用是否存在
            app_labels = set(app_labels)
            has_bad_labels = False
            for app_label in app_labels:
                try:
                    apps.get_app_config(app_label)
                except LookupError as err:
                    self.stderr.write(str(err))
                    has_bad_labels = True
            if has_bad_labels:
                sys.exit(2)
    
            # 使用 MigrationLoader 加载当前的迁移图状态,忽略数据库中不存在的迁移
            loader = MigrationLoader(None, ignore_no_migrations=True)
    
            # 检查是否有已应用的迁移早于其依赖的情况,若存在则输出警告信息
            consistency_check_labels = {config.label for config in apps.get_app_configs()}
            # Non-default databases are only checked if database routers used.
            aliases_to_check = connections if settings.DATABASE_ROUTERS else [DEFAULT_DB_ALIAS]
            for alias in sorted(aliases_to_check):
                connection = connections[alias]
                if (connection.settings_dict['ENGINE'] != 'django.db.backends.dummy' and any(
                    # At least one model must be migrated to the database.
                    router.allow_migrate(connection.alias, app_label, model_name=model._meta.object_name)
                    for app_label in consistency_check_labels
                    for model in apps.get_app_config(app_label).get_models()
                )):
                    try:
                        loader.check_consistent_history(connection)
                    except OperationalError as error:
                        warnings.warn(
                            "Got an error checking a consistent migration history "
                            "performed for database connection '%s': %s"
                            % (alias, error),
                            RuntimeWarning,
                        )
            # 使用 loader.detect_conflicts() 检测是否存在冲突的迁移。
            conflicts = loader.detect_conflicts()
    
            # If app_labels is specified, filter out conflicting migrations for unspecified apps
            if app_labels:
                conflicts = {
                    app_label: conflict for app_label, conflict in conflicts.items()
                    if app_label in app_labels
                }
    
            if conflicts and not self.merge:
                name_str = "; ".join(
                    "%s in %s" % (", ".join(names), app)
                    for app, names in conflicts.items()
                )
                raise CommandError(
                    "Conflicting migrations detected; multiple leaf nodes in the "
                    "migration graph: (%s).\nTo fix them run "
                    "'python manage.py makemigrations --merge'" % name_str
                )
    
            # If they want to merge and there's nothing to merge, then politely exit
            if self.merge and not conflicts:
                self.stdout.write("No conflicts detected to merge.")
                return
    
            # 根据用户选择的交互模式,选择对应的问题生成器
            if self.merge and conflicts:
                return self.handle_merge(loader, conflicts)
    
            if self.interactive:
                questioner = InteractiveMigrationQuestioner(specified_apps=app_labels, dry_run=self.dry_run)
            else:
                questioner = NonInteractiveMigrationQuestioner(specified_apps=app_labels, dry_run=self.dry_run)
            # Set up autodetector
            autodetector = MigrationAutodetector(
                loader.project_state(),
                ProjectState.from_apps(apps),
                questioner,
            )
    
            # 如果用户指定了 --empty 选项,为每个应用程序创建一个空的迁移。
            if self.empty:
                if not app_labels:
                    raise CommandError("You must supply at least one app label when using --empty.")
                # Make a fake changes() result we can pass to arrange_for_graph
                changes = {
                    app: [Migration("custom", app)]
                    for app in app_labels
                }
                changes = autodetector.arrange_for_graph(
                    changes=changes,
                    graph=loader.graph,
                    migration_name=self.migration_name,
                )
                self.write_migration_files(changes)
                return
    
            # Detect changes
            changes = autodetector.changes(
                graph=loader.graph,
                trim_to_apps=app_labels or None,
                convert_apps=app_labels or None,
                migration_name=self.migration_name,
            )
    
            if not changes:
                # No changes? Tell them.
                if self.verbosity >= 1:
                    if app_labels:
                        if len(app_labels) == 1:
                            self.stdout.write("No changes detected in app '%s'" % app_labels.pop())
                        else:
                            self.stdout.write("No changes detected in apps '%s'" % ("', '".join(app_labels)))
                    else:
                        self.stdout.write("No changes detected")
            else:
                self.write_migration_files(changes)
                if check_changes:
                    sys.exit(1)

2.7.migrate命令源追踪

  • migrate 命令的处理逻辑,该命令用于执行数据库迁。核心代码在django源码的core/management/commands/migrate.py文件中。
        def handle(self, *args, **options):
            database = options['database']
            if not options['skip_checks']:
                self.check(databases=[database])
    
            self.verbosity = options['verbosity']
            self.interactive = options['interactive']
    
            # 遍历已安装的应用配置,导入每个应用的 management 模块,以注册调度事件
            for app_config in apps.get_app_configs():
                if module_has_submodule(app_config.module, "management"):
                    import_module('.management', app_config.name)
    
            # Get the database we're operating from
            connection = connections[database]
    
            # 通过数据库连接获取数据库,并调用 prepare_database 方法对数据库进行准备。
            connection.prepare_database()
            # 创建 MigrationExecutor 实例,用于处理数据库迁移。
            executor = MigrationExecutor(connection, self.migration_progress_callback)
    
            # 检查是否有已应用的迁移早于其依赖的情况
            executor.loader.check_consistent_history(connection)
    
            # 检测是否存在冲突的迁移。
            conflicts = executor.loader.detect_conflicts()
            if conflicts:
                name_str = "; ".join(
                    "%s in %s" % (", ".join(names), app)
                    for app, names in conflicts.items()
                )
                raise CommandError(
                    "Conflicting migrations detected; multiple leaf nodes in the "
                    "migration graph: (%s).\nTo fix them run "
                    "'python manage.py makemigrations --merge'" % name_str
                )
    
            # 根据命令行参数确定迁移目标,包括应用、迁移名称等
            run_syncdb = options['run_syncdb']
            target_app_labels_only = True
            if options['app_label']:
                # Validate app_label.
                app_label = options['app_label']
                try:
                    apps.get_app_config(app_label)
                except LookupError as err:
                    raise CommandError(str(err))
                if run_syncdb:
                    if app_label in executor.loader.migrated_apps:
                        raise CommandError("Can't use run_syncdb with app '%s' as it has migrations." % app_label)
                elif app_label not in executor.loader.migrated_apps:
                    raise CommandError("App '%s' does not have migrations." % app_label)
    
            if options['app_label'] and options['migration_name']:
                migration_name = options['migration_name']
                if migration_name == "zero":
                    targets = [(app_label, None)]
                else:
                    try:
                        migration = executor.loader.get_migration_by_prefix(app_label, migration_name)
                    except AmbiguityError:
                        raise CommandError(
                            "More than one migration matches '%s' in app '%s'. "
                            "Please be more specific." %
                            (migration_name, app_label)
                        )
                    except KeyError:
                        raise CommandError("Cannot find a migration matching '%s' from app '%s'." % (
                            migration_name, app_label))
                    targets = [(app_label, migration.name)]
                target_app_labels_only = False
            elif options['app_label']:
                targets = [key for key in executor.loader.graph.leaf_nodes() if key[0] == app_label]
            else:
                targets = executor.loader.graph.leaf_nodes()
            # 生成迁移计划
            plan = executor.migration_plan(targets)
            exit_dry = plan and options['check_unapplied']
    
            if options['plan']:
                self.stdout.write('Planned operations:', self.style.MIGRATE_LABEL)
                if not plan:
                    self.stdout.write('  No planned migration operations.')
                for migration, backwards in plan:
                    self.stdout.write(str(migration), self.style.MIGRATE_HEADING)
                    for operation in migration.operations:
                        message, is_error = self.describe_operation(operation, backwards)
                        style = self.style.WARNING if is_error else None
                        self.stdout.write('    ' + message, style)
                if exit_dry:
                    sys.exit(1)
                return
            if exit_dry:
                sys.exit(1)
    
            # At this point, ignore run_syncdb if there aren't any apps to sync.
            run_syncdb = options['run_syncdb'] and executor.loader.unmigrated_apps
            # Print some useful info
            if self.verbosity >= 1:
                self.stdout.write(self.style.MIGRATE_HEADING("Operations to perform:"))
                if run_syncdb:
                    if options['app_label']:
                        self.stdout.write(
                            self.style.MIGRATE_LABEL("  Synchronize unmigrated app: %s" % app_label)
                        )
                    else:
                        self.stdout.write(
                            self.style.MIGRATE_LABEL("  Synchronize unmigrated apps: ") +
                            (", ".join(sorted(executor.loader.unmigrated_apps)))
                        )
                if target_app_labels_only:
                    self.stdout.write(
                        self.style.MIGRATE_LABEL("  Apply all migrations: ") +
                        (", ".join(sorted({a for a, n in targets})) or "(none)")
                    )
                else:
                    if targets[0][1] is None:
                        self.stdout.write(
                            self.style.MIGRATE_LABEL('  Unapply all migrations: ') +
                            str(targets[0][0])
                        )
                    else:
                        self.stdout.write(self.style.MIGRATE_LABEL(
                            "  Target specific migration: ") + "%s, from %s"
                                          % (targets[0][1], targets[0][0])
                                          )
    
            pre_migrate_state = executor._create_project_state(with_applied_migrations=True)
            pre_migrate_apps = pre_migrate_state.apps
            emit_pre_migrate_signal(
                self.verbosity, self.interactive, connection.alias, apps=pre_migrate_apps, plan=plan,
            )
    
            # Run the syncdb phase.
            if run_syncdb:
                if self.verbosity >= 1:
                    self.stdout.write(self.style.MIGRATE_HEADING("Synchronizing apps without migrations:"))
                if options['app_label']:
                    self.sync_apps(connection, [app_label])
                else:
                    self.sync_apps(connection, executor.loader.unmigrated_apps)
    
            #  如果有迁移计划,则执行迁移
            if self.verbosity >= 1:
                self.stdout.write(self.style.MIGRATE_HEADING("Running migrations:"))
            if not plan:
                if self.verbosity >= 1:
                    self.stdout.write("  No migrations to apply.")
                    # If there's changes that aren't in migrations yet, tell them how to fix it.
                    autodetector = MigrationAutodetector(
                        executor.loader.project_state(),
                        ProjectState.from_apps(apps),
                    )
                    changes = autodetector.changes(graph=executor.loader.graph)
                    if changes:
                        self.stdout.write(self.style.NOTICE(
                            "  Your models in app(s): %s have changes that are not "
                            "yet reflected in a migration, and so won't be "
                            "applied." % ", ".join(repr(app) for app in sorted(changes))
                        ))
                        self.stdout.write(self.style.NOTICE(
                            "  Run 'manage.py makemigrations' to make new "
                            "migrations, and then re-run 'manage.py migrate' to "
                            "apply them."
                        ))
                fake = False
                fake_initial = False
            else:
                fake = options['fake']
                fake_initial = options['fake_initial']
            post_migrate_state = executor.migrate(
                targets, plan=plan, state=pre_migrate_state.clone(), fake=fake,
                fake_initial=fake_initial,
            )
            # 记录迁移前的状态
            post_migrate_state.clear_delayed_apps_cache()
            post_migrate_apps = post_migrate_state.apps
    
            # 对于真实的应用模型,重新渲染以包含关系。
            with post_migrate_apps.bulk_update():
                model_keys = []
                for model_state in post_migrate_apps.real_models:
                    model_key = model_state.app_label, model_state.name_lower
                    model_keys.append(model_key)
                    post_migrate_apps.unregister_model(*model_key)
            post_migrate_apps.render_multiple([
                ModelState.from_model(apps.get_model(*model)) for model in model_keys
            ])
    
            # 发送 post_migrate 信号,以便各应用在这一步执行必要的操作。
            emit_post_migrate_signal(
                self.verbosity, self.interactive, connection.alias, apps=post_migrate_apps, plan=plan,
            )
  • 18
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值