# Entities

## What are Entities?

Entities are pure Python dataclasses that represent domain objects in our application. Unlike SQLAlchemy models, entities are **session-independent** and decoupled from ORM concerns. They provide a clean abstraction layer for business logic that doesn't require database session management.

## Key Benefits

### 1. Session Independence

SQLAlchemy models are tied to database sessions. Once a session is closed or expired, accessing attributes on a model can trigger lazy-loading errors or require session reattachment. Entities solve this problem:

```python
# BAD: SQLAlchemy Model (session-bound)
preferences_model = session.query(PrivacyPreferences).first()
session.close()
# Accessing preferences_model.record_data now may fail or trigger lazy loads

# GOOD: Entity (session-independent)
preferences_entity = repository.get(identity_filters={"email": "user@example.com"})
# Can be passed anywhere, serialized, cached - no session required!
```

### 2. Pure Python Dataclasses

Entities are simple dataclasses without SQLAlchemy magic. This makes them:
- **Easier to test**: No database mocking required for business logic
- **Easier to serialize**: Direct JSON conversion without ORM state management
- **Easier to reason about**: No hidden lazy-loading or relationship management
- **Type-safe**: Full IDE autocomplete and type checking support

### 3. Clean Architecture Separation

Entities enforce a clear separation of concerns:
- **Models** (`v3/models/`): Database schema and ORM mapping
- **Entities** (`v3/entities/`): Domain objects for business logic
- **Repositories** (`v3/repositories/`): Data access layer that bridges models and entities

## Architecture Pattern

```
┌─────────────────┐
│   API Routes    │  FastAPI endpoints
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│    Services     │  Business logic (works with entities)
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│   Repositories  │  Data access layer
└────────┬────────┘    - Creates/queries SQLAlchemy models
         │             - Converts models → entities via from_orm()
         │             - Returns entities to caller
         ▼
┌─────────────────┐
│   ORM Models    │  SQLAlchemy models (session-bound)
└─────────────────┘
```

## Example: PrivacyPreferencesEntity

### Entity Definition

Entities are simple dataclasses in `v3/entities/`:

```python
@dataclass
class PrivacyPreferencesEntity:
    """
    Domain entity for privacy preferences.
    
    Pure Python dataclass, decoupled from SQLAlchemy ORM.
    """
    identity: Dict[str, Any]
    scope: Dict[str, Any]
    meta: Dict[str, Any]
    preferences: List[Dict[str, Any]]
    
    @classmethod
    def from_orm(cls, orm_obj: PrivacyPreferences) -> "PrivacyPreferencesEntity":
        """Create entity from SQLAlchemy ORM object."""
        record_data = json.loads(orm_obj.record_data)
        meta = record_data["meta"]
        meta["fides"] = {"created_at": orm_obj.created_at}
        return cls(
            identity=record_data["identity"],
            scope=record_data["scope"],
            meta=meta,
            preferences=record_data["preferences"],
        )
```

### Repository Usage

Repositories handle all database operations and return entities. Each method manages its own database session and transaction:

```python
from fides.api.db.ctl_session import async_session

class PrivacyPreferencesRepository:
    """Repository that returns entities, not ORM models."""
    
    async def create(
        self,
        consent_data: ConsentCreate,
    ) -> PrivacyPreferencesEntity:
        """Create returns an entity, not the ORM model."""
        async with async_session() as session:
            # 1. Create SQLAlchemy model
            preferences = PrivacyPreferences(
                search_data={...},
                record_data=consent_data.model_dump_json(),
            )
            
            # 2. Add to session and flush to get database-generated values
            session.add(preferences)
            await session.flush()
            await session.refresh(preferences)
            
            # 3. Convert to entity (do this before commit so ORM object is still attached)
            entity = PrivacyPreferencesEntity.from_orm(preferences)
            
            # 4. Commit transaction
            await session.commit()
            
            return entity
    
    async def get(
        self,
        *,
        identity_filters: Dict[str, Any],
    ) -> List[PrivacyPreferencesEntity]:
        """Get returns entities, not ORM models."""
        async with async_session() as session:
            # 1. Build and execute query
            query = select(PrivacyPreferences)
            # ... add filters ...
            result = await session.execute(query)
            orm_objects = result.scalars().all()
            
            # 2. Convert to entities and return
            return [PrivacyPreferencesEntity.from_orm(obj) for obj in orm_objects]
```

