méthode d'itération sur les colonnes définies du modèle sqlalchemy?

95

J'ai essayé de comprendre comment parcourir la liste des colonnes définies dans un modèle SQLAlchemy. Je le veux pour écrire des méthodes de sérialisation et de copie sur quelques modèles. Je ne peux pas simplement parcourir le obj.__dict__car il contient beaucoup d'éléments spécifiques à SA.

Quelqu'un connaît-il un moyen d'obtenir simplement les noms idet les descnoms suivants?

class JobStatus(Base):
    __tablename__ = 'jobstatus'

    id = Column(Integer, primary_key=True)
    desc = Column(Unicode(20))

Dans ce petit cas, je pourrais facilement créer un:

def logme(self):
    return {'id': self.id, 'desc': self.desc}

mais je préférerais quelque chose qui génère automatiquement le dict(pour les objets plus grands).

Meule
la source

Réponses:

84

Vous pouvez utiliser la fonction suivante:

def __unicode__(self):
    return "[%s(%s)]" % (self.__class__.__name__, ', '.join('%s=%s' % (k, self.__dict__[k]) for k in sorted(self.__dict__) if '_sa_' != k[:4]))

Cela exclura les attributs magiques SA , mais n'exclura pas les relations. Donc, fondamentalement, cela pourrait charger les dépendances, les parents, les enfants, etc., ce qui n'est certainement pas souhaitable.

Mais c'est en fait beaucoup plus facile car si vous héritez de Base, vous avez un __table__attribut, donc vous pouvez faire:

for c in JobStatus.__table__.columns:
    print c

for c in JobStatus.__table__.foreign_keys:
    print c

Voir Comment découvrir les propriétés de table à partir d'un objet mappé SQLAlchemy - question similaire.

Édité par Mike: veuillez consulter les fonctions telles que Mapper.c et Mapper.mapped_table . Si vous utilisez 0.8 et supérieur, consultez également Mapper.attrs et les fonctions associées.

Exemple pour Mapper.attrs :

from sqlalchemy import inspect
mapper = inspect(JobStatus)
for column in mapper.attrs:
    print column.key
van
la source
21
Notez que __table__.columnscela vous donnera les noms de champs SQL, pas les noms d'attributs que vous avez utilisés dans vos définitions ORM (si les deux diffèrent).
Josh Kelley
11
Puis-je recommander de changer '_sa_' != k[:4]pour not k.startswith('_sa_')?
Mu Mind
12
Pas besoin de boucler avec inspect(JobStatus).columns.keys()
inspect
63

Vous pouvez obtenir la liste des propriétés définies à partir du mappeur. Pour votre cas, seuls les objets ColumnProperty vous intéressent.

from sqlalchemy.orm import class_mapper
import sqlalchemy

def attribute_names(cls):
    return [prop.key for prop in class_mapper(cls).iterate_properties
        if isinstance(prop, sqlalchemy.orm.ColumnProperty)]
Fourmis Aasma
la source
4
Merci, cela m'a permis de créer une méthode todict sur Base que j'utilise ensuite pour `` vider '' une instance vers un dict que je peux ensuite passer pour la réponse du décorateur jsonify de pylon. J'ai essayé de mettre une note plus détaillée avec un exemple de code dans ma question initiale, mais cela provoque une erreur de stackoverflow lors de la soumission.
Rick
4
btw, class_mapperdoit être importé desqlalchemy.orm
Priestc
3
Bien que ce soit une réponse légitime, après la version 0.8, il est suggéré d'utiliser inspect(), qui renvoie exactement le même objet mappeur que class_mapper(). docs.sqlalchemy.org/en/latest/core/inspection.html
kirpit
1
Cela m'a beaucoup aidé à mapper les noms de propriétés de modèle SQLAlchemy aux noms de colonnes sous-jacentes.
FearlessFuture
29

Je me rends compte que c'est une vieille question, mais je viens de rencontrer la même exigence et je voudrais proposer une solution alternative aux futurs lecteurs.

Comme le note Josh, les noms de champ SQL complets seront retournés par JobStatus.__table__.columns, donc plutôt que l' id du nom de champ d'origine , vous obtiendrez jobstatus.id . Pas aussi utile que cela pourrait être.

La solution pour obtenir une liste de noms de champs tels qu'ils ont été définis à l'origine consiste à rechercher l' _dataattribut sur l'objet de colonne, qui contient les données complètes. Si nous regardons JobStatus.__table__.columns._data, cela ressemble à ceci:

{'desc': Column('desc', Unicode(length=20), table=<jobstatus>),
 'id': Column('id', Integer(), table=<jobstatus>, primary_key=True, nullable=False)}

De là, vous pouvez simplement appeler JobStatus.__table__.columns._data.keys()ce qui vous donne une belle liste propre:

['id', 'desc']
morphiques
la source
2
Agréable! Y a-t-il un moyen avec cette méthode d'établir des relations également?
shroud
3
il n'y a pas besoin de _data attr, juste ..columns.keys () font l'affaire
Humoyun Ahmad
1
Oui, l'utilisation de l'attribut private _data doit être évitée dans la mesure du possible, @Humoyun est plus correct.
Ng Oon-Ee
AttributeError: __data
13

