You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1669 lines
60 KiB
1669 lines
60 KiB
# ext/automap.py
|
|
# Copyright (C) 2005-2023 the SQLAlchemy authors and contributors
|
|
# <see AUTHORS file>
|
|
#
|
|
# This module is part of SQLAlchemy and is released under
|
|
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
|
|
|
r"""Define an extension to the :mod:`sqlalchemy.ext.declarative` system
|
|
which automatically generates mapped classes and relationships from a database
|
|
schema, typically though not necessarily one which is reflected.
|
|
|
|
It is hoped that the :class:`.AutomapBase` system provides a quick
|
|
and modernized solution to the problem that the very famous
|
|
`SQLSoup <https://sqlsoup.readthedocs.io/en/latest/>`_
|
|
also tries to solve, that of generating a quick and rudimentary object
|
|
model from an existing database on the fly. By addressing the issue strictly
|
|
at the mapper configuration level, and integrating fully with existing
|
|
Declarative class techniques, :class:`.AutomapBase` seeks to provide
|
|
a well-integrated approach to the issue of expediently auto-generating ad-hoc
|
|
mappings.
|
|
|
|
.. tip:: The :ref:`automap_toplevel` extension is geared towards a
|
|
"zero declaration" approach, where a complete ORM model including classes
|
|
and pre-named relationships can be generated on the fly from a database
|
|
schema. For applications that still want to use explicit class declarations
|
|
including explicit relationship definitions in conjunction with reflection
|
|
of tables, the :class:`.DeferredReflection` class, described at
|
|
:ref:`orm_declarative_reflected_deferred_reflection`, is a better choice.
|
|
|
|
.. _automap_basic_use:
|
|
|
|
Basic Use
|
|
=========
|
|
|
|
The simplest usage is to reflect an existing database into a new model.
|
|
We create a new :class:`.AutomapBase` class in a similar manner as to how
|
|
we create a declarative base class, using :func:`.automap_base`.
|
|
We then call :meth:`.AutomapBase.prepare` on the resulting base class,
|
|
asking it to reflect the schema and produce mappings::
|
|
|
|
from sqlalchemy.ext.automap import automap_base
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import create_engine
|
|
|
|
Base = automap_base()
|
|
|
|
# engine, suppose it has two tables 'user' and 'address' set up
|
|
engine = create_engine("sqlite:///mydatabase.db")
|
|
|
|
# reflect the tables
|
|
Base.prepare(autoload_with=engine)
|
|
|
|
# mapped classes are now created with names by default
|
|
# matching that of the table name.
|
|
User = Base.classes.user
|
|
Address = Base.classes.address
|
|
|
|
session = Session(engine)
|
|
|
|
# rudimentary relationships are produced
|
|
session.add(Address(email_address="foo@bar.com", user=User(name="foo")))
|
|
session.commit()
|
|
|
|
# collection-based relationships are by default named
|
|
# "<classname>_collection"
|
|
u1 = session.query(User).first()
|
|
print (u1.address_collection)
|
|
|
|
Above, calling :meth:`.AutomapBase.prepare` while passing along the
|
|
:paramref:`.AutomapBase.prepare.reflect` parameter indicates that the
|
|
:meth:`_schema.MetaData.reflect`
|
|
method will be called on this declarative base
|
|
classes' :class:`_schema.MetaData` collection; then, each **viable**
|
|
:class:`_schema.Table` within the :class:`_schema.MetaData`
|
|
will get a new mapped class
|
|
generated automatically. The :class:`_schema.ForeignKeyConstraint`
|
|
objects which
|
|
link the various tables together will be used to produce new, bidirectional
|
|
:func:`_orm.relationship` objects between classes.
|
|
The classes and relationships
|
|
follow along a default naming scheme that we can customize. At this point,
|
|
our basic mapping consisting of related ``User`` and ``Address`` classes is
|
|
ready to use in the traditional way.
|
|
|
|
.. note:: By **viable**, we mean that for a table to be mapped, it must
|
|
specify a primary key. Additionally, if the table is detected as being
|
|
a pure association table between two other tables, it will not be directly
|
|
mapped and will instead be configured as a many-to-many table between
|
|
the mappings for the two referring tables.
|
|
|
|
Generating Mappings from an Existing MetaData
|
|
=============================================
|
|
|
|
We can pass a pre-declared :class:`_schema.MetaData` object to
|
|
:func:`.automap_base`.
|
|
This object can be constructed in any way, including programmatically, from
|
|
a serialized file, or from itself being reflected using
|
|
:meth:`_schema.MetaData.reflect`.
|
|
Below we illustrate a combination of reflection and
|
|
explicit table declaration::
|
|
|
|
from sqlalchemy import create_engine, MetaData, Table, Column, ForeignKey
|
|
from sqlalchemy.ext.automap import automap_base
|
|
engine = create_engine("sqlite:///mydatabase.db")
|
|
|
|
# produce our own MetaData object
|
|
metadata = MetaData()
|
|
|
|
# we can reflect it ourselves from a database, using options
|
|
# such as 'only' to limit what tables we look at...
|
|
metadata.reflect(engine, only=['user', 'address'])
|
|
|
|
# ... or just define our own Table objects with it (or combine both)
|
|
Table('user_order', metadata,
|
|
Column('id', Integer, primary_key=True),
|
|
Column('user_id', ForeignKey('user.id'))
|
|
)
|
|
|
|
# we can then produce a set of mappings from this MetaData.
|
|
Base = automap_base(metadata=metadata)
|
|
|
|
# calling prepare() just sets up mapped classes and relationships.
|
|
Base.prepare()
|
|
|
|
# mapped classes are ready
|
|
User, Address, Order = Base.classes.user, Base.classes.address,\
|
|
Base.classes.user_order
|
|
|
|
.. _automap_by_module:
|
|
|
|
Generating Mappings from Multiple Schemas
|
|
=========================================
|
|
|
|
The :meth:`.AutomapBase.prepare` method when used with reflection may reflect
|
|
tables from one schema at a time at most, using the
|
|
:paramref:`.AutomapBase.prepare.schema` parameter to indicate the name of a
|
|
schema to be reflected from. In order to populate the :class:`.AutomapBase`
|
|
with tables from multiple schemas, :meth:`.AutomapBase.prepare` may be invoked
|
|
multiple times, each time passing a different name to the
|
|
:paramref:`.AutomapBase.prepare.schema` parameter. The
|
|
:meth:`.AutomapBase.prepare` method keeps an internal list of
|
|
:class:`_schema.Table` objects that have already been mapped, and will add new
|
|
mappings only for those :class:`_schema.Table` objects that are new since the
|
|
last time :meth:`.AutomapBase.prepare` was run::
|
|
|
|
e = create_engine("postgresql://scott:tiger@localhost/test")
|
|
|
|
Base.metadata.create_all(e)
|
|
|
|
Base = automap_base()
|
|
|
|
Base.prepare(e)
|
|
Base.prepare(e, schema="test_schema")
|
|
Base.prepare(e, schema="test_schema_2")
|
|
|
|
.. versionadded:: 2.0 The :meth:`.AutomapBase.prepare` method may be called
|
|
any number of times; only newly added tables will be mapped
|
|
on each run. Previously in version 1.4 and earlier, multiple calls would
|
|
cause errors as it would attempt to re-map an already mapped class.
|
|
The previous workaround approach of invoking
|
|
:meth:`_schema.MetaData.reflect` directly remains available as well.
|
|
|
|
Automapping same-named tables across multiple schemas
|
|
-----------------------------------------------------
|
|
|
|
For the common case where multiple schemas may have same-named tables and
|
|
therefore would generate same-named classes, conflicts can be resolved either
|
|
through use of the :paramref:`.AutomapBase.prepare.classname_for_table` hook to
|
|
apply different classnames on a per-schema basis, or by using the
|
|
:paramref:`.AutomapBase.prepare.modulename_for_table` hook, which allows
|
|
disambiguation of same-named classes by changing their effective ``__module__``
|
|
attribute. In the example below, this hook is used to create a ``__module__``
|
|
attribute for all classes that is of the form ``mymodule.<schemaname>``, where
|
|
the schema name ``default`` is used if no schema is present::
|
|
|
|
e = create_engine("postgresql://scott:tiger@localhost/test")
|
|
|
|
Base.metadata.create_all(e)
|
|
|
|
def module_name_for_table(cls, tablename, table):
|
|
if table.schema is not None:
|
|
return f"mymodule.{table.schema}"
|
|
else:
|
|
return f"mymodule.default"
|
|
|
|
Base = automap_base()
|
|
|
|
Base.prepare(e, modulename_for_table=module_name_for_table)
|
|
Base.prepare(e, schema="test_schema", modulename_for_table=module_name_for_table)
|
|
Base.prepare(e, schema="test_schema_2", modulename_for_table=module_name_for_table)
|
|
|
|
|
|
The same named-classes are organized into a hierarchical collection available
|
|
at :attr:`.AutomapBase.by_module`. This collection is traversed using the
|
|
dot-separated name of a particular package/module down into the desired
|
|
class name.
|
|
|
|
.. note:: When using the :paramref:`.AutomapBase.prepare.modulename_for_table`
|
|
hook to return a new ``__module__`` that is not ``None``, the class is
|
|
**not** placed into the :attr:`.AutomapBase.classes` collection; only
|
|
classes that were not given an explicit modulename are placed here, as the
|
|
collection cannot represent same-named classes individually.
|
|
|
|
In the example above, if the database contained a table named ``accounts`` in
|
|
all three of the default schema, the ``test_schema`` schema, and the
|
|
``test_schema_2`` schema, three separate classes will be available as::
|
|
|
|
Base.by_module.mymodule.default.accounts
|
|
Base.by_module.mymodule.test_schema.accounts
|
|
Base.by_module.mymodule.test_schema_2.accounts
|
|
|
|
The default module namespace generated for all :class:`.AutomapBase` classes is
|
|
``sqlalchemy.ext.automap``. If no
|
|
:paramref:`.AutomapBase.prepare.modulename_for_table` hook is used, the
|
|
contents of :attr:`.AutomapBase.by_module` will be entirely within the
|
|
``sqlalchemy.ext.automap`` namespace (e.g.
|
|
``MyBase.by_module.sqlalchemy.ext.automap.<classname>``), which would contain
|
|
the same series of classes as what would be seen in
|
|
:attr:`.AutomapBase.classes`. Therefore it's generally only necessary to use
|
|
:attr:`.AutomapBase.by_module` when explicit ``__module__`` conventions are
|
|
present.
|
|
|
|
.. versionadded: 2.0
|
|
|
|
Added the :attr:`.AutomapBase.by_module` collection, which stores
|
|
classes within a named hierarchy based on dot-separated module names,
|
|
as well as the :paramref:`.Automap.prepare.modulename_for_table` parameter
|
|
which allows for custom ``__module__`` schemes for automapped
|
|
classes.
|
|
|
|
|
|
|
|
Specifying Classes Explicitly
|
|
=============================
|
|
|
|
.. tip:: If explicit classes are expected to be prominent in an application,
|
|
consider using :class:`.DeferredReflection` instead.
|
|
|
|
The :mod:`.sqlalchemy.ext.automap` extension allows classes to be defined
|
|
explicitly, in a way similar to that of the :class:`.DeferredReflection` class.
|
|
Classes that extend from :class:`.AutomapBase` act like regular declarative
|
|
classes, but are not immediately mapped after their construction, and are
|
|
instead mapped when we call :meth:`.AutomapBase.prepare`. The
|
|
:meth:`.AutomapBase.prepare` method will make use of the classes we've
|
|
established based on the table name we use. If our schema contains tables
|
|
``user`` and ``address``, we can define one or both of the classes to be used::
|
|
|
|
from sqlalchemy.ext.automap import automap_base
|
|
from sqlalchemy import create_engine
|
|
|
|
# automap base
|
|
Base = automap_base()
|
|
|
|
# pre-declare User for the 'user' table
|
|
class User(Base):
|
|
__tablename__ = 'user'
|
|
|
|
# override schema elements like Columns
|
|
user_name = Column('name', String)
|
|
|
|
# override relationships too, if desired.
|
|
# we must use the same name that automap would use for the
|
|
# relationship, and also must refer to the class name that automap will
|
|
# generate for "address"
|
|
address_collection = relationship("address", collection_class=set)
|
|
|
|
# reflect
|
|
engine = create_engine("sqlite:///mydatabase.db")
|
|
Base.prepare(autoload_with=engine)
|
|
|
|
# we still have Address generated from the tablename "address",
|
|
# but User is the same as Base.classes.User now
|
|
|
|
Address = Base.classes.address
|
|
|
|
u1 = session.query(User).first()
|
|
print (u1.address_collection)
|
|
|
|
# the backref is still there:
|
|
a1 = session.query(Address).first()
|
|
print (a1.user)
|
|
|
|
Above, one of the more intricate details is that we illustrated overriding
|
|
one of the :func:`_orm.relationship` objects that automap would have created.
|
|
To do this, we needed to make sure the names match up with what automap
|
|
would normally generate, in that the relationship name would be
|
|
``User.address_collection`` and the name of the class referred to, from
|
|
automap's perspective, is called ``address``, even though we are referring to
|
|
it as ``Address`` within our usage of this class.
|
|
|
|
Overriding Naming Schemes
|
|
=========================
|
|
|
|
:mod:`.sqlalchemy.ext.automap` is tasked with producing mapped classes and
|
|
relationship names based on a schema, which means it has decision points in how
|
|
these names are determined. These three decision points are provided using
|
|
functions which can be passed to the :meth:`.AutomapBase.prepare` method, and
|
|
are known as :func:`.classname_for_table`,
|
|
:func:`.name_for_scalar_relationship`,
|
|
and :func:`.name_for_collection_relationship`. Any or all of these
|
|
functions are provided as in the example below, where we use a "camel case"
|
|
scheme for class names and a "pluralizer" for collection names using the
|
|
`Inflect <https://pypi.org/project/inflect>`_ package::
|
|
|
|
import re
|
|
import inflect
|
|
|
|
def camelize_classname(base, tablename, table):
|
|
"Produce a 'camelized' class name, e.g. "
|
|
"'words_and_underscores' -> 'WordsAndUnderscores'"
|
|
|
|
return str(tablename[0].upper() + \
|
|
re.sub(r'_([a-z])', lambda m: m.group(1).upper(), tablename[1:]))
|
|
|
|
_pluralizer = inflect.engine()
|
|
def pluralize_collection(base, local_cls, referred_cls, constraint):
|
|
"Produce an 'uncamelized', 'pluralized' class name, e.g. "
|
|
"'SomeTerm' -> 'some_terms'"
|
|
|
|
referred_name = referred_cls.__name__
|
|
uncamelized = re.sub(r'[A-Z]',
|
|
lambda m: "_%s" % m.group(0).lower(),
|
|
referred_name)[1:]
|
|
pluralized = _pluralizer.plural(uncamelized)
|
|
return pluralized
|
|
|
|
from sqlalchemy.ext.automap import automap_base
|
|
|
|
Base = automap_base()
|
|
|
|
engine = create_engine("sqlite:///mydatabase.db")
|
|
|
|
Base.prepare(autoload_with=engine,
|
|
classname_for_table=camelize_classname,
|
|
name_for_collection_relationship=pluralize_collection
|
|
)
|
|
|
|
From the above mapping, we would now have classes ``User`` and ``Address``,
|
|
where the collection from ``User`` to ``Address`` is called
|
|
``User.addresses``::
|
|
|
|
User, Address = Base.classes.User, Base.classes.Address
|
|
|
|
u1 = User(addresses=[Address(email="foo@bar.com")])
|
|
|
|
Relationship Detection
|
|
======================
|
|
|
|
The vast majority of what automap accomplishes is the generation of
|
|
:func:`_orm.relationship` structures based on foreign keys. The mechanism
|
|
by which this works for many-to-one and one-to-many relationships is as
|
|
follows:
|
|
|
|
1. A given :class:`_schema.Table`, known to be mapped to a particular class,
|
|
is examined for :class:`_schema.ForeignKeyConstraint` objects.
|
|
|
|
2. From each :class:`_schema.ForeignKeyConstraint`, the remote
|
|
:class:`_schema.Table`
|
|
object present is matched up to the class to which it is to be mapped,
|
|
if any, else it is skipped.
|
|
|
|
3. As the :class:`_schema.ForeignKeyConstraint`
|
|
we are examining corresponds to a
|
|
reference from the immediate mapped class, the relationship will be set up
|
|
as a many-to-one referring to the referred class; a corresponding
|
|
one-to-many backref will be created on the referred class referring
|
|
to this class.
|
|
|
|
4. If any of the columns that are part of the
|
|
:class:`_schema.ForeignKeyConstraint`
|
|
are not nullable (e.g. ``nullable=False``), a
|
|
:paramref:`_orm.relationship.cascade` keyword argument
|
|
of ``all, delete-orphan`` will be added to the keyword arguments to
|
|
be passed to the relationship or backref. If the
|
|
:class:`_schema.ForeignKeyConstraint` reports that
|
|
:paramref:`_schema.ForeignKeyConstraint.ondelete`
|
|
is set to ``CASCADE`` for a not null or ``SET NULL`` for a nullable
|
|
set of columns, the option :paramref:`_orm.relationship.passive_deletes`
|
|
flag is set to ``True`` in the set of relationship keyword arguments.
|
|
Note that not all backends support reflection of ON DELETE.
|
|
|
|
.. versionadded:: 1.0.0 - automap will detect non-nullable foreign key
|
|
constraints when producing a one-to-many relationship and establish
|
|
a default cascade of ``all, delete-orphan`` if so; additionally,
|
|
if the constraint specifies
|
|
:paramref:`_schema.ForeignKeyConstraint.ondelete`
|
|
of ``CASCADE`` for non-nullable or ``SET NULL`` for nullable columns,
|
|
the ``passive_deletes=True`` option is also added.
|
|
|
|
5. The names of the relationships are determined using the
|
|
:paramref:`.AutomapBase.prepare.name_for_scalar_relationship` and
|
|
:paramref:`.AutomapBase.prepare.name_for_collection_relationship`
|
|
callable functions. It is important to note that the default relationship
|
|
naming derives the name from the **the actual class name**. If you've
|
|
given a particular class an explicit name by declaring it, or specified an
|
|
alternate class naming scheme, that's the name from which the relationship
|
|
name will be derived.
|
|
|
|
6. The classes are inspected for an existing mapped property matching these
|
|
names. If one is detected on one side, but none on the other side,
|
|
:class:`.AutomapBase` attempts to create a relationship on the missing side,
|
|
then uses the :paramref:`_orm.relationship.back_populates`
|
|
parameter in order to
|
|
point the new relationship to the other side.
|
|
|
|
7. In the usual case where no relationship is on either side,
|
|
:meth:`.AutomapBase.prepare` produces a :func:`_orm.relationship` on the
|
|
"many-to-one" side and matches it to the other using the
|
|
:paramref:`_orm.relationship.backref` parameter.
|
|
|
|
8. Production of the :func:`_orm.relationship` and optionally the
|
|
:func:`.backref`
|
|
is handed off to the :paramref:`.AutomapBase.prepare.generate_relationship`
|
|
function, which can be supplied by the end-user in order to augment
|
|
the arguments passed to :func:`_orm.relationship` or :func:`.backref` or to
|
|
make use of custom implementations of these functions.
|
|
|
|
Custom Relationship Arguments
|
|
-----------------------------
|
|
|
|
The :paramref:`.AutomapBase.prepare.generate_relationship` hook can be used
|
|
to add parameters to relationships. For most cases, we can make use of the
|
|
existing :func:`.automap.generate_relationship` function to return
|
|
the object, after augmenting the given keyword dictionary with our own
|
|
arguments.
|
|
|
|
Below is an illustration of how to send
|
|
:paramref:`_orm.relationship.cascade` and
|
|
:paramref:`_orm.relationship.passive_deletes`
|
|
options along to all one-to-many relationships::
|
|
|
|
from sqlalchemy.ext.automap import generate_relationship
|
|
|
|
def _gen_relationship(base, direction, return_fn,
|
|
attrname, local_cls, referred_cls, **kw):
|
|
if direction is interfaces.ONETOMANY:
|
|
kw['cascade'] = 'all, delete-orphan'
|
|
kw['passive_deletes'] = True
|
|
# make use of the built-in function to actually return
|
|
# the result.
|
|
return generate_relationship(base, direction, return_fn,
|
|
attrname, local_cls, referred_cls, **kw)
|
|
|
|
from sqlalchemy.ext.automap import automap_base
|
|
from sqlalchemy import create_engine
|
|
|
|
# automap base
|
|
Base = automap_base()
|
|
|
|
engine = create_engine("sqlite:///mydatabase.db")
|
|
Base.prepare(autoload_with=engine,
|
|
generate_relationship=_gen_relationship)
|
|
|
|
Many-to-Many relationships
|
|
--------------------------
|
|
|
|
:mod:`.sqlalchemy.ext.automap` will generate many-to-many relationships, e.g.
|
|
those which contain a ``secondary`` argument. The process for producing these
|
|
is as follows:
|
|
|
|
1. A given :class:`_schema.Table` is examined for
|
|
:class:`_schema.ForeignKeyConstraint`
|
|
objects, before any mapped class has been assigned to it.
|
|
|
|
2. If the table contains two and exactly two
|
|
:class:`_schema.ForeignKeyConstraint`
|
|
objects, and all columns within this table are members of these two
|
|
:class:`_schema.ForeignKeyConstraint` objects, the table is assumed to be a
|
|
"secondary" table, and will **not be mapped directly**.
|
|
|
|
3. The two (or one, for self-referential) external tables to which the
|
|
:class:`_schema.Table`
|
|
refers to are matched to the classes to which they will be
|
|
mapped, if any.
|
|
|
|
4. If mapped classes for both sides are located, a many-to-many bi-directional
|
|
:func:`_orm.relationship` / :func:`.backref`
|
|
pair is created between the two
|
|
classes.
|
|
|
|
5. The override logic for many-to-many works the same as that of one-to-many/
|
|
many-to-one; the :func:`.generate_relationship` function is called upon
|
|
to generate the structures and existing attributes will be maintained.
|
|
|
|
Relationships with Inheritance
|
|
------------------------------
|
|
|
|
:mod:`.sqlalchemy.ext.automap` will not generate any relationships between
|
|
two classes that are in an inheritance relationship. That is, with two
|
|
classes given as follows::
|
|
|
|
class Employee(Base):
|
|
__tablename__ = 'employee'
|
|
id = Column(Integer, primary_key=True)
|
|
type = Column(String(50))
|
|
__mapper_args__ = {
|
|
'polymorphic_identity':'employee', 'polymorphic_on': type
|
|
}
|
|
|
|
class Engineer(Employee):
|
|
__tablename__ = 'engineer'
|
|
id = Column(Integer, ForeignKey('employee.id'), primary_key=True)
|
|
__mapper_args__ = {
|
|
'polymorphic_identity':'engineer',
|
|
}
|
|
|
|
The foreign key from ``Engineer`` to ``Employee`` is used not for a
|
|
relationship, but to establish joined inheritance between the two classes.
|
|
|
|
Note that this means automap will not generate *any* relationships
|
|
for foreign keys that link from a subclass to a superclass. If a mapping
|
|
has actual relationships from subclass to superclass as well, those
|
|
need to be explicit. Below, as we have two separate foreign keys
|
|
from ``Engineer`` to ``Employee``, we need to set up both the relationship
|
|
we want as well as the ``inherit_condition``, as these are not things
|
|
SQLAlchemy can guess::
|
|
|
|
class Employee(Base):
|
|
__tablename__ = 'employee'
|
|
id = Column(Integer, primary_key=True)
|
|
type = Column(String(50))
|
|
|
|
__mapper_args__ = {
|
|
'polymorphic_identity':'employee', 'polymorphic_on':type
|
|
}
|
|
|
|
class Engineer(Employee):
|
|
__tablename__ = 'engineer'
|
|
id = Column(Integer, ForeignKey('employee.id'), primary_key=True)
|
|
favorite_employee_id = Column(Integer, ForeignKey('employee.id'))
|
|
|
|
favorite_employee = relationship(Employee,
|
|
foreign_keys=favorite_employee_id)
|
|
|
|
__mapper_args__ = {
|
|
'polymorphic_identity':'engineer',
|
|
'inherit_condition': id == Employee.id
|
|
}
|
|
|
|
Handling Simple Naming Conflicts
|
|
--------------------------------
|
|
|
|
In the case of naming conflicts during mapping, override any of
|
|
:func:`.classname_for_table`, :func:`.name_for_scalar_relationship`,
|
|
and :func:`.name_for_collection_relationship` as needed. For example, if
|
|
automap is attempting to name a many-to-one relationship the same as an
|
|
existing column, an alternate convention can be conditionally selected. Given
|
|
a schema:
|
|
|
|
.. sourcecode:: sql
|
|
|
|
CREATE TABLE table_a (
|
|
id INTEGER PRIMARY KEY
|
|
);
|
|
|
|
CREATE TABLE table_b (
|
|
id INTEGER PRIMARY KEY,
|
|
table_a INTEGER,
|
|
FOREIGN KEY(table_a) REFERENCES table_a(id)
|
|
);
|
|
|
|
The above schema will first automap the ``table_a`` table as a class named
|
|
``table_a``; it will then automap a relationship onto the class for ``table_b``
|
|
with the same name as this related class, e.g. ``table_a``. This
|
|
relationship name conflicts with the mapping column ``table_b.table_a``,
|
|
and will emit an error on mapping.
|
|
|
|
We can resolve this conflict by using an underscore as follows::
|
|
|
|
def name_for_scalar_relationship(base, local_cls, referred_cls, constraint):
|
|
name = referred_cls.__name__.lower()
|
|
local_table = local_cls.__table__
|
|
if name in local_table.columns:
|
|
newname = name + "_"
|
|
warnings.warn(
|
|
"Already detected name %s present. using %s" %
|
|
(name, newname))
|
|
return newname
|
|
return name
|
|
|
|
|
|
Base.prepare(autoload_with=engine,
|
|
name_for_scalar_relationship=name_for_scalar_relationship)
|
|
|
|
Alternatively, we can change the name on the column side. The columns
|
|
that are mapped can be modified using the technique described at
|
|
:ref:`mapper_column_distinct_names`, by assigning the column explicitly
|
|
to a new name::
|
|
|
|
Base = automap_base()
|
|
|
|
class TableB(Base):
|
|
__tablename__ = 'table_b'
|
|
_table_a = Column('table_a', ForeignKey('table_a.id'))
|
|
|
|
Base.prepare(autoload_with=engine)
|
|
|
|
|
|
Using Automap with Explicit Declarations
|
|
========================================
|
|
|
|
As noted previously, automap has no dependency on reflection, and can make
|
|
use of any collection of :class:`_schema.Table` objects within a
|
|
:class:`_schema.MetaData`
|
|
collection. From this, it follows that automap can also be used
|
|
generate missing relationships given an otherwise complete model that fully
|
|
defines table metadata::
|
|
|
|
from sqlalchemy.ext.automap import automap_base
|
|
from sqlalchemy import Column, Integer, String, ForeignKey
|
|
|
|
Base = automap_base()
|
|
|
|
class User(Base):
|
|
__tablename__ = 'user'
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
name = Column(String)
|
|
|
|
class Address(Base):
|
|
__tablename__ = 'address'
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
email = Column(String)
|
|
user_id = Column(ForeignKey('user.id'))
|
|
|
|
# produce relationships
|
|
Base.prepare()
|
|
|
|
# mapping is complete, with "address_collection" and
|
|
# "user" relationships
|
|
a1 = Address(email='u1')
|
|
a2 = Address(email='u2')
|
|
u1 = User(address_collection=[a1, a2])
|
|
assert a1.user is u1
|
|
|
|
Above, given mostly complete ``User`` and ``Address`` mappings, the
|
|
:class:`_schema.ForeignKey` which we defined on ``Address.user_id`` allowed a
|
|
bidirectional relationship pair ``Address.user`` and
|
|
``User.address_collection`` to be generated on the mapped classes.
|
|
|
|
Note that when subclassing :class:`.AutomapBase`,
|
|
the :meth:`.AutomapBase.prepare` method is required; if not called, the classes
|
|
we've declared are in an un-mapped state.
|
|
|
|
|
|
.. _automap_intercepting_columns:
|
|
|
|
Intercepting Column Definitions
|
|
===============================
|
|
|
|
The :class:`_schema.MetaData` and :class:`_schema.Table` objects support an
|
|
event hook :meth:`_events.DDLEvents.column_reflect` that may be used to intercept
|
|
the information reflected about a database column before the :class:`_schema.Column`
|
|
object is constructed. For example if we wanted to map columns using a
|
|
naming convention such as ``"attr_<columnname>"``, the event could
|
|
be applied as::
|
|
|
|
@event.listens_for(Base.metadata, "column_reflect")
|
|
def column_reflect(inspector, table, column_info):
|
|
# set column.key = "attr_<lower_case_name>"
|
|
column_info['key'] = "attr_%s" % column_info['name'].lower()
|
|
|
|
# run reflection
|
|
Base.prepare(autoload_with=engine)
|
|
|
|
.. versionadded:: 1.4.0b2 the :meth:`_events.DDLEvents.column_reflect` event
|
|
may be applied to a :class:`_schema.MetaData` object.
|
|
|
|
.. seealso::
|
|
|
|
:meth:`_events.DDLEvents.column_reflect`
|
|
|
|
:ref:`mapper_automated_reflection_schemes` - in the ORM mapping documentation
|
|
|
|
|
|
""" # noqa
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
from typing import Any
|
|
from typing import Callable
|
|
from typing import cast
|
|
from typing import ClassVar
|
|
from typing import Dict
|
|
from typing import List
|
|
from typing import NoReturn
|
|
from typing import Optional
|
|
from typing import overload
|
|
from typing import Set
|
|
from typing import Tuple
|
|
from typing import Type
|
|
from typing import TYPE_CHECKING
|
|
from typing import TypeVar
|
|
from typing import Union
|
|
|
|
from .. import util
|
|
from ..orm import backref
|
|
from ..orm import declarative_base as _declarative_base
|
|
from ..orm import exc as orm_exc
|
|
from ..orm import interfaces
|
|
from ..orm import relationship
|
|
from ..orm.decl_base import _DeferredMapperConfig
|
|
from ..orm.mapper import _CONFIGURE_MUTEX
|
|
from ..schema import ForeignKeyConstraint
|
|
from ..sql import and_
|
|
from ..util import Properties
|
|
from ..util.typing import Protocol
|
|
|
|
if TYPE_CHECKING:
|
|
from ..engine.base import Engine
|
|
from ..orm.base import RelationshipDirection
|
|
from ..orm.relationships import ORMBackrefArgument
|
|
from ..orm.relationships import Relationship
|
|
from ..sql.schema import Column
|
|
from ..sql.schema import MetaData
|
|
from ..sql.schema import Table
|
|
from ..util import immutabledict
|
|
|
|
|
|
_KT = TypeVar("_KT", bound=Any)
|
|
_VT = TypeVar("_VT", bound=Any)
|
|
|
|
|
|
class PythonNameForTableType(Protocol):
|
|
def __call__(self, base: Type[Any], tablename: str, table: Table) -> str:
|
|
...
|
|
|
|
|
|
def classname_for_table(
|
|
base: Type[Any],
|
|
tablename: str,
|
|
table: Table,
|
|
) -> str:
|
|
"""Return the class name that should be used, given the name
|
|
of a table.
|
|
|
|
The default implementation is::
|
|
|
|
return str(tablename)
|
|
|
|
Alternate implementations can be specified using the
|
|
:paramref:`.AutomapBase.prepare.classname_for_table`
|
|
parameter.
|
|
|
|
:param base: the :class:`.AutomapBase` class doing the prepare.
|
|
|
|
:param tablename: string name of the :class:`_schema.Table`.
|
|
|
|
:param table: the :class:`_schema.Table` object itself.
|
|
|
|
:return: a string class name.
|
|
|
|
.. note::
|
|
|
|
In Python 2, the string used for the class name **must** be a
|
|
non-Unicode object, e.g. a ``str()`` object. The ``.name`` attribute
|
|
of :class:`_schema.Table` is typically a Python unicode subclass,
|
|
so the
|
|
``str()`` function should be applied to this name, after accounting for
|
|
any non-ASCII characters.
|
|
|
|
"""
|
|
return str(tablename)
|
|
|
|
|
|
class NameForScalarRelationshipType(Protocol):
|
|
def __call__(
|
|
self,
|
|
base: Type[Any],
|
|
local_cls: Type[Any],
|
|
referred_cls: Type[Any],
|
|
constraint: ForeignKeyConstraint,
|
|
) -> str:
|
|
...
|
|
|
|
|
|
def name_for_scalar_relationship(
|
|
base: Type[Any],
|
|
local_cls: Type[Any],
|
|
referred_cls: Type[Any],
|
|
constraint: ForeignKeyConstraint,
|
|
) -> str:
|
|
"""Return the attribute name that should be used to refer from one
|
|
class to another, for a scalar object reference.
|
|
|
|
The default implementation is::
|
|
|
|
return referred_cls.__name__.lower()
|
|
|
|
Alternate implementations can be specified using the
|
|
:paramref:`.AutomapBase.prepare.name_for_scalar_relationship`
|
|
parameter.
|
|
|
|
:param base: the :class:`.AutomapBase` class doing the prepare.
|
|
|
|
:param local_cls: the class to be mapped on the local side.
|
|
|
|
:param referred_cls: the class to be mapped on the referring side.
|
|
|
|
:param constraint: the :class:`_schema.ForeignKeyConstraint` that is being
|
|
inspected to produce this relationship.
|
|
|
|
"""
|
|
return referred_cls.__name__.lower()
|
|
|
|
|
|
class NameForCollectionRelationshipType(Protocol):
|
|
def __call__(
|
|
self,
|
|
base: Type[Any],
|
|
local_cls: Type[Any],
|
|
referred_cls: Type[Any],
|
|
constraint: ForeignKeyConstraint,
|
|
) -> str:
|
|
...
|
|
|
|
|
|
def name_for_collection_relationship(
|
|
base: Type[Any],
|
|
local_cls: Type[Any],
|
|
referred_cls: Type[Any],
|
|
constraint: ForeignKeyConstraint,
|
|
) -> str:
|
|
"""Return the attribute name that should be used to refer from one
|
|
class to another, for a collection reference.
|
|
|
|
The default implementation is::
|
|
|
|
return referred_cls.__name__.lower() + "_collection"
|
|
|
|
Alternate implementations
|
|
can be specified using the
|
|
:paramref:`.AutomapBase.prepare.name_for_collection_relationship`
|
|
parameter.
|
|
|
|
:param base: the :class:`.AutomapBase` class doing the prepare.
|
|
|
|
:param local_cls: the class to be mapped on the local side.
|
|
|
|
:param referred_cls: the class to be mapped on the referring side.
|
|
|
|
:param constraint: the :class:`_schema.ForeignKeyConstraint` that is being
|
|
inspected to produce this relationship.
|
|
|
|
"""
|
|
return referred_cls.__name__.lower() + "_collection"
|
|
|
|
|
|
class GenerateRelationshipType(Protocol):
|
|
@overload
|
|
def __call__(
|
|
self,
|
|
base: Type[Any],
|
|
direction: RelationshipDirection,
|
|
return_fn: Callable[..., Relationship[Any]],
|
|
attrname: str,
|
|
local_cls: Type[Any],
|
|
referred_cls: Type[Any],
|
|
**kw: Any,
|
|
) -> Relationship[Any]:
|
|
...
|
|
|
|
@overload
|
|
def __call__(
|
|
self,
|
|
base: Type[Any],
|
|
direction: RelationshipDirection,
|
|
return_fn: Callable[..., ORMBackrefArgument],
|
|
attrname: str,
|
|
local_cls: Type[Any],
|
|
referred_cls: Type[Any],
|
|
**kw: Any,
|
|
) -> ORMBackrefArgument:
|
|
...
|
|
|
|
def __call__(
|
|
self,
|
|
base: Type[Any],
|
|
direction: RelationshipDirection,
|
|
return_fn: Union[
|
|
Callable[..., Relationship[Any]], Callable[..., ORMBackrefArgument]
|
|
],
|
|
attrname: str,
|
|
local_cls: Type[Any],
|
|
referred_cls: Type[Any],
|
|
**kw: Any,
|
|
) -> Union[ORMBackrefArgument, Relationship[Any]]:
|
|
...
|
|
|
|
|
|
@overload
|
|
def generate_relationship(
|
|
base: Type[Any],
|
|
direction: RelationshipDirection,
|
|
return_fn: Callable[..., Relationship[Any]],
|
|
attrname: str,
|
|
local_cls: Type[Any],
|
|
referred_cls: Type[Any],
|
|
**kw: Any,
|
|
) -> Relationship[Any]:
|
|
...
|
|
|
|
|
|
@overload
|
|
def generate_relationship(
|
|
base: Type[Any],
|
|
direction: RelationshipDirection,
|
|
return_fn: Callable[..., ORMBackrefArgument],
|
|
attrname: str,
|
|
local_cls: Type[Any],
|
|
referred_cls: Type[Any],
|
|
**kw: Any,
|
|
) -> ORMBackrefArgument:
|
|
...
|
|
|
|
|
|
def generate_relationship(
|
|
base: Type[Any],
|
|
direction: RelationshipDirection,
|
|
return_fn: Union[
|
|
Callable[..., Relationship[Any]], Callable[..., ORMBackrefArgument]
|
|
],
|
|
attrname: str,
|
|
local_cls: Type[Any],
|
|
referred_cls: Type[Any],
|
|
**kw: Any,
|
|
) -> Union[Relationship[Any], ORMBackrefArgument]:
|
|
r"""Generate a :func:`_orm.relationship` or :func:`.backref`
|
|
on behalf of two
|
|
mapped classes.
|
|
|
|
An alternate implementation of this function can be specified using the
|
|
:paramref:`.AutomapBase.prepare.generate_relationship` parameter.
|
|
|
|
The default implementation of this function is as follows::
|
|
|
|
if return_fn is backref:
|
|
return return_fn(attrname, **kw)
|
|
elif return_fn is relationship:
|
|
return return_fn(referred_cls, **kw)
|
|
else:
|
|
raise TypeError("Unknown relationship function: %s" % return_fn)
|
|
|
|
:param base: the :class:`.AutomapBase` class doing the prepare.
|
|
|
|
:param direction: indicate the "direction" of the relationship; this will
|
|
be one of :data:`.ONETOMANY`, :data:`.MANYTOONE`, :data:`.MANYTOMANY`.
|
|
|
|
:param return_fn: the function that is used by default to create the
|
|
relationship. This will be either :func:`_orm.relationship` or
|
|
:func:`.backref`. The :func:`.backref` function's result will be used to
|
|
produce a new :func:`_orm.relationship` in a second step,
|
|
so it is critical
|
|
that user-defined implementations correctly differentiate between the two
|
|
functions, if a custom relationship function is being used.
|
|
|
|
:param attrname: the attribute name to which this relationship is being
|
|
assigned. If the value of :paramref:`.generate_relationship.return_fn` is
|
|
the :func:`.backref` function, then this name is the name that is being
|
|
assigned to the backref.
|
|
|
|
:param local_cls: the "local" class to which this relationship or backref
|
|
will be locally present.
|
|
|
|
:param referred_cls: the "referred" class to which the relationship or
|
|
backref refers to.
|
|
|
|
:param \**kw: all additional keyword arguments are passed along to the
|
|
function.
|
|
|
|
:return: a :func:`_orm.relationship` or :func:`.backref` construct,
|
|
as dictated
|
|
by the :paramref:`.generate_relationship.return_fn` parameter.
|
|
|
|
"""
|
|
|
|
if return_fn is backref:
|
|
return return_fn(attrname, **kw)
|
|
elif return_fn is relationship:
|
|
return return_fn(referred_cls, **kw)
|
|
else:
|
|
raise TypeError("Unknown relationship function: %s" % return_fn)
|
|
|
|
|
|
ByModuleProperties = Properties[Union["ByModuleProperties", Type[Any]]]
|
|
|
|
|
|
class AutomapBase:
|
|
"""Base class for an "automap" schema.
|
|
|
|
The :class:`.AutomapBase` class can be compared to the "declarative base"
|
|
class that is produced by the :func:`.declarative.declarative_base`
|
|
function. In practice, the :class:`.AutomapBase` class is always used
|
|
as a mixin along with an actual declarative base.
|
|
|
|
A new subclassable :class:`.AutomapBase` is typically instantiated
|
|
using the :func:`.automap_base` function.
|
|
|
|
.. seealso::
|
|
|
|
:ref:`automap_toplevel`
|
|
|
|
"""
|
|
|
|
__abstract__ = True
|
|
|
|
classes: ClassVar[Properties[Type[Any]]]
|
|
"""An instance of :class:`.util.Properties` containing classes.
|
|
|
|
This object behaves much like the ``.c`` collection on a table. Classes
|
|
are present under the name they were given, e.g.::
|
|
|
|
Base = automap_base()
|
|
Base.prepare(autoload_with=some_engine)
|
|
|
|
User, Address = Base.classes.User, Base.classes.Address
|
|
|
|
"""
|
|
|
|
by_module: ClassVar[ByModuleProperties]
|
|
"""An instance of :class:`.util.Properties` containing a hierarchal
|
|
structure of dot-separated module names linked to classes.
|
|
|
|
This collection is an alternative to the :attr:`.AutomapBase.classes`
|
|
collection that is useful when making use of the
|
|
:paramref:`.AutomapBase.prepare.modulename_for_table` parameter, which will
|
|
apply distinct ``__module__`` attributes to generated classes.
|
|
|
|
The default ``__module__`` an automap-generated class is
|
|
``sqlalchemy.ext.automap``; to access this namespace using
|
|
:attr:`.AutomapBase.by_module` looks like::
|
|
|
|
User = Base.by_module.sqlalchemy.ext.automap.User
|
|
|
|
If a class had a ``__module__`` of ``mymodule.account``, accessing
|
|
this namespace looks like::
|
|
|
|
MyClass = Base.by_module.mymodule.account.MyClass
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
.. seealso::
|
|
|
|
:ref:`automap_by_module`
|
|
|
|
"""
|
|
|
|
metadata: ClassVar[MetaData]
|
|
"""Refers to the :class:`_schema.MetaData` collection that will be used
|
|
for new :class:`_schema.Table` objects.
|
|
|
|
.. seealso::
|
|
|
|
:ref:`orm_declarative_metadata`
|
|
|
|
"""
|
|
|
|
_sa_automapbase_bookkeeping: ClassVar[_Bookkeeping]
|
|
|
|
@classmethod
|
|
@util.deprecated_params(
|
|
engine=(
|
|
"2.0",
|
|
"The :paramref:`_automap.AutomapBase.prepare.engine` parameter "
|
|
"is deprecated and will be removed in a future release. "
|
|
"Please use the "
|
|
":paramref:`_automap.AutomapBase.prepare.autoload_with` "
|
|
"parameter.",
|
|
),
|
|
reflect=(
|
|
"2.0",
|
|
"The :paramref:`_automap.AutomapBase.prepare.reflect` "
|
|
"parameter is deprecated and will be removed in a future "
|
|
"release. Reflection is enabled when "
|
|
":paramref:`_automap.AutomapBase.prepare.autoload_with` "
|
|
"is passed.",
|
|
),
|
|
)
|
|
def prepare(
|
|
cls: Type[AutomapBase],
|
|
autoload_with: Optional[Engine] = None,
|
|
engine: Optional[Any] = None,
|
|
reflect: bool = False,
|
|
schema: Optional[str] = None,
|
|
classname_for_table: Optional[PythonNameForTableType] = None,
|
|
modulename_for_table: Optional[PythonNameForTableType] = None,
|
|
collection_class: Optional[Any] = None,
|
|
name_for_scalar_relationship: Optional[
|
|
NameForScalarRelationshipType
|
|
] = None,
|
|
name_for_collection_relationship: Optional[
|
|
NameForCollectionRelationshipType
|
|
] = None,
|
|
generate_relationship: Optional[GenerateRelationshipType] = None,
|
|
reflection_options: Union[
|
|
Dict[_KT, _VT], immutabledict[_KT, _VT]
|
|
] = util.EMPTY_DICT,
|
|
) -> None:
|
|
"""Extract mapped classes and relationships from the
|
|
:class:`_schema.MetaData` and perform mappings.
|
|
|
|
For full documentation and examples see
|
|
:ref:`automap_basic_use`.
|
|
|
|
:param autoload_with: an :class:`_engine.Engine` or
|
|
:class:`_engine.Connection` with which
|
|
to perform schema reflection; when specified, the
|
|
:meth:`_schema.MetaData.reflect` method will be invoked within
|
|
the scope of this method.
|
|
|
|
:param engine: legacy; use :paramref:`.AutomapBase.autoload_with`.
|
|
Used to indicate the :class:`_engine.Engine` or
|
|
:class:`_engine.Connection` with which to reflect tables with,
|
|
if :paramref:`.AutomapBase.reflect` is True.
|
|
|
|
:param reflect: legacy; use :paramref:`.AutomapBase.autoload_with`.
|
|
Indicates that :meth:`_schema.MetaData.reflect` should be invoked.
|
|
|
|
:param classname_for_table: callable function which will be used to
|
|
produce new class names, given a table name. Defaults to
|
|
:func:`.classname_for_table`.
|
|
|
|
:param modulename_for_table: callable function which will be used to
|
|
produce the effective ``__module__`` for an internally generated
|
|
class, to allow for multiple classes of the same name in a single
|
|
automap base which would be in different "modules".
|
|
|
|
Defaults to ``None``, which will indicate that ``__module__`` will not
|
|
be set explicitly; the Python runtime will use the value
|
|
``sqlalchemy.ext.automap`` for these classes.
|
|
|
|
When assigning ``__module__`` to generated classes, they can be
|
|
accessed based on dot-separated module names using the
|
|
:attr:`.AutomapBase.by_module` collection. Classes that have
|
|
an explicit ``__module_`` assigned using this hook do **not** get
|
|
placed into the :attr:`.AutomapBase.classes` collection, only
|
|
into :attr:`.AutomapBase.by_module`.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
.. seealso::
|
|
|
|
:ref:`automap_by_module`
|
|
|
|
:param name_for_scalar_relationship: callable function which will be
|
|
used to produce relationship names for scalar relationships. Defaults
|
|
to :func:`.name_for_scalar_relationship`.
|
|
|
|
:param name_for_collection_relationship: callable function which will
|
|
be used to produce relationship names for collection-oriented
|
|
relationships. Defaults to :func:`.name_for_collection_relationship`.
|
|
|
|
:param generate_relationship: callable function which will be used to
|
|
actually generate :func:`_orm.relationship` and :func:`.backref`
|
|
constructs. Defaults to :func:`.generate_relationship`.
|
|
|
|
:param collection_class: the Python collection class that will be used
|
|
when a new :func:`_orm.relationship`
|
|
object is created that represents a
|
|
collection. Defaults to ``list``.
|
|
|
|
:param schema: Schema name to reflect when reflecting tables using
|
|
the :paramref:`.AutomapBase.prepare.autoload_with` parameter. The name
|
|
is passed to the :paramref:`_schema.MetaData.reflect.schema` parameter
|
|
of :meth:`_schema.MetaData.reflect`. When omitted, the default schema
|
|
in use by the database connection is used.
|
|
|
|
.. note:: The :paramref:`.AutomapBase.prepare.schema`
|
|
parameter supports reflection of a single schema at a time.
|
|
In order to include tables from many schemas, use
|
|
multiple calls to :meth:`.AutomapBase.prepare`.
|
|
|
|
For an overview of multiple-schema automap including the use
|
|
of additional naming conventions to resolve table name
|
|
conflicts, see the section :ref:`automap_by_module`.
|
|
|
|
.. versionadded:: 2.0 :meth:`.AutomapBase.prepare` supports being
|
|
directly invoked any number of times, keeping track of tables
|
|
that have already been processed to avoid processing them
|
|
a second time.
|
|
|
|
:param reflection_options: When present, this dictionary of options
|
|
will be passed to :meth:`_schema.MetaData.reflect`
|
|
to supply general reflection-specific options like ``only`` and/or
|
|
dialect-specific options like ``oracle_resolve_synonyms``.
|
|
|
|
.. versionadded:: 1.4
|
|
|
|
"""
|
|
|
|
for mr in cls.__mro__:
|
|
if "_sa_automapbase_bookkeeping" in mr.__dict__:
|
|
automap_base = cast("Type[AutomapBase]", mr)
|
|
break
|
|
else:
|
|
assert False, "Can't locate automap base in class hierarchy"
|
|
|
|
glbls = globals()
|
|
if classname_for_table is None:
|
|
classname_for_table = glbls["classname_for_table"]
|
|
if name_for_scalar_relationship is None:
|
|
name_for_scalar_relationship = glbls[
|
|
"name_for_scalar_relationship"
|
|
]
|
|
if name_for_collection_relationship is None:
|
|
name_for_collection_relationship = glbls[
|
|
"name_for_collection_relationship"
|
|
]
|
|
if generate_relationship is None:
|
|
generate_relationship = glbls["generate_relationship"]
|
|
if collection_class is None:
|
|
collection_class = list
|
|
|
|
if autoload_with:
|
|
reflect = True
|
|
|
|
if engine:
|
|
autoload_with = engine
|
|
|
|
if reflect:
|
|
assert autoload_with
|
|
opts = dict(
|
|
schema=schema,
|
|
extend_existing=True,
|
|
autoload_replace=False,
|
|
)
|
|
if reflection_options:
|
|
opts.update(reflection_options)
|
|
cls.metadata.reflect(autoload_with, **opts) # type: ignore[arg-type] # noqa: E501
|
|
|
|
with _CONFIGURE_MUTEX:
|
|
table_to_map_config: Union[
|
|
Dict[Optional[Table], _DeferredMapperConfig],
|
|
Dict[Table, _DeferredMapperConfig],
|
|
] = {
|
|
cast("Table", m.local_table): m
|
|
for m in _DeferredMapperConfig.classes_for_base(
|
|
cls, sort=False
|
|
)
|
|
}
|
|
|
|
many_to_many: List[
|
|
Tuple[Table, Table, List[ForeignKeyConstraint], Table]
|
|
]
|
|
many_to_many = []
|
|
|
|
bookkeeping = automap_base._sa_automapbase_bookkeeping
|
|
metadata_tables = cls.metadata.tables
|
|
|
|
for table_key in set(metadata_tables).difference(
|
|
bookkeeping.table_keys
|
|
):
|
|
table = metadata_tables[table_key]
|
|
bookkeeping.table_keys.add(table_key)
|
|
|
|
lcl_m2m, rem_m2m, m2m_const = _is_many_to_many(cls, table)
|
|
if lcl_m2m is not None:
|
|
assert rem_m2m is not None
|
|
assert m2m_const is not None
|
|
many_to_many.append((lcl_m2m, rem_m2m, m2m_const, table))
|
|
elif not table.primary_key:
|
|
continue
|
|
elif table not in table_to_map_config:
|
|
clsdict: Dict[str, Any] = {"__table__": table}
|
|
if modulename_for_table is not None:
|
|
new_module = modulename_for_table(
|
|
cls, table.name, table
|
|
)
|
|
if new_module is not None:
|
|
clsdict["__module__"] = new_module
|
|
else:
|
|
new_module = None
|
|
|
|
newname = classname_for_table(cls, table.name, table)
|
|
if new_module is None and newname in cls.classes:
|
|
util.warn(
|
|
"Ignoring duplicate class name "
|
|
f"'{newname}' "
|
|
"received in automap base for table "
|
|
f"{table.key} without "
|
|
"``__module__`` being set; consider using the "
|
|
"``modulename_for_table`` hook"
|
|
)
|
|
continue
|
|
|
|
mapped_cls = type(
|
|
newname,
|
|
(automap_base,),
|
|
clsdict,
|
|
)
|
|
map_config = _DeferredMapperConfig.config_for_cls(
|
|
mapped_cls
|
|
)
|
|
assert map_config.cls.__name__ == newname
|
|
if new_module is None:
|
|
cls.classes[newname] = mapped_cls
|
|
|
|
by_module_properties: ByModuleProperties = cls.by_module
|
|
for token in map_config.cls.__module__.split("."):
|
|
|
|
if token not in by_module_properties:
|
|
by_module_properties[token] = util.Properties({})
|
|
|
|
props = by_module_properties[token]
|
|
|
|
# we can assert this because the clsregistry
|
|
# module would have raised if there was a mismatch
|
|
# between modules/classes already.
|
|
# see test_cls_schema_name_conflict
|
|
assert isinstance(props, Properties)
|
|
by_module_properties = props
|
|
|
|
by_module_properties[map_config.cls.__name__] = mapped_cls
|
|
|
|
table_to_map_config[table] = map_config
|
|
|
|
for map_config in table_to_map_config.values():
|
|
_relationships_for_fks(
|
|
automap_base,
|
|
map_config,
|
|
table_to_map_config,
|
|
collection_class,
|
|
name_for_scalar_relationship,
|
|
name_for_collection_relationship,
|
|
generate_relationship,
|
|
)
|
|
|
|
for lcl_m2m, rem_m2m, m2m_const, table in many_to_many:
|
|
_m2m_relationship(
|
|
automap_base,
|
|
lcl_m2m,
|
|
rem_m2m,
|
|
m2m_const,
|
|
table,
|
|
table_to_map_config,
|
|
collection_class,
|
|
name_for_scalar_relationship,
|
|
name_for_collection_relationship,
|
|
generate_relationship,
|
|
)
|
|
|
|
for map_config in _DeferredMapperConfig.classes_for_base(
|
|
automap_base
|
|
):
|
|
map_config.map()
|
|
|
|
_sa_decl_prepare = True
|
|
"""Indicate that the mapping of classes should be deferred.
|
|
|
|
The presence of this attribute name indicates to declarative
|
|
that the call to mapper() should not occur immediately; instead,
|
|
information about the table and attributes to be mapped are gathered
|
|
into an internal structure called _DeferredMapperConfig. These
|
|
objects can be collected later using classes_for_base(), additional
|
|
mapping decisions can be made, and then the map() method will actually
|
|
apply the mapping.
|
|
|
|
The only real reason this deferral of the whole
|
|
thing is needed is to support primary key columns that aren't reflected
|
|
yet when the class is declared; everything else can theoretically be
|
|
added to the mapper later. However, the _DeferredMapperConfig is a
|
|
nice interface in any case which exists at that not usually exposed point
|
|
at which declarative has the class and the Table but hasn't called
|
|
mapper() yet.
|
|
|
|
"""
|
|
|
|
@classmethod
|
|
def _sa_raise_deferred_config(cls) -> NoReturn:
|
|
raise orm_exc.UnmappedClassError(
|
|
cls,
|
|
msg="Class %s is a subclass of AutomapBase. "
|
|
"Mappings are not produced until the .prepare() "
|
|
"method is called on the class hierarchy."
|
|
% orm_exc._safe_cls_name(cls),
|
|
)
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class _Bookkeeping:
|
|
__slots__ = ("table_keys",)
|
|
|
|
table_keys: Set[str]
|
|
|
|
|
|
def automap_base(
|
|
declarative_base: Optional[Type[Any]] = None, **kw: Any
|
|
) -> Any:
|
|
r"""Produce a declarative automap base.
|
|
|
|
This function produces a new base class that is a product of the
|
|
:class:`.AutomapBase` class as well a declarative base produced by
|
|
:func:`.declarative.declarative_base`.
|
|
|
|
All parameters other than ``declarative_base`` are keyword arguments
|
|
that are passed directly to the :func:`.declarative.declarative_base`
|
|
function.
|
|
|
|
:param declarative_base: an existing class produced by
|
|
:func:`.declarative.declarative_base`. When this is passed, the function
|
|
no longer invokes :func:`.declarative.declarative_base` itself, and all
|
|
other keyword arguments are ignored.
|
|
|
|
:param \**kw: keyword arguments are passed along to
|
|
:func:`.declarative.declarative_base`.
|
|
|
|
"""
|
|
if declarative_base is None:
|
|
Base = _declarative_base(**kw)
|
|
else:
|
|
Base = declarative_base
|
|
|
|
return type(
|
|
Base.__name__,
|
|
(AutomapBase, Base),
|
|
{
|
|
"__abstract__": True,
|
|
"classes": util.Properties({}),
|
|
"by_module": util.Properties({}),
|
|
"_sa_automapbase_bookkeeping": _Bookkeeping(set()),
|
|
},
|
|
)
|
|
|
|
|
|
def _is_many_to_many(
|
|
automap_base: Type[Any], table: Table
|
|
) -> Tuple[
|
|
Optional[Table], Optional[Table], Optional[list[ForeignKeyConstraint]]
|
|
]:
|
|
fk_constraints = [
|
|
const
|
|
for const in table.constraints
|
|
if isinstance(const, ForeignKeyConstraint)
|
|
]
|
|
if len(fk_constraints) != 2:
|
|
return None, None, None
|
|
|
|
cols: List[Column[Any]] = sum(
|
|
[
|
|
[fk.parent for fk in fk_constraint.elements]
|
|
for fk_constraint in fk_constraints
|
|
],
|
|
[],
|
|
)
|
|
|
|
if set(cols) != set(table.c):
|
|
return None, None, None
|
|
|
|
return (
|
|
fk_constraints[0].elements[0].column.table,
|
|
fk_constraints[1].elements[0].column.table,
|
|
fk_constraints,
|
|
)
|
|
|
|
|
|
def _relationships_for_fks(
|
|
automap_base: Type[Any],
|
|
map_config: _DeferredMapperConfig,
|
|
table_to_map_config: Union[
|
|
Dict[Optional[Table], _DeferredMapperConfig],
|
|
Dict[Table, _DeferredMapperConfig],
|
|
],
|
|
collection_class: type,
|
|
name_for_scalar_relationship: NameForScalarRelationshipType,
|
|
name_for_collection_relationship: NameForCollectionRelationshipType,
|
|
generate_relationship: GenerateRelationshipType,
|
|
) -> None:
|
|
local_table = cast("Optional[Table]", map_config.local_table)
|
|
local_cls = cast(
|
|
"Optional[Type[Any]]", map_config.cls
|
|
) # derived from a weakref, may be None
|
|
|
|
if local_table is None or local_cls is None:
|
|
return
|
|
for constraint in local_table.constraints:
|
|
if isinstance(constraint, ForeignKeyConstraint):
|
|
fks = constraint.elements
|
|
referred_table = fks[0].column.table
|
|
referred_cfg = table_to_map_config.get(referred_table, None)
|
|
if referred_cfg is None:
|
|
continue
|
|
referred_cls = referred_cfg.cls
|
|
|
|
if local_cls is not referred_cls and issubclass(
|
|
local_cls, referred_cls
|
|
):
|
|
continue
|
|
|
|
relationship_name = name_for_scalar_relationship(
|
|
automap_base, local_cls, referred_cls, constraint
|
|
)
|
|
backref_name = name_for_collection_relationship(
|
|
automap_base, referred_cls, local_cls, constraint
|
|
)
|
|
|
|
o2m_kws: Dict[str, Union[str, bool]] = {}
|
|
nullable = False not in {fk.parent.nullable for fk in fks}
|
|
if not nullable:
|
|
o2m_kws["cascade"] = "all, delete-orphan"
|
|
|
|
if (
|
|
constraint.ondelete
|
|
and constraint.ondelete.lower() == "cascade"
|
|
):
|
|
o2m_kws["passive_deletes"] = True
|
|
else:
|
|
if (
|
|
constraint.ondelete
|
|
and constraint.ondelete.lower() == "set null"
|
|
):
|
|
o2m_kws["passive_deletes"] = True
|
|
|
|
create_backref = backref_name not in referred_cfg.properties
|
|
|
|
if relationship_name not in map_config.properties:
|
|
if create_backref:
|
|
backref_obj = generate_relationship(
|
|
automap_base,
|
|
interfaces.ONETOMANY,
|
|
backref,
|
|
backref_name,
|
|
referred_cls,
|
|
local_cls,
|
|
collection_class=collection_class,
|
|
**o2m_kws,
|
|
)
|
|
else:
|
|
backref_obj = None
|
|
rel = generate_relationship(
|
|
automap_base,
|
|
interfaces.MANYTOONE,
|
|
relationship,
|
|
relationship_name,
|
|
local_cls,
|
|
referred_cls,
|
|
foreign_keys=[fk.parent for fk in constraint.elements],
|
|
backref=backref_obj,
|
|
remote_side=[fk.column for fk in constraint.elements],
|
|
)
|
|
if rel is not None:
|
|
map_config.properties[relationship_name] = rel
|
|
if not create_backref:
|
|
referred_cfg.properties[
|
|
backref_name
|
|
].back_populates = relationship_name # type: ignore[union-attr] # noqa: E501
|
|
elif create_backref:
|
|
rel = generate_relationship(
|
|
automap_base,
|
|
interfaces.ONETOMANY,
|
|
relationship,
|
|
backref_name,
|
|
referred_cls,
|
|
local_cls,
|
|
foreign_keys=[fk.parent for fk in constraint.elements],
|
|
back_populates=relationship_name,
|
|
collection_class=collection_class,
|
|
**o2m_kws,
|
|
)
|
|
if rel is not None:
|
|
referred_cfg.properties[backref_name] = rel
|
|
map_config.properties[
|
|
relationship_name
|
|
].back_populates = backref_name # type: ignore[union-attr]
|
|
|
|
|
|
def _m2m_relationship(
|
|
automap_base: Type[Any],
|
|
lcl_m2m: Table,
|
|
rem_m2m: Table,
|
|
m2m_const: List[ForeignKeyConstraint],
|
|
table: Table,
|
|
table_to_map_config: Union[
|
|
Dict[Optional[Table], _DeferredMapperConfig],
|
|
Dict[Table, _DeferredMapperConfig],
|
|
],
|
|
collection_class: type,
|
|
name_for_scalar_relationship: NameForCollectionRelationshipType,
|
|
name_for_collection_relationship: NameForCollectionRelationshipType,
|
|
generate_relationship: GenerateRelationshipType,
|
|
) -> None:
|
|
|
|
map_config = table_to_map_config.get(lcl_m2m, None)
|
|
referred_cfg = table_to_map_config.get(rem_m2m, None)
|
|
if map_config is None or referred_cfg is None:
|
|
return
|
|
|
|
local_cls = map_config.cls
|
|
referred_cls = referred_cfg.cls
|
|
|
|
relationship_name = name_for_collection_relationship(
|
|
automap_base, local_cls, referred_cls, m2m_const[0]
|
|
)
|
|
backref_name = name_for_collection_relationship(
|
|
automap_base, referred_cls, local_cls, m2m_const[1]
|
|
)
|
|
|
|
create_backref = backref_name not in referred_cfg.properties
|
|
|
|
if table in table_to_map_config:
|
|
overlaps = "__*"
|
|
else:
|
|
overlaps = None
|
|
|
|
if relationship_name not in map_config.properties:
|
|
if create_backref:
|
|
backref_obj = generate_relationship(
|
|
automap_base,
|
|
interfaces.MANYTOMANY,
|
|
backref,
|
|
backref_name,
|
|
referred_cls,
|
|
local_cls,
|
|
collection_class=collection_class,
|
|
overlaps=overlaps,
|
|
)
|
|
else:
|
|
backref_obj = None
|
|
|
|
rel = generate_relationship(
|
|
automap_base,
|
|
interfaces.MANYTOMANY,
|
|
relationship,
|
|
relationship_name,
|
|
local_cls,
|
|
referred_cls,
|
|
overlaps=overlaps,
|
|
secondary=table,
|
|
primaryjoin=and_(
|
|
fk.column == fk.parent for fk in m2m_const[0].elements
|
|
), # type: ignore [arg-type]
|
|
secondaryjoin=and_(
|
|
fk.column == fk.parent for fk in m2m_const[1].elements
|
|
), # type: ignore [arg-type]
|
|
backref=backref_obj,
|
|
collection_class=collection_class,
|
|
)
|
|
if rel is not None:
|
|
map_config.properties[relationship_name] = rel
|
|
|
|
if not create_backref:
|
|
referred_cfg.properties[
|
|
backref_name
|
|
].back_populates = relationship_name # type: ignore[union-attr] # noqa: E501
|
|
elif create_backref:
|
|
rel = generate_relationship(
|
|
automap_base,
|
|
interfaces.MANYTOMANY,
|
|
relationship,
|
|
backref_name,
|
|
referred_cls,
|
|
local_cls,
|
|
overlaps=overlaps,
|
|
secondary=table,
|
|
primaryjoin=and_(
|
|
fk.column == fk.parent for fk in m2m_const[1].elements
|
|
), # type: ignore [arg-type]
|
|
secondaryjoin=and_(
|
|
fk.column == fk.parent for fk in m2m_const[0].elements
|
|
), # type: ignore [arg-type]
|
|
back_populates=relationship_name,
|
|
collection_class=collection_class,
|
|
)
|
|
if rel is not None:
|
|
referred_cfg.properties[backref_name] = rel
|
|
map_config.properties[
|
|
relationship_name
|
|
].back_populates = backref_name # type: ignore[union-attr]
|