método de iteração nas colunas definidas do modelo sqlalchemy?

95

Tenho tentado descobrir como iterar a lista de colunas definidas em um modelo SQLAlchemy. Eu quero escrever alguns métodos de serialização e cópia para alguns modelos. Não posso simplesmente iterar sobre o, obj.__dict__pois ele contém muitos itens específicos de SA.

Alguém sabe uma maneira de obter os nomes ide a descpartir do seguinte?

class JobStatus(Base):
    __tablename__ = 'jobstatus'

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

Neste pequeno caso, eu poderia facilmente criar um:

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

mas eu prefiro algo que gere automaticamente o dict(para objetos maiores).

Rick
fonte

Respostas:

83

Você pode usar a seguinte função:

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

Isso excluirá atributos mágicos SA , mas não excluirá as relações. Então, basicamente, ele pode carregar as dependências, pais, filhos, etc., o que definitivamente não é desejável.

Mas na verdade é muito mais fácil porque se você herdar de Base, você terá um __table__atributo, para que possa fazer:

for c in JobStatus.__table__.columns:
    print c

for c in JobStatus.__table__.foreign_keys:
    print c

Consulte Como descobrir propriedades da tabela do objeto mapeado SQLAlchemy - pergunta semelhante.

Edição de Mike: consulte funções como Mapper.c e Mapper.mapped_table . Se estiver usando 0.8 e superior, consulte também Mapper.attrs e funções relacionadas.

Exemplo para Mapper.attrs :

from sqlalchemy import inspect
mapper = inspect(JobStatus)
for column in mapper.attrs:
    print column.key
furgão
fonte
21
Observe que __table__.columnsfornecerá os nomes dos campos SQL, não os nomes dos atributos que você usou nas suas definições ORM (se os dois forem diferentes).
Josh Kelley
11
Posso recomendar mudar '_sa_' != k[:4]para not k.startswith('_sa_')?
Mu Mind
11
Não há necessidade de fazer um loop com inspecionar:inspect(JobStatus).columns.keys()
Kirpit de
63

Você pode obter a lista de propriedades definidas do mapeador. Para o seu caso, você está interessado apenas em 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)]
Formigas Aasma
fonte
4
Obrigado, isso me permite criar um método todict no Base que eu uso para 'despejar' uma instância em um dict que posso passar para a resposta do decorador jsonify do pylon. Tentei colocar uma nota mais detalhada com o exemplo de código na minha pergunta original, mas está causando o erro de stackoverflow no envio.
Rick
4
btw, class_mapperprecisa ser importado desqlalchemy.orm
priestc
3
Embora seja uma resposta legítima, após a versão 0.8 sugere-se o uso inspect(), que retorna exatamente o mesmo objeto mapeador que class_mapper(). docs.sqlalchemy.org/en/latest/core/inspection.html
kirpit
1
Isso me ajudou muito a mapear os nomes das propriedades do modelo SQLAlchemy para os nomes das colunas subjacentes.
FearlessFuture
29

Sei que essa é uma questão antiga, mas acabo de me deparar com a mesma exigência e gostaria de oferecer uma solução alternativa para futuros leitores.

Como Josh observa, nomes completos de campos SQL serão retornados por JobStatus.__table__.columns, portanto, em vez do id do nome do campo original , você obterá jobstatus.id . Não é tão útil quanto poderia ser.

A solução para obter uma lista de nomes de campo conforme foram originalmente definidos é examinar o _dataatributo no objeto de coluna, que contém os dados completos. Se olharmos JobStatus.__table__.columns._data, é assim:

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

A partir daqui, você pode simplesmente ligar, o JobStatus.__table__.columns._data.keys()que lhe dá uma lista bonita e limpa:

['id', 'desc']
morfica
fonte
2
Agradável! Existe uma maneira com esse método de chegar a relacionamentos também?
mortalha de
3
não há necessidade de _data attr, apenas ..columns.keys () fazem o truque
Humoyun Ahmad
1
Sim, o uso do atributo _data privado deve ser evitado sempre que possível, @Humoyun é mais correto.
Ng Oon-Ee
AttributeError: __data
13

self.__table__.columnsirá "apenas" dar-lhe as colunas definidas naquela classe particular, ou seja, sem as herdadas. se precisar de tudo, use self.__mapper__.columns. no seu exemplo eu provavelmente usaria algo assim:

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
fonte
10

Supondo que você esteja usando o mapeamento declarativo do SQLAlchemy, você pode usar o __mapper__atributo para chegar ao mapeador de classe. Para obter todos os atributos mapeados (incluindo relacionamentos):

obj.__mapper__.attrs.keys()

Se quiser nomes de coluna estritamente, use obj.__mapper__.column_attrs.keys(). Consulte a documentação para outras visualizações.

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

jrc
fonte
Esta é a resposta simples.
stgrmks
7

Para obter um as_dictmétodo em todas as minhas aulas, usei uma Mixinclasse que usa as 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

E então use-o assim em suas aulas

class MyClass(BaseMixin, Base):
    pass

Dessa forma, você pode invocar o seguinte em uma instância de MyClass.

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

Espero que isto ajude.


Eu brinquei com isso um pouco mais adiante, na verdade eu precisava renderizar minhas instâncias dictcomo a forma de um objeto HAL com seus links para objetos relacionados. Então, adicionei essa pequena mágica aqui, que irá rastrear todas as propriedades da classe igual à anterior, com a diferença de que eu irei rastrear mais profundamente nas Relaionshippropriedades e gerar linkspara elas automaticamente.

Observe que isso só funcionará para relacionamentos com uma única chave primária

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
fonte
Adicione from sqlalchemy.orm import class_mapper, ColumnPropertyacima de seu snippet de código
JVK
Obrigado por seu comentário! Eu adicionei as importações ausentes
flazzarini
É a base declarativa do sqlalchemy leia mais sobre isso aqui docs.sqlalchemy.org/en/13/orm/extensions/declarative/…
flazzarini
1
self.__dict__

retorna um dict em que as chaves são nomes de atributos e valores os valores do objeto.

/! \ há um atributo suplementar: '_sa_instance_state' mas você pode lidar com isso :)

Valentin Fabianski
fonte
Somente quando os atributos são definidos.
stgrmks
-1

Eu sei que esta é uma pergunta antiga, mas e quanto a:

class JobStatus(Base):

    ...

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

Então, para obter os nomes das colunas: jobStatus.columns()

Isso iria voltar ['id', 'desc']

Em seguida, você pode fazer um loop e fazer coisas com as colunas e valores:

for col in jobStatus.colums():
    doStuff(getattr(jobStatus, col))
vktec
fonte
você não pode fazer isinstance (col, Column) em uma string
Muposat
Votos negados porque table .columns e mapper .columns fornecem esses dados sem reinventar a roda.
David Watson