1
votes

Empêcher SQLAlchemy de définir automatiquement IDENTITY_INSERT

J'ai une table SQL Server avec une clé primaire auto-incrémentée que je manipule à l'aide de SQLAlchemy. Si je crée une instance du modèle de cette table qui inclut la clé primaire au lieu de la laisser désactivée et d'appeler session.add (instance) , SQLAlchemy émet automatiquement SET IDENTITY_INSERT [table] ON code > dans la base de données et l'insertion réussit.

Existe-t-il un moyen de contrôler cela moi-même à la place? Je préfère obtenir une erreur si j'essaie d'insérer une clé primaire spécifique au lieu de la laisser être auto-incrémentée. Si j'ai vraiment besoin de définir la clé primaire, je préfère le dire explicitement.

Voici le modèle:

t = Tub(id=20, tare_weight=200)
session.merge(t)
session.commit()

Voici un exemple insert:

BEGIN
SET IDENTITY_INSERT [Tub] ON
INSERT INTO [Tub] ([ID], [TareWeight]) VALUES (20, 200)
SET IDENTITY_INSERT [Tub] OFF
COMMIT

Voici le SQL résultant:

t = Tub(id=20, tare_weight=200)
session.add(t)
session.commit()

Mise à jour: p >

Une autre situation pertinente que j'ai réalisée est la suivante:

Base = declarative_base()

class Tub(Base):
    __tablename__ = 'Tub'

    id = Column('ID', Integer, primary_key=True, autoincrement=True)
    tare_weight = Column('TareWeight', Float(53), nullable=False)

Si le bac existe déjà dans la base de données, je souhaite que son poids soit mis à jour. Si ce n'est pas le cas, je souhaite que l'insertion échoue car elle inclut une clé primaire explicite.


0 commentaires

4 Réponses :


3
votes

Selon la Documentation SQLAlchemy 1.3 :

Dans le cas où cette génération par défaut de IDENTITY n'est pas souhaitée, spécifiez False pour l'indicateur Column.autoincrement , sur le premier entier colonne de clé primaire:

m = MetaData()
t = Table('t', m,
        Column('id', Integer, primary_key=True, autoincrement=False),
        Column('x', Integer))
m.create_all(engine)


1 commentaires

Cependant, je veux que la colonne s'incrémente automatiquement, et c'est ainsi que je l'ai définie dans ma base de données. Le problème est que lorsque j'inclus accidentellement une valeur pour la colonne de clé primaire, SQLAlchemy remplace automatiquement la propriété d'identité de la colonne en appelant SET IDENTITY_INSERT ON . Je n'aime pas cette neutralisation automatique et silencieuse et préférerais qu'elle génère une erreur.



2
votes

Pourquoi ne pas simplement remplacer le constructeur?

import warnings

class AutoIncrMixin:
    id = Column('ID', Integer, primary_key=True, autoincrement=True)

    def __init__(self, id=None, **kwargs):
        if id is not None:
            warnings.warn("supplied `id` value was ignored")
        super().__init__(**kwargs)

class Tub(Base, AutoIncrMixin):
    tare_weight = ...

Si vous voulez que la même chose s'applique à de nombreuses classes, vous pouvez implémenter un mixin:

class Tub(Base):
    __tablename__ = 'Tub'

    id = Column('ID', Integer, primary_key=True, autoincrement=True)
    tare_weight = Column('TareWeight', Float(53), nullable=False)

    def __init__(self, id=None, **kwargs):
        if id is not None:
            raise ValueError("Hey, don't pass in `id`!")
            # or you could even just ignore that it was passed in and issue a warning.
        super().__init__(**kwargs)


3 commentaires

Par souci d'exhaustivité, cette solution doit également éviter de créer le Tub sans identifiant, puis de définir tub.id sur autre chose par la suite. Mais malheureusement, même avec cette amélioration, je ne pense pas pouvoir utiliser cette solution de contournement car j'ai toujours besoin de la possibilité de créer des Tubs avec des identifiants explicites. Le cas d'utilisation est que je veux pouvoir mettre à jour un Tub existant à partir de la base de données via les étapes: 1) alimenter JSON d'une requête PUT / PATCH à marshmallow-sqlalchemy pour générer un objet Tub, 2) définir l'id explicitement à partir du URL, 3) appelez session.merge () pour mettre à jour le Tub correspondant dans la base de données.


Et tout comme avec session.add () , je ne veux pas que session.merge () remplace automatiquement la caractéristique d'auto-incrémentation de la colonne de base de données pour insérer de force un Tub avec un identifiant explicite qui n'est pas déjà présent dans la table. Je veux juste dire à SQLAlchemy: "Dans l'ensemble, ne désactivez jamais automatiquement le comportement d'auto-incrémentation pour faire une insertion."


Bien que cette réponse ne soit pas quelque chose que je puisse utiliser dans mon application actuelle, j'ai décidé de +1 car elle semble toujours utile. En ce qui concerne mon commentaire selon lequel il devrait également interdire la définition de la clé primaire de l'extérieur du constructeur (par exemple tub = Tub (...); tub.id = 5 ), je pensais envelopper la colonne id dans une propriété peut fonctionner, comme dans cette réponse: stackoverflow.com/a/2317843/5405967 . Si cela a fonctionné, cela devrait supprimer le besoin d'un constructeur spécial. Je ne suis pas allé jusqu'à essayer de le mettre en œuvre, mais j'ai pensé que je mentionnerais l'idée de celui qui finit par lire ceci.



1
votes

À long terme, vous pourriez faire un patching complet de singe. Je ne pense pas que cela résout tous vos problèmes, mais cela empêche l'utilisation de SET IDENTITY_INSERT . Vous pourriez singe patcher MSExecutionContext s pre_exec et post_exec . Par exemple:

