¿Cómo puedo usar UUID en SQLAlchemy?

94

¿Hay alguna forma de definir una columna (clave principal) como UUID en SQLAlchemy si usa PostgreSQL (Postgres)?

Vasil
fuente
2
Desafortunadamente , el tipo de GUID independiente del backend de la documentación de SQLAlchemy para tipos de columna no parece funcionar para claves primarias en motores de base de datos SQLite. No es tan ecuménico como esperaba.
adamek
SQLAlchemy utils proporciona decorador UUIDType , no hay necesidad de reinventar la rueda
Filipe Bezerra de Sousa

Respuestas:

152

El dialecto sqlalchemy postgres admite columnas UUID. Esto es fácil (y la pregunta es específicamente postgres): no entiendo por qué las otras respuestas son tan complicadas.

Aquí hay un ejemplo:

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

db = SQLAlchemy()

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

Tenga cuidado de no pasar por alto la callable uuid.uuid4definición de la columna, en lugar de llamar a la función con uuid.uuid4(). De lo contrario, tendrá el mismo valor escalar para todas las instancias de esta clase. Más detalles aquí :

Una expresión escalar, invocable de Python o ColumnElement que representa el valor predeterminado para esta columna, que se invocará al insertarla si esta columna no se especifica en la cláusula VALUES de la inserción.

JDiMatteo
fuente
6
Estoy totalmente de acuerdo contigo. Algunas de las otras respuestas son interesantes para otras bases de datos, pero para postgres esta es la solución más limpia. (También puede establecer un valor predeterminado como uuid.uuid4).
pacha
1
¿Puede proporcionar un MWE? ¿O tal vez el serializador en flask_sqlalchemy entiende el tipo de UUID? El código en el pastebin debajo de errores, pastebin.com/hW8KPuYw
Brandon Dube
1
no importa, si desea utilizar objetos UUID de stdlib, hágaloColumn(UUID(as_uuid=True) ...)
Brandon Dube
1
¡Gracias! Sería bueno si Columny Integerse importaran en la parte superior del fragmento de código, o se cambiaran para leer db.Columnydb.Integer
Greg Sadetsky
1
No, no es necesario @nephanth
Filipe Bezerra de Sousa
64

Escribí esto y el dominio se ha ido, pero aquí están las agallas ...

Independientemente de cómo se sientan mis colegas que realmente se preocupan por el diseño adecuado de la base de datos sobre los UUID y los GUID utilizados para los campos clave. A menudo encuentro que necesito hacerlo. Creo que tiene algunas ventajas sobre el autoincremento que hacen que valga la pena.

He estado refinando un tipo de columna UUID durante los últimos meses y creo que finalmente lo tengo sólido.

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

Creo que almacenar como binario (16 bytes) debería terminar siendo más eficiente que la representación de cadena (36 bytes?), Y parece haber alguna indicación de que indexar bloques de 16 bytes debería ser más eficiente en mysql que las cadenas. De todos modos, no esperaría que fuera peor.

Una desventaja que he encontrado es que al menos en phpymyadmin, no puede editar registros porque implícitamente intenta hacer algún tipo de conversión de caracteres para "seleccionar * de la tabla donde id = ..." y hay varios problemas de visualización.

Aparte de eso, todo parece funcionar bien, así que lo estoy tirando por ahí. Deje un comentario si ve un error evidente con él. Agradezco cualquier sugerencia para mejorarlo.

A menos que me falte algo, la solución anterior funcionará si la base de datos subyacente tiene un tipo de UUID. Si no es así, es probable que obtenga errores cuando se cree la tabla. La solución que se me ocurrió originalmente tenía como objetivo MSSqlServer y luego fui MySql al final, así que creo que mi solución es un poco más flexible, ya que parece funcionar bien en mysql y sqlite. Todavía no me he molestado en comprobar Postgres.

