¿Cómo crear una vista SQL con SQLAlchemy?

82

¿Existe una forma "Pythonic" (es decir, no una consulta "SQL puro") para definir una vista SQL con SQLAlchemy?

Thibaut D.
fuente

Respuestas:

69

Actualización: consulte también la receta de uso de SQLAlchemy aquí

La creación de una vista (no materializada de solo lectura) no es compatible de forma inmediata, hasta donde yo sé. Pero agregar esta funcionalidad en SQLAlchemy 0.7 es sencillo (similar al ejemplo que di aquí ). Solo tienes que escribir una extensión del compilador CreateView . Con esta extensión, puede escribir (asumiendo que tes un objeto de tabla con una columna id)

createview = CreateView('viewname', t.select().where(t.c.id>5))
engine.execute(createview)

v = Table('viewname', metadata, autoload=True)
for r in engine.execute(v.select()):
    print r

Aquí hay un ejemplo de trabajo:

from sqlalchemy import Table
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.sql.expression import Executable, ClauseElement

class CreateView(Executable, ClauseElement):
    def __init__(self, name, select):
        self.name = name
        self.select = select

@compiles(CreateView)
def visit_create_view(element, compiler, **kw):
    return "CREATE VIEW %s AS %s" % (
         element.name,
         compiler.process(element.select, literal_binds=True)
         )

# test data
from sqlalchemy import MetaData, Column, Integer
from sqlalchemy.engine import create_engine
engine = create_engine('sqlite://')
metadata = MetaData(engine)
t = Table('t',
          metadata,
          Column('id', Integer, primary_key=True),
          Column('number', Integer))
t.create()
engine.execute(t.insert().values(id=1, number=3))
engine.execute(t.insert().values(id=9, number=-3))

# create view
createview = CreateView('viewname', t.select().where(t.c.id>5))
engine.execute(createview)

# reflect view and print result
v = Table('viewname', metadata, autoload=True)
for r in engine.execute(v.select()):
    print r

Si lo desea, también puede especializarse en un dialecto, p. Ej.

@compiles(CreateView, 'sqlite')
def visit_create_view(element, compiler, **kw):
    return "CREATE VIEW IF NOT EXISTS %s AS %s" % (
         element.name,
         compiler.process(element.select, literal_binds=True)
         )
Stephan
fuente
¿Puedo usar map v con orm.mapper? como v = Table('viewname', metadata, autoload=True) class ViewName(object): def __init__(self, name): self.name = name mapper(ViewName, v) arriba es posible? Porque usaré View con session.
Syed Habib M
1
@SyedHabibM: sí, esto es posible. Sin embargo, debe configurar manualmente la clave principal, algo como orm.mapper(ViewName, v, primary_key=pk, properties=prop)dónde pky propson su clave principal (o claves) y propiedades respectivamente. Consulte docs.sqlalchemy.org/en/latest/orm/… .
stephan
2
@SyedHabibM: también puede hacer lo que Stephan mencionó cuando usa tablas autocargadas anulando una especificación de columna y especificando un PK:v = Table('viewname', metadata, Column('my_id_column', Integer, primary_key=True), autoload=True)
van
@SyedHabibMI respondió a su pregunta respectiva stackoverflow.com/q/20518521/92092 con un ejemplo de trabajo ahora. También agregaré el comentario de Van.
stephan
27

La respuesta de Stephan es buena y cubre la mayoría de las bases, pero lo que me dejó insatisfecho fue la falta de integración con el resto de SQLAlchemy (el ORM, caída automática, etc.). Después de horas de experimentar y juntar conocimientos de todos los rincones de Internet, se me ocurrió lo siguiente:

import sqlalchemy_views
from sqlalchemy import Table
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.sql.ddl import DropTable


class View(Table):
    is_view = True


class CreateView(sqlalchemy_views.CreateView):
    def __init__(self, view):
        super().__init__(view.__view__, view.__definition__)


@compiles(DropTable, "postgresql")
def _compile_drop_table(element, compiler, **kwargs):
    if hasattr(element.element, 'is_view') and element.element.is_view:
        return compiler.visit_drop_view(element)

    # cascade seems necessary in case SQLA tries to drop 
    # the table a view depends on, before dropping the view
    return compiler.visit_drop_table(element) + ' CASCADE'

Tenga en cuenta que estoy utilizando el sqlalchemy_viewspaquete, solo para simplificar las cosas.

Definición de una vista (por ejemplo, globalmente como sus modelos de tabla):

from sqlalchemy import MetaData, text, Text, Column


class SampleView:
    __view__ = View(
        'sample_view', MetaData(),
        Column('bar', Text, primary_key=True),
    )

    __definition__ = text('''select 'foo' as bar''')

# keeping track of your defined views makes things easier
views = [SampleView]

Mapeo de las vistas (habilite la funcionalidad ORM):

Hágalo al cargar su aplicación, antes de cualquier consulta y después de configurar la base de datos.

for view in views:
    if not hasattr(view, '_sa_class_manager'):
        orm.mapper(view, view.__view__)

Creando las vistas:

Hágalo al inicializar la base de datos, por ejemplo, después de una llamada a create_all ().

from sqlalchemy import orm


for view in views:
    db.engine.execute(CreateView(view))

Cómo consultar una vista:

results = db.session.query(SomeModel, SampleView).join(
    SampleView,
    SomeModel.id == SampleView.some_model_id
).all()

Esto devolvería exactamente lo que espera (una lista de objetos que cada uno tiene un objeto SomeModel y un objeto SampleView).

Dejar caer una vista:

SampleView.__view__.drop(db.engine)

También se eliminará automáticamente durante una llamada drop_all ().

