Como posso usar UUIDs no SQLAlchemy?

95

Existe uma maneira de definir uma coluna (chave primária) como um UUID em SQLAlchemy se estiver usando PostgreSQL (Postgres)?

Vasil
fonte
2
Infelizmente, o tipo de GUID independente de backend da documentação do SQLAlchemy para tipos de coluna não parece funcionar para chaves primárias em mecanismos de banco de dados SQLite. Não tão ecumênico quanto eu esperava.
adamek
SQLAlchemy utils fornece decorador UUIDType , não há necessidade de reinventar a roda
Filipe Bezerra de Sousa

Respostas:

159

O dialeto sqlalchemy postgres oferece suporte a colunas UUID. Isso é fácil (e a questão é especificamente postgres) - eu não entendo por que as outras respostas são tão complicadas.

Aqui está um exemplo:

from sqlalchemy.dialects.postgresql import UUID
from flask_sqlalchemy import SQLAlchemy
import uuid

db = SQLAlchemy()

class Foo(db.Model):
    id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, unique=True)

Tenha cuidado para não deixar de passar o callable uuid.uuid4para a definição de coluna, em vez de chamar a própria função com uuid.uuid4(). Caso contrário, você terá o mesmo valor escalar para todas as instâncias desta classe. Mais detalhes aqui :

Uma expressão escalar, Python chamável ou ColumnElement que representa o valor padrão para esta coluna, que será chamada na inserção se esta coluna não for especificada de outra forma na cláusula VALUES da inserção.

JDiMatteo
fonte
6
Eu concordo totalmente com você. Algumas das outras respostas são legais para outros bancos de dados, mas para o postgres esta é a solução mais limpa. (Você também pode definir um padrão como uuid.uuid4).
pacha de
1
Você pode fornecer um MWE? Ou talvez o serializador em flask_sqlalchemy entenda o tipo de UUID? O código no pastebin abaixo dos erros, pastebin.com/hW8KPuYw
Brandon Dube
1
deixa pra lá, se você quiser usar objetos UUID do stdlib, façaColumn(UUID(as_uuid=True) ...)
Brandon Dube
1
Obrigado! Pode ser bom se Columne Integerfosse importado no topo do snippet de código ou alterado para ler db.Columnedb.Integer
Greg Sadetsky
2
Não precisa @nephanth
Filipe Bezerra de Sousa
65

Eu escrevi isso e o domínio se foi, mas aqui está a coragem ....

Independentemente de como meus colegas que realmente se preocupam com o design de banco de dados adequado se sentem sobre UUIDs e GUIDs usados ​​para campos-chave. Freqüentemente acho que preciso fazer isso. Acho que tem algumas vantagens sobre o incremento automático que o fazem valer a pena.

Venho refinando um tipo de coluna UUID nos últimos meses e acho que finalmente entendi.

from sqlalchemy import types
from sqlalchemy.dialects.mysql.base import MSBinary
from sqlalchemy.schema import Column
import uuid


class UUID(types.TypeDecorator):
    impl = MSBinary
    def __init__(self):
        self.impl.length = 16
        types.TypeDecorator.__init__(self,length=self.impl.length)

    def process_bind_param(self,value,dialect=None):
        if value and isinstance(value,uuid.UUID):
            return value.bytes
        elif value and not isinstance(value,uuid.UUID):
            raise ValueError,'value %s is not a valid uuid.UUID' % value
        else:
            return None

    def process_result_value(self,value,dialect=None):
        if value:
            return uuid.UUID(bytes=value)
        else:
            return None

    def is_mutable(self):
        return False


id_column_name = "id"

def id_column():
    import uuid
    return Column(id_column_name,UUID(),primary_key=True,default=uuid.uuid4)

# Usage
my_table = Table('test',
         metadata,
         id_column(),
         Column('parent_id',
            UUID(),
            ForeignKey(table_parent.c.id)))

