web-dev-qa-db-fra.com

Comment créer une vue SQL avec SQLAlchemy?

Tout est dans le titre. Existe-t-il un moyen "pythonique" (je veux dire, aucune requête "pure SQL") pour définir une vue SQL avec SQLAlchemy?

Merci de votre aide,

53
Thibaut D.

Update: Voir aussi la recette d'utilisation de SQLAlchemy ici

Pour autant que je sache, la création d'une vue (en lecture seule non matérialisée) n'est pas prise en charge immédiatement. Mais ajouter cette fonctionnalité dans SQLAlchemy 0.7 est simple (semblable à l'exemple que j'ai donné ici ). Il vous suffit d'écrire une extension compilerCreateView. Avec cette extension, vous pouvez alors écrire (en supposant que t est un objet de table avec une colonne 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

Voici un exemple de travail:

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 vous le souhaitez, vous pouvez également vous spécialiser pour un dialecte, par exemple.

@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)
         )
57
stephan

De nos jours, il existe un paquet PyPI pour cela: SQLAlchemy Views .

De sa page 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

Cependant, vous avez demandé une pas de requête "SQL pur", vous souhaitez donc probablement que la variable definition ci-dessus soit créée avec l'objet requête SQLAlchemy.

Heureusement, la fonction text() de l'exemple ci-dessus montre clairement que le paramètre definition de CreateView est un tel objet de requête. Donc, quelque chose comme ça devrait marcher:

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

Voici le bit intéressant:

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

la réponse de Stephan est bonne et couvre la plupart des bases, mais ce qui m'a laissé insatisfait était le manque d'intégration avec le reste de SQLAlchemy (l'ORM, la suppression automatique, etc.). Après des heures d’expérimentation et de compilation de connaissances provenant de tous les coins d’Internet, j’ai trouvé ce qui suit:

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'

Notez que j'utilise le paquetage sqlalchemy_views, juste pour simplifier les choses.

Définition d'une vue (par exemple, globalement, comme vos modèles de 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]

Mappage des vues (activer la fonctionnalité ORM):

Do lors du chargement de votre application, avant toute requête et après la configuration de la base de données.

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

Création des vues:

Do lors de l'initialisation de la base de données, par exemple après un appel à create_all ().

from sqlalchemy import orm


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

Comment interroger une vue:

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

Cela renverrait exactement ce que vous attendez (une liste d'objets ayant chacun un objet SomeModel et un objet SampleView).

Déposer une vue:

SampleView.__view__.drop(db.engine)

Il sera également automatiquement supprimé lors d'un appel drop_all ().

C’est évidemment une solution très astucieuse, mais à mes yeux, c’est la meilleure solution et la plus propre en ce moment. Je l'ai testé ces derniers jours et je n'ai rencontré aucun problème. Je ne sais pas comment ajouter des relations (problèmes rencontrés ici), mais ce n'est pas vraiment nécessaire, comme le montre la requête ci-dessus.

Si quelqu'un a des idées, découvre des problèmes inattendus ou connaît une meilleure façon de faire les choses, veuillez laisser un commentaire ou laissez-moi savoir.

Ceci a été testé sur SQLAlchemy 1.2.6 et Python 3.6.

11
fgblomqvist

SQLAlchemy-utils vient d'ajouter cette fonctionnalité dans 0.33.6 (disponible dans pypi). Il comporte des vues, des vues matérialisées et s'intègre à l'ORM. Ce n'est pas encore documenté, mais j'utilise avec succès les vues + ORM.

Vous pouvez utiliser leur test à titre d'exemple pour les vues régulières et matérialisées à l'aide de l'ORM. 

Pour créer une vue, une fois le package installé, utilisez le code suivant du test ci-dessus comme base pour votre vue:

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
    )

Base est le declarative_base, sa est le package SQLAlchemy et create_view est une fonction de sqlalchemy_utils.view.

3
bustawin

Je n'ai pas pu trouver de réponse courte et pratique.

Je n'ai pas besoin de fonctionnalités supplémentaires de View (le cas échéant). Je considère donc simplement une vue comme une table ordinaire comme toute autre définition de table.

Donc, fondamentalement, j'ai a.py où définit toutes les tables et vues, tout ce qui concerne SQL, et main.py où j'importe ces classes de a.py et les utilise.

Voici ce que j'ajoute dans a.py et fonctionne:

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

Notamment, vous devez ajouter la propriété primary_key même s'il n'y a pas de clé primaire dans la vue. 

0
Rick