self.__table__.columnsvous donnera "seulement" les colonnes définies dans cette classe particulière, c'est-à-dire sans celles héritées. si vous avez besoin de tout, utilisez self.__mapper__.columns. dans votre exemple, j'utiliserais probablement quelque chose comme ceci:

class JobStatus(Base):

    ...

    def __iter__(self):
        values = vars(self)
        for attr in self.__mapper__.columns.keys():
            if attr in values:
                yield attr, values[attr]

    def logme(self):
        return dict(self)
Andi Zeidler
la source
10

En supposant que vous utilisez le mappage déclaratif de SQLAlchemy, vous pouvez utiliser l' __mapper__attribut pour accéder au mappeur de classe. Pour obtenir tous les attributs mappés (y compris les relations):

obj.__mapper__.attrs.keys()

Si vous voulez des noms de colonnes strictement, utilisez obj.__mapper__.column_attrs.keys(). Consultez la documentation pour d'autres vues.

https://docs.sqlalchemy.org/en/latest/orm/mapping_api.html#sqlalchemy.orm.mapper.Mapper.attrs

jrc
la source
C'est la réponse simple.
stgrmks le
7

Pour obtenir une as_dictméthode sur toutes mes classes, j'ai utilisé une Mixinclasse qui utilise les techniques décrites par Ants Aasma .

class BaseMixin(object):                                                                                                                                                                             
    def as_dict(self):                                                                                                                                                                               
        result = {}                                                                                                                                                                                  
        for prop in class_mapper(self.__class__).iterate_properties:                                                                                                                                 
            if isinstance(prop, ColumnProperty):                                                                                                                                                     
                result[prop.key] = getattr(self, prop.key)                                                                                                                                           
        return result

Et puis utilisez-le comme ça dans vos cours

class MyClass(BaseMixin, Base):
    pass

De cette façon, vous pouvez appeler ce qui suit sur une instance de MyClass.

> myclass = MyClass()
> myclass.as_dict()

J'espère que cela t'aides.


J'ai joué un peu plus loin avec cela, j'avais en fait besoin de rendre mes instances dictsous la forme d'un objet HAL avec ses liens vers des objets associés. J'ai donc ajouté cette petite magie ici, qui explorera toutes les propriétés de la classe comme ci-dessus, à la différence que je vais explorer plus en profondeur les Relaionshippropriétés et les générer linksautomatiquement.

Veuillez noter que cela ne fonctionnera que pour les relations ayant une seule clé primaire

from sqlalchemy.orm import class_mapper, ColumnProperty
from functools import reduce


def deepgetattr(obj, attr):
    """Recurses through an attribute chain to get the ultimate value."""
    return reduce(getattr, attr.split('.'), obj)


class BaseMixin(object):
    def as_dict(self):
        IgnoreInstrumented = (
            InstrumentedList, InstrumentedDict, InstrumentedSet
        )
        result = {}
        for prop in class_mapper(self.__class__).iterate_properties:
            if isinstance(getattr(self, prop.key), IgnoreInstrumented):
                # All reverse relations are assigned to each related instances
                # we don't need to link these, so we skip
                continue
            if isinstance(prop, ColumnProperty):
                # Add simple property to the dictionary with its value
                result[prop.key] = getattr(self, prop.key)
            if isinstance(prop, RelationshipProperty):
                # Construct links relaions
                if 'links' not in result:
                    result['links'] = {}

                # Get value using nested class keys
                value = (
                    deepgetattr(
                        self, prop.key + "." + prop.mapper.primary_key[0].key
                    )
                )
                result['links'][prop.key] = {}
                result['links'][prop.key]['href'] = (
                    "/{}/{}".format(prop.key, value)
                )
        return result
flazzarini
la source
Veuillez ajouter from sqlalchemy.orm import class_mapper, ColumnPropertyen haut de votre extrait de code
JVK
Merci pour votre commentaire! J'ai ajouté les importations manquantes
flazzarini
C'est la base déclarative de sqlalchemy pour en savoir plus ici docs.sqlalchemy.org/en/13/orm/extensions/declarative
...
1
self.__dict__

renvoie un dict où les clés sont des noms d'attributs et valorise les valeurs de l'objet.

/! \ il y a un attribut supplémentaire: '_sa_instance_state' mais vous pouvez le gérer :)

Valentin Fabianski
la source
Uniquement lorsque les attributs sont définis.
stgrmks le
-1

Je sais que c'est une vieille question, mais qu'en est-il:

class JobStatus(Base):

    ...

    def columns(self):
        return [col for col in dir(self) if isinstance(col, db.Column)]

Ensuite, pour obtenir les noms de colonnes: jobStatus.columns()

Cela reviendrait ['id', 'desc']

Ensuite, vous pouvez faire une boucle et faire des choses avec les colonnes et les valeurs:

for col in jobStatus.colums():
    doStuff(getattr(jobStatus, col))
vktec
la source
vous ne pouvez pas faire une instance (col, Column) sur une chaîne
Muposat
Évalué parce que les .columns de table et les .columns de mappeur vous donnent ces données sans réinventer la roue.
David Watson