Wie erstelle ich eine SQL-Ansicht mit SQLAlchemy?

83

Gibt es eine "pythonische" Möglichkeit (ich meine, keine "reine SQL" -Abfrage), eine SQL-Ansicht mit SQLAlchemy zu definieren?

Thibaut D.
quelle

Antworten:

69

Update: Siehe auch das SQLAlchemy-Verwendungsrezept hier

Das Erstellen einer (schreibgeschützten, nicht materialisierten) Ansicht wird meines Wissens nicht sofort unterstützt. Aber das Hinzufügen dieser Funktionalität in SQLAlchemy 0.7 ist einfach (ähnlich dem Beispiel gab ich hier ). Sie müssen nur eine Compiler-Erweiterung schreiben CreateView. Mit dieser Erweiterung können Sie dann schreiben (vorausgesetzt, es thandelt sich um ein Tabellenobjekt mit einer Spalte 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

Hier ist ein Arbeitsbeispiel:

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

Wenn Sie möchten, können Sie sich auch auf einen Dialekt spezialisieren, z

@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
quelle
Kann ich map v mit orm.mapper verwenden? wie v = Table('viewname', metadata, autoload=True) class ViewName(object): def __init__(self, name): self.name = name mapper(ViewName, v) oben ist möglich? Weil ich View with session verwenden werde.
Syed Habib M
1
@ SyedHabibM: Ja, das ist möglich. Sie müssen den Primärschlüssel jedoch manuell festlegen, z. B. orm.mapper(ViewName, v, primary_key=pk, properties=prop)wo pkund propsind Ihr Primärschlüssel (oder Ihre Primärschlüssel) bzw. Eigenschaften. Siehe docs.sqlalchemy.org/en/latest/orm/… .
stephan
2
@ SyedHabibM: Sie können das, was stephan erwähnt hat, auch tun, wenn Sie automatisch geladene Tabellen verwenden, indem Sie eine Spaltenspezifikation überschreiben und eine PK angeben:v = Table('viewname', metadata, Column('my_id_column', Integer, primary_key=True), autoload=True)
van
@SyedHabibMI hat Ihre jeweilige Frage stackoverflow.com/q/20518521/92092 jetzt mit einem funktionierenden Beispiel beantwortet . Ich werde dort auch Vans Kommentar hinzufügen.
stephan
27

Die Antwort von stephan ist gut und deckt die meisten Grundlagen ab, aber was mich unzufrieden machte, war die mangelnde Integration mit dem Rest der SQLAlchemy (ORM, automatisches Löschen usw.). Nachdem ich stundenlang experimentiert und Wissen aus allen Ecken des Internets zusammengesetzt hatte, kam ich auf Folgendes:

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'

Beachten Sie, dass ich das sqlalchemy_viewsPaket verwende, um die Dinge zu vereinfachen.

Definieren einer Ansicht (z. B. global wie Ihre Tabellenmodelle):

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]

Zuordnen der Ansichten (Aktivieren der ORM-Funktionalität):

Tun Sie dies beim Laden Ihrer App, vor allen Abfragen und nach dem Einrichten der Datenbank.

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

Ansichten erstellen:

Tun Sie dies beim Initialisieren der Datenbank, z. B. nach einem Aufruf von create_all ().

from sqlalchemy import orm


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

So fragen Sie eine Ansicht ab:

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

Dies würde genau das zurückgeben, was Sie erwarten (eine Liste von Objekten, die jeweils ein SomeModel-Objekt und ein SampleView-Objekt haben).

Ansicht löschen:

SampleView.__view__.drop(db.engine)

Es wird auch automatisch während eines Aufrufs von drop_all () gelöscht.

Dies ist offensichtlich eine sehr hackige Lösung, aber in meinen Augen ist es die beste und sauberste, die es derzeit gibt. Ich habe es in den letzten Tagen getestet und hatte keine Probleme. Ich bin nicht sicher, wie ich Beziehungen hinzufügen soll (dort sind Probleme aufgetreten), aber es ist nicht wirklich notwendig, wie oben in der Abfrage gezeigt.

Wenn jemand Eingaben hat, unerwartete Probleme findet oder eine bessere Vorgehensweise kennt, hinterlassen Sie bitte einen Kommentar oder lassen Sie es mich wissen.

