Repository: litestar-org/litestar

Issue:
Bug: Duplicate Response Schema name when using ManyToOne relationships
### Description

I am creating log file models that have a relationship to a user model.

The models are similar to the following.
```py
class User(UUIDBase):
    name: Mapped[str]

class Logfile(UUIDAuditBase):
    name: Mapped[str]
    user_id: Mapped[UUID] = mapped_column(ForeignKey("user.id"))
    user: Mapped[User] = relationship(lazy="joined", innerjoin=True, viewonly=True)

class LogfileDTO(SQLAlchemyDTO[Logfile]):
    config = SQLAlchemyDTOConfig()

@get(path="/logs", dto=LogfileDTO)
async def get_logs(db_session: "AsyncSession") -> list[Logfile]:
    return list(await db_session.scalars(select(Logfile)))
```

The expected outcome would be two response body schemas, with different names, the Logfile and the nested User.

Actual outcome is two schemas with the same name are generated, so one gets the long format name.

From Swagger of the MCVE, showing the two ResponseBody schemas
![From Swagger of the MCVE, showing the two ResponseBody schemas](https://github.com/litestar-org/litestar/assets/7027949/6ccb9d2d-d9f9-43ad-ada8-3888f233a27c)

I thought it was an issue with SQLAlchemyDTO, but after simplifying the MCVE further, it presents itself with even DataclassDTO.



### URL to code causing the issue

_No response_

### MCVE

```python
from dataclasses import dataclass


from litestar import Litestar, get
from litestar.dto import DataclassDTO, DTOConfig


@dataclass
class User:
    name: str
    logs: list["Logfile"]


@dataclass
class Logfile:
    name: str
    user: User


class UserDTO(DataclassDTO[User]):
    config = DTOConfig()


class LogfileDTO(DataclassDTO[Logfile]):
    config = DTOConfig()


@get(path="/logs", dto=LogfileDTO)
async def get_logs() -> list[Logfile]:
    user = User(name="test", logs=[])
    logfiles = [Logfile(name="test", user=user)]
    user.logs = logfiles
    return logfiles


@get(path="/users", dto=UserDTO)
async def get_users() -> list[User]:
    user = User(name="test", logs=[])
    logfiles = [Logfile(name="test", user=user)]
    user.logs = logfiles
    return [user]


app = Litestar(
    route_handlers=[get_logs, get_users],
)
```


### Steps to reproduce

```bash
1. Run the MCVE
2. Go to the Swagger interface ex. http://127.0.0.1:8000/schema/swagger/
3. Scroll to the bottom
4. See error
```


### Screenshots

```bash
"![From Swagger of the MCVE, showing the two ResponseBody schemas](https://github.com/litestar-org/litestar/assets/7027949/6ccb9d2d-d9f9-43ad-ada8-3888f233a27c)
```


### Logs

_No response_

### Litestar Version

2.6.1

### Platform

- [X] Linux
- [ ] Mac
- [ ] Windows
- [ ] Other (Please specify in the description above)

<!-- POLAR PLEDGE BADGE START -->
---
> [!NOTE]  
> While we are open for sponsoring on [GitHub Sponsors](https://github.com/sponsors/litestar-org/) and 
> [OpenCollective](https://opencollective.com/litestar), we also utilize [Polar.sh](https://polar.sh/) to engage in pledge-based sponsorship.
>
> Check out all issues funded or available for funding [on our Polar.sh dashboard](https://polar.sh/litestar-org)
> * If you would like to see an issue prioritized, make a pledge towards it!
> * We receive the pledge once the issue is completed & verified
> * This, along with engagement in the community, helps us know which features are a priority to our users.

<a href="https://polar.sh/litestar-org/litestar/issues/3134">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/3134/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/3134/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Discussion:

Resolving patch:
diff --git a/litestar/dto/_backend.py b/litestar/dto/_backend.py
--- a/litestar/dto/_backend.py
+++ b/litestar/dto/_backend.py
@@ -406,6 +406,8 @@ def _create_transfer_type(
             if nested_depth == self.dto_factory.config.max_nested_depth:
                 raise RecursionError
 
+            unique_name = f"{unique_name}{field_definition.raw.__name__}"
+
             nested_field_definitions = self.parse_model(
                 model_type=field_definition.annotation,
                 exclude=exclude,


Test patch:
diff --git a/tests/unit/test_dto/test_factory/test_backends/test_backends.py b/tests/unit/test_dto/test_factory/test_backends/test_backends.py
--- a/tests/unit/test_dto/test_factory/test_backends/test_backends.py
+++ b/tests/unit/test_dto/test_factory/test_backends/test_backends.py
@@ -196,6 +196,7 @@ def handler(data: DC) -> DC:
     schemas = creator.schema_registry.generate_components_schemas()
     assert isinstance(ref, Reference)
     schema = schemas[ref.value]
+    assert schema.title == "HandlerDCResponseBody"
     assert schema.properties is not None
     a, b, c = schema.properties["a"], schema.properties["b"], schema.properties["c"]
     assert isinstance(a, Schema)
@@ -208,6 +209,7 @@ def handler(data: DC) -> DC:
     assert c.items.type == "integer"
     assert isinstance(nested := schema.properties["nested"], Reference)  # noqa: RUF018
     nested_schema = schemas[nested.value]
+    assert nested_schema.title == "HandlerDCNestedDCResponseBody"
     assert nested_schema.properties is not None
     nested_a, nested_b = nested_schema.properties["a"], nested_schema.properties["b"]
     assert isinstance(nested_a, Schema)