Entropi's avatar

Nested relationships with ordering and pagination

Hi all,

I'm trying build an achievements system using the simplified database schema and models listed at the bottom of this post. I've hit a bit of a roadblock when figuring out how to retrieve a user's unlocked achievements while ordering them and paginating.

The following is an example of my desired result to be outputted:

Category 1
  - Sequence 1
    - Achievement 1
    - Achievement 2
    - Achievement 3
  - Sequence 2
    - Achievement 4
    - Achievement 5
Category 2
  - Sequence 3
    - Achievement 6

The categories should be ordered by name, the sequences within a category should also be ordered by name, the achievements within a sequence should be ordered by level, and finally the result should be paginated by n sequences per page. Only achievements where progress unlocked at is not null should be included.

The following seems to work fine in terms of getting every achievement in the desired fashion (since I can simply output the category name by comparing current/previous category name when iterating through the sequences). However I'm trying to get the result based on the user's unlocked status rather than all achievements in the system.

AchievementSequence::query()
    ->join('achievement_categories', 'achievement_sequences.achievement_category_id', '=', 'achievement_categories.id')
    ->with([
        'achievements' => fn ($query) => $query->orderByDesc('level'),
        'achievementCategory'
    ])
    ->orderBy('achievement_categories.name')
    ->orderBy('achievement_sequences.name')
    ->paginate(5, 'achievement_sequences.*');

DB Schema: https://i.imgur.com/jWI2USz.png

// User.php
class User extends Authenticatable
{
    public function achievementProgress(): HasMany
    {
        return $this->hasMany(AchievementProgress::class);
    }
}

// AchievementCategory.php
class AchievementCategory extends Model
{
    public function achievementSequences(): HasMany
    {
        return $this->hasMany(AchievementSequence::class);
    }
}

// AchievementSequence.php
class AchievementSequence extends Model
{
    public function achievementCategory(): BelongsTo
    {
        return $this->belongsTo(AchievementCategory::class);
    }

    public function achievements(): HasMany
    {
        return $this->hasMany(Achievement::class);
    }
}

// Achievement.php
class Achievement extends Model
{
    public function achievementSequence(): BelongsTo
    {
        return $this->belongsTo(AchievementSequence::class);
    }

    public function progress(): HasMany
    {
        return $this->hasMany(AchievementProgress::class);
    }
}

// AchievementProgress.php
class AchievementProgress extends Model
{
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function achievement(): BelongsTo
    {
        return $this->belongsTo(Achievement::class);
    }
}
0 likes
5 replies
jlrdw's avatar

Just double or triple paginate:

Just simplified example:

Your case would be:

next Category

next Sequence

next Achievement

Use this as guide: http://novate.co.uk/using-multiple-pagination-links-on-one-page/

But in your case will have to keep the paginators in sync.

I have never needed triple pagination, but used double for accounts receivable and accounts payable reports.

Entropi's avatar

Hi @jlrdw, thanks for your response, however I think you misunderstood. I'm not looking to add several pagination links to the page. I just want to display the entire relationship tree for a user's achievement progress, while paginating just by the sequence. In other words, what I'm looking for is all eloquent/query builder help related.

So page one looks like:

Category 1
 - Sequence 1
   - Achievement 1 + When the user earned it
   - Achievement 2 + When the user earned it
   - Achievement 3  + When the user earned it
 - Sequence 2
   - Achievement 4 + When the user earned it
   - Achievement 5 + When the user earned it
Category 2
 - Sequence 3
   - Achievement 6 + When the user earned it

And page two looks like:

Category 2
 - Sequence 4
   - Achievement 7 + When the user earned it
   - Achievement 8 + When the user earned it
   - Achievement 9 + When the user earned it
Category 3
 - Sequence 5
   - Achievement 10 + When the user earned it
   - Achievement 11 + When the user earned it

Image version: https://i.imgur.com/dERIHgb.png

The categories should be ordered by name, the sequences within a category should also be ordered by name, the achievements within a sequence should be ordered by level, and finally the result should be paginated by n sequences per page. Only achievements where progress unlocked at is not null should be included.

Does that help clarify any?

Snapey's avatar

you cannot paginate based on a child

You would have to create a join of all three tables and paginate that. This means detecting changes in category and sequence un the view

jlrdw's avatar
jlrdw
Best Answer
Level 75

You could work out a paginator by category, you would probably want to use the lengthaware paginator and do some custom coding.

Basically:

Category 1
------- everything under category 1

Next category link

Remember that's why that length aware paginator is included it's very powerful for custom situations.

Also if your data isn't growing too large have you looked at trying to use collections.

Another alternative which I don't think is necessary is nested sets.

But want you are after can easily be worked out using regular SQL and PDO.

Entropi's avatar

Hmm that's unfortunate. I've managed to get it working by using a few queries and some collection methods and have a few ideas for how to optimize it further. But that may be unnecessary for slight performance gains since the amount of data shouldn't be ridiculous with my use case.

Nested sets seem like overkill. I'll play around with LengthAwarePaginator, it looks useful at a quick glance.

Thanks!

Please or to participate in this conversation.