【问题标题】:SQLAlchemy automap: Best practices for performanceSQLAlchemy 自动映射:性能最佳实践
【发布时间】:2022-01-19 08:04:32
【问题描述】:

我正在围绕现有 (mysql) 数据库构建一个 python 应用程序,并使用自动映射来推断表和关系:

    base = automap_base()

    self.engine = create_engine(
        'mysql://%s:%s@%s/%s?charset=utf8mb4' % (
            config.DB_USER, config.DB_PASSWD, config.DB_HOST, config.DB_NAME
        ), echo=False
    )

    # reflect the tables
    base.prepare(self.engine, reflect=True)

    self.TableName = base.classes.table_name

使用它,我可以执行session.query(TableName) 等操作... 但是,我担心性能,因为每次应用运行时,它都会再次进行整个推理。

  • 这是一个合理的担忧吗?
  • 如果是这样,是否有可能“缓存”Automap 的输出?

【问题讨论】:

    标签: sqlalchemy


    【解决方案1】:

    我认为“反映”您的数据库结构不是可行的方法。除非您的应用程序尝试从结构中“推断”事物,例如对源文件进行静态代码分析,否则这是不必要的。在运行时反映它的另一个原因是减少使用 SQLAlchemy 开始“使用”数据库的时间。然而:

    另一种选择是使用 SQLACodegen (https://pypi.python.org/pypi/sqlacodegen) 之类的东西:

    它将“反映”您的数据库一次,并创建一组准确率达 99.5% 的声明性 SQLAlchemy 模型供您使用。但是,这确实要求您随后使模型与数据库的结构保持同步。我认为这不是一个大问题,因为您已经使用的表足够稳定,因此它们结构的运行时反映不会对您的程序产生太大影响。

    生成声明性模型本质上是反射的“缓存”。只是 SQLACodegen 将其保存到一组非常易读的类 + 字段中,而不是内存中的数据。即使结构发生了变化,并且我自己对生成的声明性模型进行了“更改”,我仍然在以后的项目中使用 SQLACodegen,每当我进行数据库更改时。这意味着我的模型通常彼此之间是一致的,并且我没有由于复制粘贴而导致的拼写错误和数据不匹配等问题。

    【讨论】:

      【解决方案2】:

      性能可能是一个合理的问题。如果数据库模式未更改,则每次运行脚本时反映数据库可能会很耗时。这在开发过程中更多是一个问题,而不是启动一个长时间运行的应用程序。如果您的数据库位于远程服务器上(同样,尤其是在开发期间),这也可以节省大量时间。

      我使用类似于答案here 的代码(如@ACV 所述)。一般的计划是第一次执行反射,然后pickle元数据对象。下次运行脚本时,它会查找 pickle 文件并使用它。该文件可以在任何地方,但我将我的放在~/.sqlalchemy_cache。这是一个基于您的代码的示例。

      import os
      from sqlalchemy.ext.declarative import declarative_base
      
      self.engine = create_engine(
          'mysql://%s:%s@%s/%s?charset=utf8mb4' % (
              config.DB_USER, config.DB_PASSWD, config.DB_HOST, config.DB_NAME
          ), echo=False
      )
      
      metadata_pickle_filename = "mydb_metadata"
      cache_path = os.path.join(os.path.expanduser("~"), ".sqlalchemy_cache")
      cached_metadata = None
      if os.path.exists(cache_path):
      try:
          with open(os.path.join(cache_path, metadata_pickle_filename), 'rb') as cache_file:
              cached_metadata = pickle.load(file=cache_file)
      except IOError:
          # cache file not found - no problem, reflect as usual
          pass
      
      if cached_metadata:
          base = declarative_base(bind=self.engine, metadata=cached_metadata)
      else:
          base = automap_base()
          base.prepare(self.engine, reflect=True) # reflect the tables
      
          # save the metadata for future runs
          try:
              if not os.path.exists(cache_path):
                  os.makedirs(cache_path)
              # make sure to open in binary mode - we're writing bytes, not str
              with open(os.path.join(cache_path, metadata_pickle_filename), 'wb') as cache_file:
                  pickle.dump(Base.metadata, cache_file)
          except:
              # couldn't write the file for some reason
              pass
      
      self.TableName = base.classes.table_name
      

      对于使用声明性表类定义的任何人,假设 Base 对象定义为例如

      Base = declarative_base(bind=engine)

      metadata_pickle_filename = "ModelClasses_trilliandb_trillian.pickle"
      
      # ------------------------------------------
      # Load the cached metadata if it's available
      # ------------------------------------------
      # NOTE: delete the cached file if the database schema changes!!
      cache_path = os.path.join(os.path.expanduser("~"), ".sqlalchemy_cache")
      cached_metadata = None
      if os.path.exists(cache_path):
          try:
              with open(os.path.join(cache_path, metadata_pickle_filename), 'rb') as cache_file:
                  cached_metadata = pickle.load(file=cache_file)
          except IOError:
              # cache file not found - no problem
              pass
      # ------------------------------------------
      
      # define all tables
      #
      class MyTable(Base):
          if cached_metadata:
              __table__ = cached_metadata.tables['my_schema.my_table']
          else:
              __tablename__ = 'my_table'
              __table_args__ = {'autoload':True, 'schema':'my_schema'}
      
      ...
      # ----------------------------------------
      # If no cached metadata was found, save it
      # ----------------------------------------
      if cached_metadata is None:
          # cache the metadata for future loading
          # - MUST DELETE IF THE DATABASE SCHEMA HAS CHANGED
          try:
              if not os.path.exists(cache_path):
                  os.makedirs(cache_path)
              # make sure to open in binary mode - we're writing bytes, not str
              with open(os.path.join(cache_path, metadata_pickle_filename), 'wb') as cache_file:
                  pickle.dump(Base.metadata, cache_file)
          except:
              # couldn't write the file for some reason
              pass
      

      重要提示!!如果数据库架构发生变化,您必须删除缓存文件以强制代码自动加载并创建新缓存。如果不这样做,更改将反映在代码中。这很容易忘记。

      【讨论】:

        【解决方案3】:

        您的第一个问题的答案在很大程度上是主观的。您正在添加数据库查询以将反射元数据提取到应用程序加载时间。开销是否很大取决于您的项目要求。

        作为参考,我有一个使用反射模式的内部工具在工作,因为我们的团队可以接受加载时间。如果它是面向外部的产品,情况可能并非如此。我的直觉是,对于大多数应用程序来说,反射开销不会支配应用程序的总加载时间。

        如果您认为它对您的目的很重要,这个question 有一个有趣的答案,用户挑选数据库元数据以便在本地缓存它。

        【讨论】:

          【解决方案4】:

          除此之外,@Demitri 的回答接近正确,但(至少在sqlalchemy 1.4.29 中),当从缓存文件生成时,该示例将在最后一行self.TableName = base.classes.table_name 失败。在这种情况下,declarative_base() 没有属性 classes

          修复就像改变一样简单:

          if cached_metadata:
              base = declarative_base(bind=self.engine, metadata=cached_metadata)
          else:
              base = automap_base()
              base.prepare(self.engine, reflect=True) # reflect the tables
          

          if cached_metadata:
              base = automap_base(declarative_base(bind=self.engine, metadata=cached_metadata))
              base.prepare()
          else:
              base = automap_base()
              base.prepare(self.engine, reflect=True) # reflect the tables
          

          这将创建具有适当属性的 automap 对象。

          【讨论】:

            猜你喜欢
            • 2021-07-24
            • 2023-03-16
            • 2020-04-19
            • 1970-01-01
            • 2012-01-17
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2019-01-21
            相关资源
            最近更新 更多