123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224 |
- # ext/automap.py
- # Copyright (C) 2005-2022 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.
- .. versionadded:: 0.9.1 Added :mod:`sqlalchemy.ext.automap`.
- 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.
- 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(engine, reflect=True)
- # 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"
- 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
- Specifying Classes Explicitly
- =============================
- 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(engine, reflect=True)
- # 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(engine, reflect=True,
- 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(engine, reflect=True,
- 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(engine, reflect=True,
- 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(engine, reflect=True)
- 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(engine, reflect=True)
- .. 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 .. 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_
- def classname_for_table(base, tablename, table):
- """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)
- def name_for_scalar_relationship(base, local_cls, referred_cls, constraint):
- """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()
- def name_for_collection_relationship(
- base, local_cls, referred_cls, constraint
- ):
- """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"
- def generate_relationship(
- base, direction, return_fn, attrname, local_cls, referred_cls, **kw
- ):
- 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)
- class AutomapBase(object):
- """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 = None
- """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(engine=some_engine, reflect=True)
- User, Address = Base.classes.User, Base.classes.Address
- """
- @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,
- autoload_with=None,
- engine=None,
- reflect=False,
- schema=None,
- classname_for_table=None,
- collection_class=None,
- name_for_scalar_relationship=None,
- name_for_collection_relationship=None,
- generate_relationship=None,
- reflection_options=util.EMPTY_DICT,
- ):
- """Extract mapped classes and relationships from the
- :class:`_schema.MetaData` and
- perform mappings.
- :param engine: an :class:`_engine.Engine` or
- :class:`_engine.Connection` with which
- to perform schema reflection, if specified.
- If the :paramref:`.AutomapBase.prepare.reflect` argument is False,
- this object is not used.
- :param reflect: if True, the :meth:`_schema.MetaData.reflect`
- method is called
- on the :class:`_schema.MetaData` associated with this
- :class:`.AutomapBase`.
- The :class:`_engine.Engine` passed via
- :paramref:`.AutomapBase.prepare.engine` will be used to perform the
- reflection if present; else, the :class:`_schema.MetaData`
- should already be
- bound to some engine else the operation will fail.
- :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 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: When present in conjunction with the
- :paramref:`.AutomapBase.prepare.reflect` flag, is passed to
- :meth:`_schema.MetaData.reflect`
- to indicate the primary schema where tables
- should be reflected from. When omitted, the default schema in use
- by the database connection is used.
- .. versionadded:: 1.1
- :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
- """
- 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:
- opts = dict(
- schema=schema,
- extend_existing=True,
- autoload_replace=False,
- )
- if reflection_options:
- opts.update(reflection_options)
- cls.metadata.reflect(autoload_with, **opts)
- with _CONFIGURE_MUTEX:
- table_to_map_config = dict(
- (m.local_table, m)
- for m in _DeferredMapperConfig.classes_for_base(
- cls, sort=False
- )
- )
- many_to_many = []
- for table in cls.metadata.tables.values():
- lcl_m2m, rem_m2m, m2m_const = _is_many_to_many(cls, table)
- if lcl_m2m 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:
- mapped_cls = type(
- classname_for_table(cls, table.name, table),
- (cls,),
- {"__table__": table},
- )
- map_config = _DeferredMapperConfig.config_for_cls(
- mapped_cls
- )
- cls.classes[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(
- cls,
- 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(
- cls,
- 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(cls):
- 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):
- 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),
- )
- def automap_base(declarative_base=None, **kw):
- 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({})},
- )
- def _is_many_to_many(automap_base, table):
- fk_constraints = [
- const
- for const in table.constraints
- if isinstance(const, ForeignKeyConstraint)
- ]
- if len(fk_constraints) != 2:
- return None, None, None
- cols = 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,
- map_config,
- table_to_map_config,
- collection_class,
- name_for_scalar_relationship,
- name_for_collection_relationship,
- generate_relationship,
- ):
- local_table = map_config.local_table
- local_cls = 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 = {}
- 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
- 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
- def _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,
- ):
- 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
- ),
- secondaryjoin=and_(
- fk.column == fk.parent for fk in m2m_const[1].elements
- ),
- 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
- 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
- ),
- secondaryjoin=and_(
- fk.column == fk.parent for fk in m2m_const[0].elements
- ),
- 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
|