Obviamente, esta es una solución muy peligrosa, pero en mi opinión, es la mejor y la más limpia que existe en este momento. Lo he probado estos últimos días y no he tenido ningún problema. No estoy seguro de cómo agregar relaciones (tuve problemas allí) pero no es realmente necesario, como se demostró anteriormente en la consulta.

Si alguien tiene algún comentario, encuentra algún problema inesperado o conoce una mejor manera de hacer las cosas, por favor deje un comentario o hágamelo saber.

Esto se probó en SQLAlchemy 1.2.6 y Python 3.6.

fgblomqvist
fuente
Tiempo loco, solo he estado lidiando con esto yo mismo. Para py 2.7 y SQLa 1.1.2 (no preguntes ...), los únicos cambios necesarios eran super(CreateView, self).__init__y tener elclass SampleView(object)
Steven Dickinson
1
@Steven Dickinson ¡Sí, suena bien! Sí, pensé que esta es una tarea muy común, por lo que me sorprendió que la documentación fuera tan pobre / desactualizada / superficial. Pero bueno, un paso a la vez supongo.
fgblomqvist
2
Para aquellos que buscan hacer esto de manera declarativa, definí mis vistas en un archivo separado de mis tablas con una instancia de metadatos diferente: Base = declarative_base(metadata=db.MetaData()) class ViewSample(Base): __tablename__ = 'view_sample' aún __definition__incluí la propiedad y llamé a CreateView para crearla como se sugiere en la publicación original. Finalmente, tuve que modificar el método de decoración de gota: if element.element.name.startswith('view_'): return compiler.visit_drop_view(element) porque no pude encontrar una manera de agregar la propiedad a la tabla incrustada.
Casey
23

En estos días hay un paquete PyPI para eso: SQLAlchemy Views .

Desde su página PyPI:

>>> from sqlalchemy import Table, MetaData
>>> from sqlalchemy.sql import text
>>> from sqlalchemy_views import CreateView, DropView

>>> view = Table('my_view', metadata)
>>> definition = text("SELECT * FROM my_table")

>>> create_view = CreateView(view, definition, or_replace=True)
>>> print(str(create_view.compile()).strip())
CREATE OR REPLACE VIEW my_view AS SELECT * FROM my_table

Sin embargo, solicitó una consulta sin "SQL puro" , por lo que probablemente desee que lo definitionanterior se cree con el objeto de consulta SQLAlchemy.

Afortunadamente, text()en el ejemplo anterior deja claro que el definitionparámetro to CreateViewes un objeto de consulta. Entonces, algo como esto debería funcionar:

>>> from sqlalchemy import Table, Column, Integer, String, MetaData, ForeignKey
>>> from sqlalchemy.sql import select
>>> from sqlalchemy_views import CreateView, DropView

>>> metadata = MetaData()

>>> users = Table('users', metadata,
...     Column('id', Integer, primary_key=True),
...     Column('name', String),
...     Column('fullname', String),
... )

>>> addresses = Table('addresses', metadata,
...   Column('id', Integer, primary_key=True),
...   Column('user_id', None, ForeignKey('users.id')),
...   Column('email_address', String, nullable=False)
...  )

Aquí está la parte interesante:

>>> view = Table('my_view', metadata)
>>> definition = select([users, addresses]).where(
...     users.c.id == addresses.c.user_id
... )
>>> create_view = CreateView(view, definition, or_replace=True)
>>> print(str(create_view.compile()).strip())
CREATE OR REPLACE VIEW my_view AS SELECT users.id, users.name,
users.fullname, addresses.id, addresses.user_id, addresses.email_address 
FROM users, addresses 
WHERE users.id = addresses.user_id
LeoRochael
fuente
17

SQLAlchemy-utils acaba de agregar esta funcionalidad en 0.33.6 (disponible en pypi). Tiene vistas, vistas materializadas y se integra con el ORM. Aún no está documentado, pero estoy usando con éxito las vistas + ORM.

Puedes usar su prueba como ejemplo. para vistas tanto regulares como materializadas usando el ORM.

Para crear una vista, una vez que instale el paquete, use el siguiente código de la prueba anterior como base para su vista:

class ArticleView(Base):
    __table__ = create_view(
        name='article_view',
        selectable=sa.select(
            [
                Article.id,
                Article.name,
                User.id.label('author_id'),
                User.name.label('author_name')
            ],
            from_obj=(
                Article.__table__
                    .join(User, Article.author_id == User.id)
            )
        ),
        metadata=Base.metadata
    )

¿De dónde Baseestá declarative_base, saes el SQLAlchemypaquete y create_viewes una función sqlalchemy_utils.view?

bustawin
fuente
¿Has encontrado una forma de usarlo junto con alambique?
Jorge Leitao
@JorgeLeitao Lamentablemente no lo he probado.
bustawin
1

No pude encontrar una respuesta corta y práctica.

No necesito funcionalidad adicional de Ver (si corresponde), así que simplemente trato una vista como una tabla ordinaria como otras definiciones de tabla.

Básicamente, tengo a.pydónde define todas las tablas y vistas, cosas relacionadas con SQL y de main.pydónde importo esas clasesa.py y las uso.

Esto es lo que agrego a.pyy funciona:

class A_View_From_Your_DataBase(Base):
    __tablename__ = 'View_Name'
    keyword = Column(String(100), nullable=False, primary_key=True)

En particular, debe agregar la primary_keypropiedad aunque no haya una clave principal en la vista.

Almiar
fuente
-9

Vista SQL sin SQL puro? Puede crear una clase o función para implementar una vista definida.

function get_view(con):
  return Table.query.filter(Table.name==con.name).first()
corazón
fuente
2
Lo siento, pero eso no es lo que pregunté. Mi inglés no es perfecto, lo siento si no entendiste :)
Thibaut D.