Acredito que armazenar como binário (16 bytes) deve acabar sendo mais eficiente do que a representação de string (36 bytes?), E parece haver alguma indicação de que a indexação de blocos de 16 bytes deve ser mais eficiente no mysql do que strings. Eu não esperava que fosse pior de qualquer maneira.

Uma desvantagem que descobri é que pelo menos no phpymyadmin, você não pode editar registros porque ele tenta implicitamente fazer algum tipo de conversão de caracteres para "select * from table where id = ..." e há diversos problemas de exibição.

Fora isso, tudo parece funcionar bem, então estou jogando fora por aí. Deixe um comentário se você vir um erro gritante nele. Eu agradeço qualquer sugestão para melhorá-lo.

A menos que esteja faltando alguma coisa, a solução acima funcionará se o banco de dados subjacente tiver um tipo UUID. Do contrário, você provavelmente obterá erros quando a tabela for criada. A solução que encontrei era originalmente destinada ao MSSqlServer e depois fui para o MySql no final, então acho que minha solução é um pouco mais flexível, pois parece funcionar bem no mysql e no sqlite. Ainda não me preocupei em verificar o postgres.

Tom Willis
fonte
sim, eu postei depois de ver referências da resposta de Jacob.
Tom Willis
4
Observe que se você estiver usando a versão 0.6 ou superior, a instrução de importação MSBinary na solução de Tom deve ser alterada para "de sqlalchemy.dialects.mysql.base import MSBinary". Fonte: mail-archive.com/[email protected]/msg18397.html
Cal Jacobson
2
"Eu escrevi isso" é um link morto.
julho de
2
Veja também o UUIDType que vem com SQLAlchemy-utils
driftcatcher
2
@codeninja postgresql já tem um tipo de UUID nativo, então use sqlalchemy.dialects.postgresql.UUIDdiretamente. consulte Tipo de GUID agnóstico de backend
cowbert
24

Se você está satisfeito com uma coluna 'String' com valor UUID, aqui vai uma solução simples:

def generate_uuid():
    return str(uuid.uuid4())

class MyTable(Base):
    __tablename__ = 'my_table'

    uuid = Column(String, name="uuid", primary_key=True, default=generate_uuid)
Kushal Ahmed
fonte
5
Não armazene UUID como string, a menos que você esteja usando um banco de dados realmente estranho que não os suporta. caso contrário, talvez armazene todos os seus dados como strings ...;)
Nick
@Nick porque? qual é a desvantagem?
rayepps
6
@rayepps - há muitas desvantagens - algumas que vêm à mente: tamanho - string uuid ocupa o dobro do espaço - 16 bytes vs 32 caracteres - sem incluir nenhum formatador. Tempo de processamento - mais bytes = mais tempo de processamento pela CPU conforme seu conjunto de dados fica maior. formatos de string uuid diferem por idioma, adicionando traduções necessárias. Mais fácil para alguém usar indevidamente a coluna, pois você pode colocar qualquer coisa lá, coisas que não são uuids. Isso deve ser o suficiente para começar.
Nick
Você não deve usar Strings como colunas para um uuid, para problemas de desempenho. Um binário (16) é mais recomendado.
Cyril N.
19

Usei o UUIDTypedo SQLAlchemy-Utilspacote: http://sqlalchemy-utils.readthedocs.org/en/latest/data_types.html#module-sqlalchemy_utils.types.uuid

Berislav Lopac
fonte
No momento, estou tentando usar isso, o problema é que recebo um erro: raise InvalidStatus("notfound: {k}. (cls={cls})".format(k=k, cls=cls)) alchemyjsonschema.InvalidStatus: notfound: BINARY(16). (cls=<class 'sqlalchemy_utils.types.uuid.UUIDType'>)
CodeTrooper
Vocês receberam o erro NameError: name 'sqlalchemy_utils' is not defined:?
Walter
1
SQLAlchemy-Utilsé um pacote de terceiros, você precisa instalá-lo primeiro:pip install sqlalchemy-utils
Berislav Lopac
Este é o caminho a seguir, embora suas migrações precisem de contas ou sistemas que tenham valores UUID vs CHAR / BINARY para uuids.
rjurney
9

