método de iterar sobre las columnas definidas del modelo sqlalchemy?


95

He estado tratando de averiguar cómo iterar sobre la lista de columnas definidas en un modelo SQLAlchemy. Lo quiero para escribir algunos métodos de serialización y copia en un par de modelos. No puedo simplemente iterar sobre el obj.__dict__ya que contiene muchos elementos específicos de SA.

¿Alguien conoce una forma de obtener los nombres idy descde los siguientes?

class JobStatus(Base):
    __tablename__ = 'jobstatus'

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

En este pequeño caso, podría crear fácilmente un:

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

pero prefiero algo que genere automáticamente dict(para objetos más grandes).

Respuestas:


84

Puede utilizar la siguiente función:

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

Excluirá los atributos mágicos de SA , pero no excluirá las relaciones. Entonces, básicamente, podría cargar las dependencias, padres, hijos, etc., lo que definitivamente no es deseable.

Pero en realidad es mucho más fácil porque si hereda Base, tiene un __table__atributo, por lo que puede hacer:

for c in JobStatus.__table__.columns:
    print c

for c in JobStatus.__table__.foreign_keys:
    print c

Consulte Cómo descubrir las propiedades de la tabla a partir del objeto mapeado de SQLAlchemy - pregunta similar.

Editar por Mike: Por favor ver funciones como Mapper.c y Mapper.mapped_table . Si usa 0.8 y superior, vea también Mapper.attrs y funciones relacionadas.

Ejemplo de Mapper.attrs :

from sqlalchemy import inspect
mapper = inspect(JobStatus)
for column in mapper.attrs:
    print column.key

21
Tenga en cuenta que __table__.columnsle dará los nombres de los campos SQL, no los nombres de los atributos que ha utilizado en sus definiciones ORM (si los dos difieren).
Josh Kelley

11
¿Puedo recomendar cambiar '_sa_' != k[:4]a not k.startswith('_sa_')?
Mu Mind

12
No es necesario hacer un bucle con inspeccionar:inspect(JobStatus).columns.keys()
kirpit

63

Puede obtener la lista de propiedades definidas del asignador. En su caso, solo le interesan los objetos ColumnProperty.

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

4
Gracias, esto me permitió crear un método todict en Base que luego uso para 'volcar' una instancia en un dict que luego puedo pasar para obtener la respuesta del decorador jsonify de pylon. Intenté poner una nota de más detalles con un ejemplo de código en mi pregunta original, pero está causando un error de stackoverflow en el envío.
Rick

4
Por cierto, class_mapperdebe importarse desqlalchemy.orm
Priestc

3
Si bien esta es una respuesta legítima, se sugiere usar después de la versión 0.8 inspect(), que devuelve exactamente el mismo objeto mapeador que class_mapper(). docs.sqlalchemy.org/en/latest/core/inspection.html
kirpit

1
Esto me ayudó mucho a asignar los nombres de las propiedades del modelo SQLAlchemy a los nombres de las columnas subyacentes.
FearlessFuture

29

Me doy cuenta de que esta es una pregunta antigua, pero acabo de encontrar el mismo requisito y me gustaría ofrecer una solución alternativa a los futuros lectores.

Como señala Josh, los nombres de campo SQL completos serán devueltos por JobStatus.__table__.columns, por lo que , en lugar del id del nombre del campo original , obtendrá jobstatus.id . No es tan útil como podría ser.

La solución para obtener una lista de nombres de campo como se definieron originalmente es buscar el _dataatributo en el objeto de columna, que contiene los datos completos. Si lo miramos JobStatus.__table__.columns._data, se ve así:

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

Desde aquí, simplemente puede llamar, lo JobStatus.__table__.columns._data.keys()que le brinda una lista agradable y limpia:

['id', 'desc']

2
¡Agradable! ¿Hay alguna manera con este método de llegar a las relaciones también?
sudario

3
no hay necesidad de _data attr, solo ..columns.keys () hacen el truco
Humoyun Ahmad

1
Sí, se debe evitar el uso del atributo _data privado siempre que sea posible, @Humoyun es más correcto.
Ng Oon-Ee

AttributeError: __data

13

self.__table__.columns"sólo" le dará las columnas definidas en esa clase en particular, es decir, sin las heredadas. si lo necesita todo, utilice self.__mapper__.columns. en su ejemplo probablemente usaría algo como esto:

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)

10

Suponiendo que está usando el mapeo declarativo de SQLAlchemy, puede usar el __mapper__atributo para llegar al mapeador de clases. Para obtener todos los atributos asignados (incluidas las relaciones):

obj.__mapper__.attrs.keys()

Si desea nombres de columna estrictamente, use obj.__mapper__.column_attrs.keys(). Consulte la documentación para conocer otras vistas.

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


Esta es la respuesta simple.
stgrmks

7

Para obtener un as_dictmétodo en todas mis clases, utilicé una Mixinclase que utiliza las técnicas descritas por 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

Y luego úsalo así en tus clases.

class MyClass(BaseMixin, Base):
    pass

De esa forma, puede invocar lo siguiente en una instancia de MyClass.

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

Espero que esto ayude.


He jugado un poco más con esto, en realidad necesitaba renderizar mis instancias dictcomo la forma de un objeto HAL con sus enlaces a objetos relacionados. Así que agregué esta pequeña magia aquí abajo, que rastreará todas las propiedades de la clase igual que la anterior, con la diferencia de que rastrearé más profundamente en las Relaionshippropiedades y generaré linkspara estas automáticamente.

Tenga en cuenta que esto solo funcionará para las relaciones que tengan una única clave principal

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

Agregue from sqlalchemy.orm import class_mapper, ColumnPropertyen la parte superior del fragmento de código
JVK

¡Gracias por tu comentario!
Agregué las

Es la base declarativa de sqlalchemy leer más sobre eso aquí docs.sqlalchemy.org/en/13/orm/extensions/declarative/…
flazzarini

1
self.__dict__

devuelve un dict donde las claves son nombres de atributos y valora los valores del objeto.

/! \ hay un atributo suplementario: '_sa_instance_state' pero puedes manejarlo :)


Solo cuando se establecen atributos.
stgrmks

-1

Sé que esta es una vieja pregunta, pero ¿qué pasa con:

class JobStatus(Base):

    ...

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

Luego, para obtener los nombres de las columnas: jobStatus.columns()

Que volvería ['id', 'desc']

Luego puede recorrer y hacer cosas con las columnas y valores:

for col in jobStatus.colums():
    doStuff(getattr(jobStatus, col))

no puede hacer isinstance (col, Column) en una cadena
Muposat

Votado en contra porque la tabla .columns y el mapper .columns le brindan estos datos sin reinventar la rueda.
David Watson
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.