Получайте последнее сообщение (строку) для пользователя в Laravel

TL; DR: требуется последнее сообщение от каждого отправителя.

В моем приложении Laravel у меня есть две таблицы:

Пользователи

  • ID
  • имя

Сообщения:

  • ID
  • sender_id
  • recipient_id
  • Тело
  • created_at

И, конечно, модели.

Модель пользователя:

public function messages() {
    return $this->hasMany('App\Message', 'recipient_id');
}

Модель сообщений:

public function sender() {
    return $this->belongsTo('App\User', 'sender_id');
}

public function recipient() {
    return $this->belongsTo('App\User', 'recipient_id');
}

Когда пользователь открывает свой почтовый ящик, он должен увидеть список последних сообщений от любого другого пользователя.

Итак, если есть сообщения:

id sender_id recipient_id body created_at
1, 2,        1,           hi,  2016-06-20 12:00:00
2, 2,        1,           hi,  2016-06-21 12:00:00
3, 3,        1,           hi,  2016-06-20 12:00:00
4, 3,        1,           hi,  2016-06-21 12:00:00

Затем пользователь с идентификатором 1 (recipient_id) должен видеть только сообщения с идентификаторами 2 и 4.

Это текущее решение в модели пользователей:

return Message::whereIn('id', function($query) {
                $query->selectRaw('max(`id`)')
                ->from('messages')
                ->where('recipient_id', '=', $this->id)
                ->groupBy('sender_id');
            })->select('sender_id', 'body', 'created_at')
            ->orderBy('created_at', 'desc')
            ->get();

Это работает, но я блуждал, если можно достичь этого пути Laravel. Вероятно, с нетерпением. Мои навыки Laravel просто не достаточно, и после нескольких дней попыток у меня нет решения.

Спасибо.

Ответы

Ответ 1

Вдохновляя этот пост, самый эффективный способ сделать это будет следующим:

DB::table('messages AS m1')
    ->leftjoin('messages AS m2', function($join) {
        $join->on('m1.sender_id', '=', 'm2.sender_id');
        $join->on('m1.id', '<', 'm2.id')
    })->whereNull('m2.id')
    ->select('m1.sender_id', 'm1.body', 'm1.created_at')
    ->orderBy('m1.created_at', 'm1.desc')->get();

Хотя это не самый дружелюбный к Laravel, это лучшее решение, основанное на производительности, как указано в сообщении, указанном в этом ответе выше.

Ответ 2

Почему бы просто не получить доступ к messages, например:

// get the authenticated user
$user = \Auth::user(); 

// find the messages for that user
return User::with('message')->find($user->id)->messages;

Ответ 3

Я попробовал подобный подход и обнаружил, что вам нужно только заказатьBy created_at, сразу вы найдете все сообщения User, затем вы можете использовать Laravel Collection, чтобы сгруппировать их по sender_id. Поэтому, насколько мне известно, следующий подход должен работать, то есть вы получите последнее сообщение, полученное пользователем от отправителя s:

Предположим, что у вас есть Аутентифицированный User как $user, который в этом контексте совпадает с receiver с receiver_id в таблице messages, а затем:

$messages = $user->messages()
    ->orderBy('created_at', 'desc')
    ->get(['sender_id', 'body', 'created_at'])
    ->groupBy('sender_id'); //this is collections method

Затем сверните вокруг коллекции и выберите первый:

$result =  new \Illuminate\Database\Eloquent\Collection ();
foreach ($messages as $message){
    $result->push($message->first()); //the first model in the collection is the latest message
}

Вы должны получить результат, например:

 Illuminate\Database\Eloquent\Collection {#875
     all: [
       App\Message {#872
        sender_id: "2",
        body: "hi",
        created_at: "2016-06-21 12:00:00",
      },
      App\Message {#873
        sender_id: "4",
        body: "hi",
        created_at: "2016-06-21 12:00:00",
      },
    ]

PS: Заметьте, что я не могу сказать, насколько это эффективно, но одно можно сказать наверняка, это ограниченное количество запросов на db.

Надеюсь, это поможет:)

ОБНОВЛЕНИЕ:

Я сломаю его, когда попробую. ~

Он извлекает все записи messages, принадлежащие пользователю, в коллекцию (уже упорядоченную по create_at), затем используя laravel groupBy() у вас есть результат, подобный примеру, указанному в этом документе.

Группа LaravelBy()

На этот раз я не конвертировался в Array. Вместо этого это коллекция коллекций. (коллекция (Сообщения))

Затем вы выбираете первую модель для каждого индекса. Параметры, которые я уже передал в метод get(), обеспечивают только те поля (т.е. ->get(['sender_id', 'body', 'created_at'])). Это полностью отличается от mysql groupBy(), поскольку он не возвращает одну строку для каждой группы, а просто группирует все записи по данному идентификатору.