Dies wurde auf SQLAlchemy 1.2.6 und Python 3.6 getestet.

fgblomqvist
quelle
Verrücktes Timing, habe mich gerade selbst darum gekümmert. Für py 2.7 und SQLa 1.1.2 (fragen Sie nicht ...) waren die einzigen erforderlichen Änderungen die super(CreateView, self).__init__und mit demclass SampleView(object)
Steven Dickinson
1
@Steven Dickinson yup klingt ungefähr richtig! Ja, ich dachte, dies ist eine sehr häufige Aufgabe, weshalb ich überrascht war, dass die Dokumentation so schlecht / veraltet / flach war. Aber hey, ich nehme an, Schritt für Schritt.
fgblomqvist
2
Für diejenigen, die dies deklarativ tun möchten, habe ich meine Ansichten in einer von meinen Tabellen getrennten Datei mit einer anderen Metadateninstanz definiert: Base = declarative_base(metadata=db.MetaData()) class ViewSample(Base): __tablename__ = 'view_sample' Ich habe die __definition__Eigenschaft weiterhin eingefügt und CreateView aufgerufen, um sie wie im ursprünglichen Beitrag vorgeschlagen zu erstellen. Schließlich musste ich die Drop-Decorated-Methode ändern: if element.element.name.startswith('view_'): return compiler.visit_drop_view(element) weil ich keine Möglichkeit fand, die Eigenschaft zur eingebetteten Tabelle hinzuzufügen.
Casey
23

Heutzutage gibt es dafür ein PyPI-Paket: SQLAlchemy Views .

Von der PyPI-Seite:

>>> 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

Sie haben jedoch nach einer Abfrage ohne "reines SQL" gefragt , daher möchten Sie wahrscheinlich, dass die definitionoben genannte Abfrage mit dem SQLAlchemy-Abfrageobjekt erstellt wird.

Glücklicherweise text()macht das obige Beispiel deutlich, dass der definitionParameter to CreateViewein solches Abfrageobjekt ist. So etwas sollte funktionieren:

>>> 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)
...  )

Hier ist das Interessante:

>>> 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
quelle
17

SQLAlchemy-utils hat diese Funktionalität gerade hinzugefügt in 0.33.6 hinzugefügt (verfügbar in pypi). Es hat Ansichten, materialisierte Ansichten und ist in das ORM integriert. Es ist noch nicht dokumentiert, aber ich verwende erfolgreich die Ansichten + ORM.

Sie können ihren Test als Beispiel verwenden für reguläre und materialisierte Ansichten mithilfe des ORM verwenden.

Verwenden Sie zum Erstellen einer Ansicht nach der Installation des Pakets den folgenden Code aus dem obigen Test als Grundlage für Ihre Ansicht:

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
    )

Wo Baseist das declarative_base, saist das SQLAlchemyPaket und create_viewist eine Funktion von sqlalchemy_utils.view.

Bustawin
quelle
Haben Sie einen Weg gefunden, es zusammen mit Destillierkolben zu verwenden?
Jorge Leitao
@JorgeLeitao Leider habe ich nicht versucht.
Bustawin
1

Ich konnte keine kurze und handliche Antwort finden.

Ich benötige keine zusätzlichen Funktionen von View (falls vorhanden), daher behandle ich eine Ansicht einfach als normale Tabelle wie andere Tabellendefinitionen.

Im Grunde habe ich also, a.pywo alle Tabellen und Ansichten definiert werden, SQL-bezogene Dinge und main.pywo ich diese Klasse importierea.py und verwende.

Folgendes füge ich hinzu a.pyund funktioniert:

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

Insbesondere müssen Sie die primary_keyEigenschaft hinzufügen , obwohl die Ansicht keinen Primärschlüssel enthält.

Rick
quelle
-9

SQL View ohne reines SQL? Sie können eine Klasse oder Funktion erstellen, um eine definierte Ansicht zu implementieren.

function get_view(con):
  return Table.query.filter(Table.name==con.name).first()
focusheart
quelle
2
Entschuldigung, aber das habe ich nicht gefragt. Mein Englisch ist nicht perfekt, es tut mir leid, wenn Sie falsch verstanden haben :)
Thibaut D.