from sqlalchemy import engine
from sqlalchemy.dialects.mssql.base import MSExecutionContext

def pre_exec(self):
    if self.isinsert:
        tbl = self.compiled.statement.table
        seq_column = tbl._autoincrement_column
        insert_has_sequence = seq_column is not None

        self._select_lastrowid = (
            not self.compiled.inline
            and insert_has_sequence
            and not self.compiled.returning
            and not self._enable_identity_insert
            and not self.executemany
        )


def post_exec(self):
    conn = self.root_connection
    if self._select_lastrowid:
        if self.dialect.use_scope_identity:
            conn._cursor_execute(
                self.cursor,
                "SELECT scope_identity() AS lastrowid",
                (),
                self,
            )
        else:
            conn._cursor_execute(
                self.cursor, "SELECT @@identity AS lastrowid", (), self
            )
        # fetchall() ensures the cursor is consumed without closing it
        row = self.cursor.fetchall()[0]
        self._lastrowid = int(row[0])

    if (
        self.isinsert or self.isupdate or self.isdelete
    ) and self.compiled.returning:
        self._result_proxy = engine.FullyBufferedResultProxy(self)


MSExecutionContext.pre_exec = pre_exec
MSExecutionContext.post_exec = post_exec

Mon exemple brut de ceci en cours d'utilisation monkey corrige les fonctions qui utilisent la fonctionnalité _enable_identity_insert et supprime ces parties. Désormais, toute tentative pour INSÉRER spécifiquement un identifiant vous donnera l'erreur suivante:

sqlalchemy.exc.IntegrityError: (pyodbc.IntegrityError) ('23000', "[23000] [Microsoft] [SQL Server Native Client 11.0] [SQL Server] Impossible d'insérer une valeur explicite pour la colonne d'identité dans la table" Tub "lorsque IDENTITY_INSERT est réglé sur OFF. (544) (SQLExecDirectW) ")

Votre problème maintenant sera probablement que vous dépendez entièrement de la fonctionnalité d'incrémentation automatique, mais peut-être que cela sera utile à certains, ou vous trouverez un autre moyen de contourner cette fonctionnalité.


4 commentaires

Wow, c'est génial / intense. Je l'ai essayé et cela a fonctionné correctement pour add () et merge () pour une table avec __table_args__ = {'implicit_returning': False} . Cependant, par curiosité, j'ai désactivé les déclencheurs de la table et supprimé cette ligne d'arguments de table, et elle a cessé de fonctionner. Maintenant, sur la ligne self._result_proxy = engine.FullyBufferedResultProxy (self) , j'obtiens AttributeError: l'objet 'Engine' n'a pas d'attribut 'FullyBufferedResultProxy' . Aussi, qu'est-ce que l'utilisation de @@ identity au lieu de OUTPUT ? N'est-ce pas dangereux?


Pour clarifier, j'ai testé à la fois des ajouts et des fusions qui devraient réussir ou échouer. Avec implicit_returning = False , les quatre scénarios fonctionnent correctement. Avec implicit_returning = True , tous les scénarios sauf un fonctionnent toujours. Celui qui entraîne l'erreur est un ajout qui devrait réussir car il n'implique pas de clé primaire explicite: session.add (Tub (tare_weight = 100)); session.commit () .


@MarredCheese Ah, je vois. Il a besoin de du moteur d'importation sqlalchemy , alors peut-être l'alias pour éviter une collision avec le moteur existant. Comment ça marche pour vous?


L'ajout de à partir du moteur d'importation sqlalchemy a fonctionné. Bon travail. J'accepterai cette réponse car je doute que quelque chose de plus simple soit publié. J'espérais vraiment qu'il y aurait une option de configuration simple pour contrôler ce comportement au lieu d'avoir besoin de monkey-patch core fonctionnalité. Il me semble tellement évident que le comportement par défaut consiste à remplacer silencieusement une règle de base de données.



0
votes

J'ajoute ma propre réponse pour développer mes commentaires sur la réponse de @ SuperShoot. Je pense que si vous allez faire une solution de contournement où vous empêchez la clé primaire d'obtenir une valeur en premier lieu, vous devriez le faire via une propriété plutôt qu'un constructeur:

tub = Tub(_Tub__id=5, tare_weight=100)

or

tub = Tub(tare_weight=100)
tub._Tub__id = 5


4 commentaires

Vos commentaires sur le fait d'exiger que cela fonctionne avec la fusion sont la raison exacte pour laquelle je n'ai pas mis à jour ma réponse pour inclure ce modèle. Avez-vous jeté un coup d'œil aux hooks d'événements pour voir s'il y a quelque chose que vous pouvez y accrocher? Je voulais jeter un coup d'œil mais trop occupé.


@SuperShoot Ouais, j'ai pensé. Pas de soucis. (Je suppose que cela doit vous avoir envoyé un ping depuis que je l'ai fait @ avant votre nom. Je n'avais même pas réalisé que c'était une chose dans les réponses - cool.) En ce qui concerne ce problème d'insertion d'identité, j'ai décidé de passer à autre chose parce que ce n'est pas critique - juste quelque chose qui m'a déconcerté.


Aucune notification, ouvrez simplement un onglet avec le flux de balises que je regarde. Heureux que vous l'ayez trié à votre satisfaction alors. Curieux, avez-vous implémenté la solution Monkey Patch?


@SuperShoot Avec le patch monkey, il semble que le fardeau de la complexité / maintenance est injustifié juste pour faire face à quelque chose qui me dérange. Je vais probablement faire cette chose de propriété pour les cas où je n'ai pas besoin de fusionner et de ne rien faire autrement.