ДРУГОЕ ОБНОВЛЕНИЕ

Позже я обнаружил, что вызов метода unique() в результирующих упорядоченных сообщениях действительно выполнит задание, но в этом случае уникальный идентификатор будет sender_id. (ref: Laravel unique) То есть:

$messages = $user->messages()
    ->orderBy('created_at', 'desc')
    ->get(['sender_id', 'body', 'created_at'])
    ->unique('sender_id'); //unique replaces `groupBy`

Следствием этого является то, что нам не нужны foreach и groupBy. Здесь мы имеем результат.

Другим способом избежать повторения (из вышеперечисленного), если это необходимо в нескольких местах, является использование область запроса Laravel, т.е. в модели Message мы можем иметь что-то такое:

public function scopeLatestSendersMessages($query) 
{
     return $query->orderBy('created_at', 'desc')
         ->get(['sender_id', 'body', 'created_at'])
         ->unique('sender_id');
}

Затем в контроллере используйте:

$messages = $user->messages()->latestSendersMessages();

PS: Не уверен, какой из них оптимально лучше другого.

Ответ 4

это может быть решение (не проверено)

User::with([
'messages' => function ($q) {
    $q->select('sender_id', 'body')->groupBy('sender_id')->orderBy('created_at', 'desc');
}
])->find(1);

Ответ 5

Мне нравится более простой подход, который упоминается здесь.

В вашей модели User дополнительно к существующей связи messages() добавьте это отношение

public function latestMessage() 
{
    return $this->hasOne(Message::class, 'recipient_id')->latest();
}

Затем, когда вы запрашиваете просто такой запрос.

$messages = User::with('latestMessage')->get();

$messages содержит последнее сообщение для пользователя.

Edit

Чтобы заказать результат с помощью datetime/id, вы можете сделать это следующим образом.

$messages = User::with(['latestMessage' => function($message) {
    $message->orderBy('id', 'desc');
}])->get();

$messages содержит последнее сообщение для пользователя, заказанного id. Обратитесь к этому вопросу

Ответ 6

Это самый "необычный способ", который я нашел из этого:

В модели пользователя:

public function messages() {
    return $this->hasMany(Message::class, 'recipient_id');
}

public function latestMessagePerSender()
{
    return $this->messages()
        ->whereNotExists(function ($query) {
            $query->from('messages AS m2')
                ->whereRaw('m2.sender_id = messages.sender_id')
                ->whereRaw('m2.created_at > messages.created_at');
    });
}

Тогда просто $user->latestMessagePerSender

Ответ 7

Возможно, вы можете попробовать следующее:

        $user = \Auth::user(); 

        // Get the latest date
        $last_date_created = Message::latest()->first()->created_at; // also check for possible null

        // Get the date only - use to specify date range in the where section in the eloquent query
        $target_date = date('Y-m-d', strtotime( $last_date_created ) );

        // Retrieve the messages
        $latest_posts = Message::where('recipient_id', $user->id)
                               ->where('created_at', '>=', $target_date . ' 00:00:00')
                               ->where('created_at', '<=',  $target_date . ' 23:59:59')
                               ->groupBy('sender_id')
                               ->groupBy('created_at')
                               ->get();

        return $latest_posts;

Это может быть не очень эффективно, поскольку для выполнения этой задачи потребовалось два запроса, но вы получите преимущества благодаря читаемости кода.

Я надеюсь, что это работает так, как должно быть. Я не тестировал его, но как обычно я это делаю... Приветствия!

Ответ 8

Я нашел это решение на другом форуме, думаю, это то, что вы искали. Я публикую его, чтобы он мог быть полезен другим пользователям

        DB::select('
            SELECT t1.*
            FROM messages AS t1
            INNER JOIN
            (
                SELECT
                    LEAST(sender_id, recipient_id) AS user_id,
                    GREATEST(sender_id, recipient_id) AS recipient_id,
                    MAX(id) AS max_id
                FROM messages
                GROUP BY
                    LEAST(sender_id, recipient_id),
                    GREATEST(sender_id, recipient_id)
            ) AS t2
                ON LEAST(t1.sender_id, t1.recipient_id) = t2.sender_id AND
                   GREATEST(t1.sender_id, t1.recipient_id) = t2.recipient_id AND
                   t1.id = t2.max_id
                WHERE t1.sender_id = ? OR t1.recipient_id = ?
            ', [auth()->guard('api')->user()->id, auth()->guard('api')->user()->id]);

оригинальное сообщение: https://laracasts.com/discuss/channels/laravel/get-the-latest-message-of-chat-model-with-mysql-just-cannot-get-the-idea-how-to-do-this?page=1#reply=392529

Ответ 9

Вы можете попробовать этот

Messages::where('recipient_id',**{USER_ID}**)
          ->group_by('sender_id')
          ->order_by('id','desc')
          ->get();