Jak utworzyć widok SQL za pomocą SQLAlchemy?

84

Czy istnieje sposób „Pythonic” (mam na myśli brak zapytania „czystego SQL”) na zdefiniowanie widoku SQL za pomocą SQLAlchemy?

Thibaut D.
źródło

Odpowiedzi:

69

Aktualizacja: Zobacz także przepis na użycie SQLAlchemy tutaj

Tworzenie niezmaterializowanego widoku (tylko do odczytu) nie jest obsługiwane po wyjęciu z pudełka, o ile wiem. Ale dodanie tej funkcjonalności w SQLAlchemy 0.7 jest proste (podobnie jak w przykładzie, który tu podałem ). Musisz tylko napisać rozszerzenie kompilatora CreateView . Dzięki temu rozszerzeniu możesz następnie pisać (zakładając, że tjest to obiekt tabeli z kolumną 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

Oto działający przykład:

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

Jeśli chcesz, możesz też specjalizować się w dialekcie, np

@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
źródło
Czy mogę używać mapy v z orm.mapper? jak v = Table('viewname', metadata, autoload=True) class ViewName(object): def __init__(self, name): self.name = name mapper(ViewName, v) powyżej jest możliwe? Ponieważ użyję View z sesją.
Syed Habib M,
1
@SyedHabibM: tak, to jest możliwe. Musisz jednak ręcznie ustawić klucz podstawowy, coś w rodzaju, orm.mapper(ViewName, v, primary_key=pk, properties=prop)gdzie pki propsą odpowiednio klucz podstawowy (lub klucze) i właściwości. Zobacz docs.sqlalchemy.org/en/latest/orm/… .
Stephan
2
@SyedHabibM: można zrobić co Stephan wspomniano podczas korzystania ładowane automatycznie tabele też nadrzędnymi specyfikację kolumny i określić PK:v = Table('viewname', metadata, Column('my_id_column', Integer, primary_key=True), autoload=True)
van
@SyedHabibMI odpowiedział na Twoje pytanie stackoverflow.com/q/20518521/92092 z działającym przykładem. Dodam tam również komentarz van.
Stephan
27

Odpowiedź Stephana jest dobra i obejmuje większość baz, ale niezadowolony był brak integracji z resztą SQLAlchemy (ORM, automatyczne usuwanie itp.). Po godzinach eksperymentowania i zbierania wiedzy ze wszystkich zakątków internetu wymyśliłem:

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'

Zwróć uwagę, że korzystam z sqlalchemy_viewspakietu, aby uprościć sprawę.

Definiowanie widoku (np. Globalnie jak modele Table):

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]

Mapowanie widoków (włącz funkcjonalność ORM):

Zrób to podczas ładowania aplikacji, przed zapytaniami i po skonfigurowaniu bazy danych.

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

Tworzenie widoków:

Zrób podczas inicjalizacji bazy danych, np. Po wywołaniu create_all ().

from sqlalchemy import orm


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

Jak zapytać o widok:

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

To zwróci dokładnie to, czego oczekujesz (listę obiektów, z których każdy ma obiekt SomeModel i obiekt SampleView).

Upuszczanie widoku:

SampleView.__view__.drop(db.engine)

Zostanie również automatycznie usunięty podczas wywołania drop_all ().

To oczywiście bardzo hakerskie rozwiązanie, ale moim zdaniem jest to obecnie najlepsze i najczystsze rozwiązanie. Testowałem to przez ostatnie kilka dni i nie miałem żadnych problemów. Nie jestem pewien, jak dodać relacje (napotkałem tam problemy), ale nie jest to konieczne, jak pokazano powyżej w zapytaniu.

Jeśli ktoś ma jakieś uwagi, znajdzie jakieś nieoczekiwane problemy lub zna lepszy sposób na zrobienie czegoś, zostaw komentarz lub daj mi znać.

Zostało to przetestowane w SQLAlchemy 1.2.6 i Python 3.6.

fgblomqvist
źródło
Szalone wyczucie czasu, sam sobie z tym radziłem. Dla py 2.7 i SQLa 1.1.2 (nie pytaj ...), jedynymi potrzebnymi zmianami były super(CreateView, self).__init__i posiadająceclass SampleView(object)
Steven Dickinson
1
@Steven Dickinson tak brzmi dobrze! Tak, pomyślałem, że to naprawdę powszechne zadanie, dlatego zdziwiłem się, że dokumentacja na ten temat jest tak słaba / nieaktualna / płytka. Ale hej, chyba jeden krok na raz.
fgblomqvist
2
Dla tych, którzy chcą to zrobić deklaratywnie, zdefiniowałem moje widoki w oddzielnym pliku z moich tabel z inną instancją metadanych: Base = declarative_base(metadata=db.MetaData()) class ViewSample(Base): __tablename__ = 'view_sample' nadal uwzględniłem __definition__właściwość i wywołałem CreateView, aby utworzyć ją zgodnie z sugestią w oryginalnym poście. Na koniec musiałem zmodyfikować metodę dekorowanego upuszczania: if element.element.name.startswith('view_'): return compiler.visit_drop_view(element) ponieważ nie mogłem znaleźć sposobu na dodanie właściwości do tabeli osadzonej.
Casey
23

Obecnie jest do tego pakiet PyPI: SQLAlchemy Views .

Z jego strony 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

Jednak poprosiłeś o zapytanie „czystego SQL” , więc prawdopodobnie chcesz, aby definitionpowyższe zostało utworzone za pomocą obiektu zapytania SQLAlchemy.

Na szczęście text()w powyższym przykładzie widać, że definitionparametr to CreateViewjest takim obiektem zapytania. Więc coś takiego powinno działać:

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

Oto interesujący fragment:

>>> 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
źródło
17

SQLAlchemy-utils właśnie dodało tę funkcjonalność w wersji 0.33.6 (dostępna w pypi). Ma poglądy, zmaterializowane widoki i integruje się z ORM. Nie jest to jeszcze udokumentowane, ale z powodzeniem używam widoków + ORM.

Możesz użyć ich testu jako przykładu zarówno dla zwykłych, jak i zmaterializowanych widoków przy użyciu ORM.

Aby utworzyć widok, po zainstalowaniu pakietu użyj następującego kodu z powyższego testu jako podstawy widoku:

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
    )

Gdzie Basejest declarative_base, sajest SQLAlchemypakietem i create_viewjest funkcją z sqlalchemy_utils.view.

bustawin
źródło
Czy znalazłeś sposób na użycie go razem z alembikiem?
Jorge Leitao
1

Nie mogłem znaleźć krótkiej i przydatnej odpowiedzi.

Nie potrzebuję dodatkowej funkcjonalności widoku (jeśli w ogóle), więc po prostu traktuję widok jako zwykłą tabelę, tak jak inne definicje tabel.

Więc w zasadzie mam a.pygdzie definiuje wszystkie tabele i widoki, rzeczy związane z sql i main.pygdzie importuję te klasy a.pyi używam ich.

Oto, co dodam a.pyi działa:

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

Warto zauważyć, że musisz dodać primary_keywłaściwość, mimo że w widoku nie ma klucza podstawowego.

Stóg
źródło
-9

Widok SQL bez czystego SQL? Możesz utworzyć klasę lub funkcję, aby zaimplementować zdefiniowany widok.

function get_view(con):
  return Table.query.filter(Table.name==con.name).first()
focusheart
źródło
2
Przepraszam, ale nie o to prosiłem. Mój angielski nie jest doskonały, przepraszam, jeśli źle zrozumiałeś :)
Thibaut D.