Tom Willis
fuente
sí, lo publiqué después de ver referencias de la respuesta de Jacob.
Tom Willis
4
Tenga en cuenta que si está utilizando la versión 0.6 o superior, la declaración de importación de MSBinary en la solución de Tom debe cambiarse a "from sqlalchemy.dialects.mysql.base import MSBinary". Fuente: mail-archive.com/[email protected]/msg18397.html
Cal Jacobson
2
"Escribí esto" es un enlace muerto.
julio
2
Consulte también el UUIDType que se envía con SQLAlchemy-utils
driftcatcher
2
@codeninja postgresql ya tiene un tipo UUID nativo, así que utilícelo sqlalchemy.dialects.postgresql.UUIDdirectamente. consulte Tipo de GUID
independiente de backend
24

Si está satisfecho con una columna 'Cadena' que tiene un valor UUID, aquí tiene una solución simple:

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
fuente
5
No almacene UUID como una cadena a menos que esté usando una base de datos realmente extraña que no los admita. de lo contrario, tal vez almacene todos sus datos como cadenas ...;)
Nick
@Nick ¿por qué? cual es la desventaja?
rayepps
6
@rayepps - hay muchas desventajas - algunas de las más importantes: tamaño - string uuid ocupa el doble de espacio - 16 bytes frente a 32 caracteres - sin incluir ningún formateador. Tiempo de procesamiento: más bytes = más tiempo de procesamiento por parte de la CPU a medida que su conjunto de datos aumenta. Los formatos de cadena uuid difieren según el idioma, por lo que se requieren traducciones adicionales. Es más fácil para alguien hacer un mal uso de la columna, ya que puede poner cualquier cosa allí, cosas que no son fluidos. Eso debería ser suficiente para empezar.
Nick
No debe usar cadenas como columnas para un uuid, por problemas de rendimiento. Se recomienda más un binario (16).
Cyril N.
19

He usado el UUIDTypedel SQLAlchemy-Utilspaquete: http://sqlalchemy-utils.readthedocs.org/en/latest/data_types.html#module-sqlalchemy_utils.types.uuid

Berislav Lopac
fuente
Actualmente estoy tratando de usar esto, el problema es que recibo un error: raise InvalidStatus("notfound: {k}. (cls={cls})".format(k=k, cls=cls)) alchemyjsonschema.InvalidStatus: notfound: BINARY(16). (cls=<class 'sqlalchemy_utils.types.uuid.UUIDType'>)
CodeTrooper
¿Han recibido el error NameError: name 'sqlalchemy_utils' is not defined:?
Walter
1
SQLAlchemy-Utilses un paquete de terceros, primero debe instalarlo:pip install sqlalchemy-utils
Berislav Lopac
Este es el camino a seguir, aunque sus migraciones deben tener una cuenta o sistemas que tengan valores UUID vs CHAR / BINARY para uuids.
rjurney
9

Como está usando Postgres, esto debería 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
fuente
1
Esta debería ser la única respuesta aceptada para aquellos desarrolladores que utilizan una base de datos PostgreSQL.
José L. Patiño
5

Aquí hay un enfoque basado en el GUID independiente del backend de los documentos de SQLAlchemy, pero usando un campo BINARIO para almacenar los UUID en bases de datos que no son 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
fuente
1
¿Cuál sería el uso de esto?
CodeTrooper
3

En caso de que alguien esté interesado, he estado usando la respuesta de Tom Willis, pero me resultó útil agregar una cadena a la conversión uuid.UUID en el 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
fuente
-19

Podría intentar escribir un tipo personalizado , por ejemplo:

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
fuente
Además de la respuesta de Florian , también está esta entrada de blog . Se ve similar excepto que subclasifica en types.TypeDecoratorlugar de types.TypeEngine. ¿Alguno de los enfoques tiene una ventaja o una desventaja sobre el otro?
Jacob Gabrielson
11
Esto ni siquiera funciona, es solo un trabajo de cortar y pegar del ejemplo de tipo ficticio de los documentos. La respuesta de Tom Willis a continuación es mucho mejor.
Jesse Dhillon
¿No necesita un default=?? por ejemploColumn('id', UUID(), primary_key=True, default=<someautouuidgeneratingthing>)
iJames
El enlace apunta a "Página no encontrada", docs.sqlalchemy.org/en/13/core/… probablemente se parezca al anterior
barbsan