diff --git a/api/thing.py b/api/thing.py index b65672880..367237f58 100644 --- a/api/thing.py +++ b/api/thing.py @@ -17,6 +17,7 @@ from fastapi_pagination.ext.sqlalchemy import paginate from sqlalchemy import select from sqlalchemy.exc import ProgrammingError +from sqlalchemy.orm import selectinload from starlette.status import ( HTTP_200_OK, HTTP_201_CREATED, @@ -361,6 +362,7 @@ async def get_thing_deployments( """ thing = simple_get_by_id(session, Thing, thing_id) sql = select(Deployment).where(Deployment.thing_id == thing.id) + sql = sql.options(selectinload(Deployment.sensor)) return paginate(query=sql, conn=session) diff --git a/db/deployment.py b/db/deployment.py index 8f4aaf84e..0b2dc61df 100644 --- a/db/deployment.py +++ b/db/deployment.py @@ -50,6 +50,4 @@ class Deployment(Base, AutoBaseMixin, ReleaseMixin): # Many-To-One: A Deployment is for one Thing. thing: Mapped["Thing"] = relationship("Thing", back_populates="deployments") # Many-To-One: A Deployment is of one piece of equipment (sensor). - sensor: Mapped["Sensor"] = relationship( - "Sensor", back_populates="deployments", lazy="joined" - ) + sensor: Mapped["Sensor"] = relationship("Sensor", back_populates="deployments") diff --git a/db/location.py b/db/location.py index fda4611f9..6376988f6 100644 --- a/db/location.py +++ b/db/location.py @@ -118,9 +118,7 @@ class LocationThingAssociation(Base, AutoBaseMixin): ) # --- Relationship Definitions --- - location: Mapped["Location"] = relationship( - back_populates="thing_associations", lazy="joined" - ) + location: Mapped["Location"] = relationship(back_populates="thing_associations") thing: Mapped["Thing"] = relationship(back_populates="location_associations") diff --git a/db/observation.py b/db/observation.py index 90971bc52..27fe70458 100644 --- a/db/observation.py +++ b/db/observation.py @@ -94,7 +94,7 @@ class Observation(Base, AutoBaseMixin, ReleaseMixin): # Many-To-One: An Observation measures one Parameter. parameter: Mapped["Parameter"] = relationship( - "Parameter", back_populates="observations", lazy="joined" + "Parameter", back_populates="observations", lazy="selectin" ) diff --git a/db/thing.py b/db/thing.py index 286372242..9d73a8f98 100644 --- a/db/thing.py +++ b/db/thing.py @@ -207,7 +207,6 @@ class Thing( cascade="all, delete-orphan", passive_deletes=True, order_by="LocationThingAssociation.effective_start.desc()", - lazy="joined", ) contact_associations = relationship( @@ -254,7 +253,6 @@ class Thing( back_populates="thing", cascade="all, delete-orphan", passive_deletes=True, - lazy="joined", ) well_casing_materials: Mapped[List["WellCasingMaterial"]] = relationship( @@ -262,7 +260,6 @@ class Thing( back_populates="thing", cascade="all, delete-orphan", passive_deletes=True, - lazy="joined", ) links: Mapped[List["ThingIdLink"]] = relationship( @@ -270,7 +267,6 @@ class Thing( back_populates="thing", cascade="all, delete-orphan", passive_deletes=True, - lazy="joined", ) # One-To-Many: A Thing (well) can have multiple measuring points over time. @@ -279,7 +275,6 @@ class Thing( back_populates="thing", cascade="all, delete-orphan", passive_deletes=True, - lazy="joined", ) monitoring_frequencies: Mapped[List["MonitoringFrequencyHistory"]] = relationship( @@ -287,7 +282,6 @@ class Thing( back_populates="thing", cascade="all, delete-orphan", passive_deletes=True, - lazy="joined", ) # One-To-Many: A Thing can be associated with many AquiferSystems via the ThingAquiferAssociation join table. @@ -296,7 +290,6 @@ class Thing( back_populates="thing", cascade="all, delete-orphan", passive_deletes=True, - lazy="joined", ) # Many-To-Many: A Thing can penetrate many GeologicFormations. @@ -306,7 +299,6 @@ class Thing( back_populates="thing", cascade="all, delete-orphan", passive_deletes=True, - lazy="joined", ) ) @@ -348,9 +340,6 @@ class Thing( # Full-text search vector search_vector = Column(TSVectorType("name")) - # for temporary backwards compatibility - well_construction_notes = mapped_column(String(1000), nullable=True) - @property def current_location(self): """ diff --git a/db/thing_aquifer_association.py b/db/thing_aquifer_association.py index cca5758a9..d9a3e6ac4 100644 --- a/db/thing_aquifer_association.py +++ b/db/thing_aquifer_association.py @@ -34,12 +34,12 @@ class ThingAquiferAssociation(Base, AutoBaseMixin, ReleaseMixin): # --- Relationship Definitions --- # Many-To-One: This association links to one Thing. thing: Mapped["Thing"] = relationship( - "Thing", back_populates="aquifer_associations", lazy="joined" + "Thing", back_populates="aquifer_associations" ) # Many-To-One: This association links to one AquiferSystem. aquifer_system: Mapped["AquiferSystem"] = relationship( - "AquiferSystem", back_populates="thing_associations", lazy="joined" + "AquiferSystem", back_populates="thing_associations" ) # One-To-Many: An association can have multiple aquifer types. aquifer_types: Mapped[list["AquiferType"]] = relationship( @@ -47,5 +47,4 @@ class ThingAquiferAssociation(Base, AutoBaseMixin, ReleaseMixin): back_populates="thing_aquifer_association", cascade="all, delete-orphan", passive_deletes=True, - lazy="joined", ) diff --git a/db/thing_geologic_formation_association.py b/db/thing_geologic_formation_association.py index 0707df269..71d393404 100644 --- a/db/thing_geologic_formation_association.py +++ b/db/thing_geologic_formation_association.py @@ -51,10 +51,10 @@ class ThingGeologicFormationAssociation(Base, AutoBaseMixin, ReleaseMixin): # --- Relationship Definitions --- # Many-To-One: This association links to one Thing. thing: Mapped["Thing"] = relationship( - "Thing", back_populates="formation_associations", lazy="joined" + "Thing", back_populates="formation_associations" ) # Many-To-One: This association links to one GeologicFormation. geologic_formation: Mapped["GeologicFormation"] = relationship( - "GeologicFormation", back_populates="thing_associations", lazy="joined" + "GeologicFormation", back_populates="thing_associations" ) diff --git a/schemas/contact.py b/schemas/contact.py index d43cd4aaf..eeecd6bfd 100644 --- a/schemas/contact.py +++ b/schemas/contact.py @@ -22,7 +22,6 @@ from core.enums import Role, ContactType, PhoneType, EmailType, AddressType from schemas import BaseResponseModel, BaseCreateModel, BaseUpdateModel -from schemas.thing import ThingResponse # -------- VALIDATORS ---------- @@ -199,6 +198,15 @@ class AddressResponse(BaseItemResponse): address_type: AddressType +class ThingResponseForContact(BaseResponseModel): + """ + Response schema for thing details related to a contact. All that is needed + are the id and name + """ + + name: str + + class ContactResponse(BaseResponseModel): """ Response schema for contact details. @@ -212,7 +220,7 @@ class ContactResponse(BaseResponseModel): emails: List[EmailResponse] = [] phones: List[PhoneResponse] = [] addresses: List[AddressResponse] = [] - things: List[ThingResponse] = [] # List of related things + things: List[ThingResponseForContact] = [] @field_validator("incomplete_nma_phones", mode="before") def make_incomplete_nma_phone_str(cls, v: list) -> list: diff --git a/services/thing_helper.py b/services/thing_helper.py index fdd0424db..731db8429 100644 --- a/services/thing_helper.py +++ b/services/thing_helper.py @@ -16,13 +16,13 @@ from datetime import datetime from zoneinfo import ZoneInfo -from fastapi import Request +from fastapi import Request, HTTPException from fastapi_pagination.ext.sqlalchemy import paginate from pydantic import BaseModel from shapely import wkb from shapely.geometry import mapping from sqlalchemy import select, func -from sqlalchemy.orm import Session, aliased +from sqlalchemy.orm import Session, aliased, selectinload from starlette.status import HTTP_404_NOT_FOUND, HTTP_409_CONFLICT from db import ( @@ -33,9 +33,11 @@ WellScreen, WellPurpose, WellCasingMaterial, + ThingAquiferAssociation, + GroupThingAssociation, + MeasuringPointHistory, ) -from db.group import GroupThingAssociation -from db.measuring_point_history import MeasuringPointHistory + from services.audit_helper import audit_add from services.crud_helper import model_patcher from services.exceptions_helper import PydanticStyleException @@ -47,6 +49,22 @@ "well_casing_materials": (WellCasingMaterial, "material"), } +WELL_LOADER_OPTIONS = [ + selectinload(Thing.location_associations).selectinload( + LocationThingAssociation.location + ), + selectinload(Thing.well_purposes), + selectinload(Thing.well_casing_materials), + selectinload(Thing.links), + selectinload(Thing.measuring_points), + selectinload(Thing.monitoring_frequencies), + selectinload(Thing.aquifer_associations).selectinload( + ThingAquiferAssociation.aquifer_system + ), +] + +WELL_THING_TYPE = "water well" + def wkb_to_geojson(wkb_element): if wkb_element is None: @@ -74,6 +92,12 @@ def get_db_things( if thing_type: sql = sql.where(Thing.thing_type == thing_type) + if thing_type == WELL_THING_TYPE: + sql = sql.options(*WELL_LOADER_OPTIONS) + else: + # add all eager loads for generic thing query until/unless GET /thing is deprecated + sql = sql.options(*WELL_LOADER_OPTIONS) + if name: sql = sql.where(Thing.name == name) @@ -118,8 +142,7 @@ def get_thing_type_from_request(request: Request) -> str: return thing_type -def verify_thing_type_correspondence(thing: Thing, request: Request): - thing_type = get_thing_type_from_request(request) +def verify_thing_type_correspondence(thing: Thing, thing_type: str): if thing.thing_type != thing_type: raise PydanticStyleException( status_code=HTTP_404_NOT_FOUND, @@ -135,9 +158,21 @@ def verify_thing_type_correspondence(thing: Thing, request: Request): def get_thing_of_a_thing_type_by_id(session: Session, request: Request, thing_id: int): - thing = simple_get_by_id(session, Thing, thing_id) + thing_type = get_thing_type_from_request(request) + sql = select(Thing).where(Thing.id == thing_id) + + if thing_type == WELL_THING_TYPE: + sql = sql.options(*WELL_LOADER_OPTIONS) - verify_thing_type_correspondence(thing, request) + thing = session.execute(sql).scalar_one_or_none() + + if not thing: + raise HTTPException( + status_code=HTTP_404_NOT_FOUND, + detail=f"Thing with ID {thing_id} not found.", + ) + + verify_thing_type_correspondence(thing, thing_type) return thing @@ -261,7 +296,8 @@ def patch_thing( ): thing = simple_get_by_id(session, Thing, thing_id) - verify_thing_type_correspondence(thing, request) + thing_type = get_thing_type_from_request(request) + verify_thing_type_correspondence(thing, thing_type) thing = model_patcher(session, Thing, thing_id, payload, user) return thing