## Best Practices

### DO: Use repositories for data access

Always interact with the database through repositories. Repositories manage their own sessions:

```python
# In your service layer - no database session needed!
async def get_user_preferences(email: str):
    repository = PrivacyPreferencesRepository()
    entities = await repository.get(identity_filters={"email": email})
    # Work with entities in your business logic
    return entities
```

### DO: Work with entities in business logic

Services and business logic should operate on entities, not ORM models:

```python
def process_preferences(entity: PrivacyPreferencesEntity) -> dict:
    """Business logic works with entities - no session needed!"""
    return {
        "user": entity.identity.get("email"),
        "consent_count": len(entity.preferences),
        "created": entity.meta["fides"]["created_at"],
    }
```

### DON'T: Pass SQLAlchemy models outside repository layer

Never return SQLAlchemy models from repositories:

```python
# BAD: Returning session-bound ORM model
async def get_preferences_bad():
    async with async_session() as session:
        result = await session.execute(select(PrivacyPreferences))
        return result.scalars().first()  # Session-bound model!

# GOOD: Returning session-independent entity
async def get_preferences_good():
    repository = PrivacyPreferencesRepository()
    entities = await repository.get(identity_filters={...})
    return entities  # Session-independent entities!
```

### DON'T: Skip the repository pattern

Always use repositories, even for simple queries:

```python
# BAD: Direct database access in service
async def my_service():
    async with async_session() as session:
        result = await session.execute(select(PrivacyPreferences))
        return result.scalars().all()

# GOOD: Use repository
async def my_service():
    repository = PrivacyPreferencesRepository()
    return await repository.get(identity_filters={...})
```

### DON'T: Pass database sessions around

Repositories manage their own sessions internally:

```python
# BAD: Passing sessions through layers
async def api_endpoint(db: AsyncSession = Depends(get_db)):
    service = MyService(db)
    return await service.do_something()

# GOOD: Services and repositories manage sessions internally
async def api_endpoint():
    service = MyService()
    return await service.do_something()
```

#### Exception: handling multiple operations within a single transaction
When you need multiple repository operations in a single transaction, repository methods accept an optional `session` parameter:

```python
async with async_session() as session:
    created_entity_a = await repository.create(entity_a, session=session)
    created_entity_b = await repository.create(entity_b, session=session)
    await session.commit()  # Single transaction for both
```

When a session is provided, the repository flushes instead of committing: the caller controls the commit.
This allows the service layer to orchestrate different operations within a single transaction; the service layer
only manages the session lifecycle but does not handle any SQLAlchemy or session-bound objects.

## Migration Path

Going forward, all new v3 code should follow this pattern:

1. **Define your entity** in `v3/entities/` as a dataclass with a `from_orm()` method
2. **Create a repository** in `v3/repositories/` that:
   - Imports `async_session` from `fides.api.db.ctl_session`
   - Uses `async with async_session() as session:` in each method
   - Commits transactions with `await session.commit()` for write operations
   - Returns entities, never ORM models
3. **Use the repository** in your services without passing database sessions
4. **Never expose SQLAlchemy models** outside the repository layer
5. **Never pass database sessions** between layers - repositories manage them internally

This architectural pattern provides:
- **Better maintainability**: Clear transaction boundaries in each repository method
- **Better separation of concerns**: Database session management is encapsulated in repositories
- **Session independence**: Entities can be freely passed around, serialized, and cached
- **Simplicity**: No dependency injection needed for database sessions