Como você está usando o Postgres, isso deve funcionar:

from app.main import db
from sqlalchemy.dialects.postgresql import UUID

class Foo(db.Model):
    id = db.Column(UUID(as_uuid=True), primary_key=True)
    name = db.Column(db.String, nullable=False)
Granat
fonte
1
Esta deve ser a única resposta aceita para os desenvolvedores que usam um banco de dados PostgreSQL.
José L. Patiño
6

Aqui está uma abordagem baseada no GUID agnóstico de Backend dos documentos SQLAlchemy, mas usando um campo BINARY para armazenar os UUIDs em bancos de dados não postgresql.

import uuid

from sqlalchemy.types import TypeDecorator, BINARY
from sqlalchemy.dialects.postgresql import UUID as psqlUUID

class UUID(TypeDecorator):
    """Platform-independent GUID type.

    Uses Postgresql's UUID type, otherwise uses
    BINARY(16), to store UUID.

    """
    impl = BINARY

    def load_dialect_impl(self, dialect):
        if dialect.name == 'postgresql':
            return dialect.type_descriptor(psqlUUID())
        else:
            return dialect.type_descriptor(BINARY(16))

    def process_bind_param(self, value, dialect):
        if value is None:
            return value
        else:
            if not isinstance(value, uuid.UUID):
                if isinstance(value, bytes):
                    value = uuid.UUID(bytes=value)
                elif isinstance(value, int):
                    value = uuid.UUID(int=value)
                elif isinstance(value, str):
                    value = uuid.UUID(value)
        if dialect.name == 'postgresql':
            return str(value)
        else:
            return value.bytes

    def process_result_value(self, value, dialect):
        if value is None:
            return value
        if dialect.name == 'postgresql':
            return uuid.UUID(value)
        else:
            return uuid.UUID(bytes=value)
Zwirbeltier
fonte
1
Qual seria o uso disso?
CodeTrooper
3

Caso alguém esteja interessado, estou usando a resposta de Tom Willis, mas achei útil adicionar uma string à conversão uuid.UUID no método process_bind_param

class UUID(types.TypeDecorator):
    impl = types.LargeBinary

    def __init__(self):
        self.impl.length = 16
        types.TypeDecorator.__init__(self, length=self.impl.length)

    def process_bind_param(self, value, dialect=None):
        if value and isinstance(value, uuid.UUID):
            return value.bytes
        elif value and isinstance(value, basestring):
            return uuid.UUID(value).bytes
        elif value:
            raise ValueError('value %s is not a valid uuid.UUId' % value)
        else:
            return None

    def process_result_value(self, value, dialect=None):
        if value:
            return uuid.UUID(bytes=value)
        else:
            return None

    def is_mutable(self):
        return False
Nemeth
fonte
-19

Você pode tentar escrever um tipo personalizado , por exemplo:

import sqlalchemy.types as types

class UUID(types.TypeEngine):
    def get_col_spec(self):
        return "uuid"

    def bind_processor(self, dialect):
        def process(value):
            return value
        return process

    def result_processor(self, dialect):
        def process(value):
            return value
        return process

table = Table('foo', meta,
    Column('id', UUID(), primary_key=True),
)
Florian Bösch
fonte
Além da resposta de Florian , há também esta entrada no blog . Parece semelhante, exceto que subclasses em types.TypeDecoratorvez de types.TypeEngine. Qualquer uma das abordagens tem uma vantagem ou desvantagem sobre a outra?
Jacob Gabrielson
11
Isso nem funciona, é apenas um trabalho de recortar e colar do exemplo de tipo fictício dos documentos. A resposta de Tom Willis abaixo é muito melhor.
Jesse Dhillon
Não precisa de um default=?? por exemploColumn('id', UUID(), primary_key=True, default=<someautouuidgeneratingthing>)
iJames
O link aponta para "Página não encontrada", docs.sqlalchemy.org/en/13/core/… é provavelmente próximo do antigo
barbsan