Saltar al contenido principal

orm

Ver en Git


Mapeo de la base de datos

La funcion start_mappers es la encagada de inicializar el mappeo de la db, se llama a esta funcion en archivo run.py encargado de inicializar la api.

[!NOTE] from features.activities.orm.mappers import start_mappers

Crear mappeo

from sqlalchemy import UUID, Column, Integer, String, Table

# Modelo de python
@dataclass
class Farm:
id: int
name: str
farm_uuid: str

# Tabla sql
farm_table = Table(
"estancias",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("nombre", String, key='name'),
Column("uuid_estancia", UUID(as_uuid=False), key='farm_uuid'),
)

# Defina que la relacion de las entidad con su tabla
mapper_registry.map_imperatively(Farm, farm_table)

Properties

Relaciones entre tables

Al momento de realizar el mappeo podemos agregar propiedades, una de estas pueden ser las relaciones entre tablas

from sqlalchemy.orm import relationship
mapper_registry.map_imperatively(
Labour,
labour_table,
properties={
#Agregamos una lista de LabourInput
'inputs': relationship(
LabourInput,
cascade="save-update, merge",
lazy='joined',
primaryjoin=(labour_table.c.id_labour == input_labour_table.c.id_labour)
& (input_labour_table.c.deleted_at.is_(None)),
viewonly=False,
)
}

cascade

  • 'save-update': Propaga las operaciones de persistencia de "save" y "update" desde el objeto principal a los objetos secundarios.
  • 'merge': Propaga la operación de persistencia de "merge" desde el objeto principal a los objetos secundarios.
  • 'refresh': Propaga la operación de persistencia de "refresh" desde el objeto principal a los objetos secundarios.
  • 'expunge': Propaga la operación de persistencia de "expunge" desde el objeto principal a los objetos secundarios.
  • 'delete': Propaga la operación de persistencia de "delete" desde el objeto principal a los objetos secundarios.
  • all': Realiza todas las acciones de cascada posibles.
  • None: no se propaga ninguna accion, este es el valor por defecto

lazy

  • 'select' (predeterminado): Los objetos relacionados se recuperan de la base de datos cuando se accede por primera vez a la propiedad de la relación. Se realiza una consulta SELECT para cargar los objetos relacionados en ese momento.
  • 'joined': Realiza una unión (JOIN) en la consulta SQL original para cargar los objetos relacionados junto con los objetos principales. Esto suele ser más eficiente que la carga perezosa select.
  • 'subquery': Similar a 'joined', pero utiliza una subconsulta para cargar los objetos relacionados. Puede ser más eficiente que 'joined' en ciertos casos.

Esto se puede modificar tambien al momento se realizar las consultas

# set children to load lazily
session.query(Parent).options(lazyload(Parent.children)).all()

# set children to load eagerly with a join
session.query(Parent).options(joinedload(Parent.children)).all()

# Tambien se pueden encadenar
session.query(Parent).options(
joinedload(Parent.children).subqueryload(Child.subelements)
).all()

primaryjoin

Por defecto utiliza la ForeignKey para realizar el join, en el ejemplo anterior cambiamos el comportamiento, para que ademas verifique que no este eliminado el objeto

primaryjoin=(labour_table.c.id_labour == input_labour_table.c.id_labour)
& (input_labour_table.c.deleted_at.is_(None))

viewonly

Como el nombre lo indica sirve para que el objeto sea solo lectura, por lo tanto sqlalquemy no intentara actualizar el objeto de la relacion. Solo lo agrego en caso que sea True

Relacion de muchos a muchos

staff_rol_association_table = Table(
'resources_staff_roles',
metadata,
Column('id', Integer, primary_key=True),
Column('person_rol_id', Integer, ForeignKey('resources_staff_rol.person_rol_id')),
Column('person_id', Integer, ForeignKey('resources_staff.person_id')),
Column('deleted_at', TIMESTAMP),
)

mapper_registry.map_imperatively(
Staff,
staff_table,
properties={
'roles': relationship(
Role,
secondary=staff_rol_association_table,
secondaryjoin=(
(rol_table.c.person_rol_id == staff_rol_association_table.c.person_rol_id)
& (staff_rol_association_table.c.deleted_at.is_(None))
),
viewonly=True,
)
},
)

Para relacionar Staff con Role utilizamos la tabla intermedia secondary=staff_rol_association_table

En este caso tenemos 2 join:

  • primaryjoin: dejo el default por lo tanto tomara ForeignKey('resources_staff.person_id')
  • secondaryjoin: en este caso tomara por defecto ForeignKey('resources_staff.person_rol_id'), aunque estoy modificando su comportamiento para que tambien verifique que la relacion no haya sido eliminada

Agregar columnas

Se puede agregar una columna, ejemplo

mapper_registry.map_imperatively(
WorkOrderInputRequired,
work_order_input_retired_table,
properties={
"input_name": column_property(
select(Input.name).where(Input.uuid == work_order_input_retired_table.c.input_uuid).scalar_subquery()
)
},
)

Se puede agregar deferred, para que se cargue unicamente cuando se acceda a la variable (esto implica realizar una consulta en ese momento)

mapper_registry.map_imperatively(
WorkOrderInputRequired,
work_order_input_retired_table,
properties={
"input_name": column_property(
deferred(
select(Input.name).where(Input.uuid == work_order_input_retired_table.c.input_uuid).scalar_subquery()
)
)
},
)

Composite

Podemos separar las columnas de una tabla en objeros distintos ej:

seeding_table = Table(
"siembras",
metadata,
...
# seed_rate
Column("densidad", Float),
Column("unidad_medida_densidad", String),
)

mapper_registry.map_imperatively(
Seeding,
seeding_table,
properties={
"seed_rate": composite(
Measurement,
seeding_table.c.densidad,
seeding_table.c.unidad_medida_densidad,
)
}
)

te esta manera las columnas densidad y unidad_medida_densidad no estaran directamente en el obj Seeding sino en Seeding.seed_rate siendo un obj de Measurement

Polimorfismo

En ejemplo de esto es el caso de las labores que tenemos una tabla padre "labores" y luego una especifica para cada tipo de labor, esto lo podemos definir en el orm para que solo nos traiga el tipo correspondiente

# Clase padre especificamos polymorphic_on(columna que nos indica que tipo es), polymorphic_identity id del tipo (en este caso 0 no hay ninguna labor del tipo general)
mapper_registry.map_imperatively(
Labour,
labour_table,
polymorphic_identity=0,
polymorphic_on=labour_table.c.labour_type_id,
)
# En las clase hijas se especifica cual es el objeto padre (Labour) y el id del tipo polymorphic_identity (en este caso las labores del tipo 3 las construira como Seeding)
mapper_registry.map_imperatively(
Seeding,
seeding_table,
inherits=Labour,
polymorphic_identity=3,
)

Querys

ejemplo de como realizar una query

responsible = (
# Objeto que quiero traer
self.session.query(WorkOrderResponsible)
# Quiero que la relacion staff se cargue con un join (pisa el valor cargado en el mappeo para esta consulta)
.options(joinedload(WorkOrderResponsible.staff))
# Todos los filtros que quiera separados por ',' es igual a 'and'
.filter(WorkOrderResponsible.responsible_uuid == uuid, WorkOrderResponsible.deleted_at.is_(None))
#obtener el primero que coincide, puede ser all() para traerse una lista de objetos
.first()
)