Существует множество подходов к решению этой проблемы. Все зависит от того, сколько времени вы готовы потратить на их реализацию, а так же от используемых фреймворков. Если вы говорите про "модель", значит, вы используете паттерны либо ActiveRecord, либо Table.
1. Если вы используете готовые фреймворки, то юзайте инструментарий "из коробки" - для учебного проекта этого будет достаточно.
2. Если вы сами пишете, то тут возможны варианты:
2.1 Можно создавать классы-компоненты (storage), которые будет организовывать выборку и нужным образом компоновать классы между собой:
2.2 Можно создать в БД вьюхи (view), которые объединят нужные данные, а в коде использовать, как единую сущность.
2.3 Можно написать постобработку для CRUD-операций (триггеры в БД, events в коде и тд), которая будет собирать все данные в четвертую таблицу, отдельную. Тогда просто один класс доступа будет.
Варианты во 2-м пункте расположены по возрастанию уровня сложности.
Если вы сами пишете, то в том, что касается кода, я советую на выходе собирать единый объект (не используйте для композитного объекта понятие сущности - это не корректно). Как вы его будете собирать - зависит от того, что вы выберете из пунктов выше. Но в любом случае итоговый композит должен выглядить подобно:
class Comment
{
/**
* Я бы еще рекомендовал передавать связанные сущности в конструктор, ибо без них комментарий не имеет смысла
* @param User
* @param File | null
**/
public function __construct(User $user, File $file = null); //я б
/**
* тут никаких интерфейсов, ибо интерфейсы, дублирующие сущность - моветон
* @return User
**/
public function getUser(): User
/**
* @return File
**/
public function getFile(): File
}
Либо использовать фабрики. Но фабрики - это уже "на вкус и цвет", как говорится.