[SQL/PHP/Laravel] Status gebaseerd op child-status

Pagina: 1
Acties:

Acties:
  • 0 Henk 'm!

  • Edwin88
  • Registratie: Januari 2005
  • Laatst online: 03-10 23:05
Hallo allemaal

Ik heb, misschien, een hele simpele vraag maar ik weet niet zo goed hoe ik dit aan moet pakken:

Ik ben een Laravel (5.1) API aan het schrijven voor een fitness app (Angular) waarbij je trainingen kan maken van oefeningen (gekoppeld aan exercises). Die training maak je door verschillende training_steps achter elkaar te plakken.

Versimpelde tabellen structuur:

* trainings
- id
- status

* training_steps
- id
- training_id
- status

Niet heel bijzonder dus, en een gebruikelijke opstelling denk ik.

Je ziet hem misschien al aankomen; de status van de 'training' hangt af van de status van de verschillende training_steps. Ik vraag me nu alleen nu af; wat is de beste plaats om deze logic te verwerken?

- controleer ik bij elke update van training_steps->status of alle training_steps.status true is en update ik dan ook de training_status?
- is er een manier om in Laravel (Eloquent) de status op te halen afhankelijk van de substatus (en soort subquery?)

Het is een API met doel in de toekomst wellicht ook andere front-apps te maken die communiceren met de API, dus de logic hoort eigenlijk aan de PHP kant thuis.
Ik bedenk me ook dat de API endpoint moet teruggeven of de training_status nu completed word... wellicht:
code:
1
2
3
4
return [
  "completed" => true,
  "completes_training" => true|false
]


Ik ben niet zo sterk met Laravel (al programmerende leert men) en hier zou het fijn zijn als iemand me wat richting kan geven :)

Acties:
  • 0 Henk 'm!

  • Edwin88
  • Registratie: Januari 2005
  • Laatst online: 03-10 23:05
Update even als nieuwe post:

PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    /**
     * Mark a training step as complete
     * @param $step_id
     */
    public function completeStep($step_id) {
        $step = $this->model->findOrFail($step_id);
        $step->status = true;
        $success = $step->save();
        // Check if this completes the training
        $completes_training = $this->model->where('training_id', $step->training_id)->min('status');
        return [
            "success" => $success,
            "completes_training" => $completes_training == 1 ? true : false
        ];
    }


Dit werkt in ieder geval;

Is het dan nu het handigst om direct de status van de training dan te updaten, of is daar een subquery gebruiken om de status dynamisch op te halen handiger? Het lijkt me dat je niet handmatig dingen wilt gaan updaten???

Acties:
  • 0 Henk 'm!

  • vdstaak
  • Registratie: Oktober 2015
  • Laatst online: 30-09 08:56
Is het geen optie om bij je Training model ook een completed() toe te voegen? Kun je daar al je completed logic in kwijt, en die heeft toegang tot alle childs. Zou je op je Training_step ook nog een where functie toe kunnen voegen die kijkt of het completed is.

(even heel kort geschreven, zit met ander werk :P )

Acties:
  • 0 Henk 'm!

  • Edwin88
  • Registratie: Januari 2005
  • Laatst online: 03-10 23:05
Hoi

Om nog even terug te komen op deze structuur; ik wil nu voor de 'trainings' resource opvragen aan de database hoeveel training_steps er zijn en hoeveel er dan completed zijn. Dus een ' 5/8 stappen compleet ' opzet.

Nu kan ik dat in SQL / Laravel vrij simpel doen, echter niet heel erg efficient:

SQL:
1
2
3
SELECT COUNT('trainings_steps') AS completed FROM training_steps WHERE `status` = 1 && `training_id` = 1

SELECT COUNT('trainings_steps') AS total FROM training_steps WHERE `training_id` = 1


Ik werk met eloquent, incl eager loading van eventueel andere info (gebaseerd op een 'include' string die je mee kan geven aan de API). Via de Larasponse (daar komt de parse_includes vandaan) library kan je via 'transformers' heel mooi API responses vormgeven:

TrainingTransformer
PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class TrainingTransformer extends TransformerAbstract {

    protected $availableIncludes = [
        'creator',
        'owner'
    ];

    public function transform(Training $training)
    {
        $overdue = false;
        // If the status is 'waiting' but it's overdue (now > due_date) flag it as overdue
        if(new \DateTime() > new \DateTime($training->due_date) && $training->status == 0) {
            $overdue = true;
        }

        return [
            'id'   => $training->id,
            'due_date'      => date_create($training->due_date)->format(DATE_ISO8601), // return in a format that Angular understands
            'title'         => $training->title,
            'status'        => $training->status,
            'overdue'       => $overdue        
        ];
    }

    public function includeCreator(Training $training)
    {
        return $this->item($training->creator, new UserTransformer());
    }

    public function includeOwner(Training $training)
    {
        return $this->item($training->owner, new UserTransformer());
    }
}


Alleen als je bijvoorbeeld de 'creator' van een training ook wilt weten, doet hij een enkele query naar 'users -> creators' waar hij alle ID's ophaalt van het resultaat van de exercises query:

getTrainings functie in de repository:
PHP:
1
        $trainings = $this->model->with($this->parse_eager_includes($include))->where('owner_id', $user_id)->orderBy('due_date', 'desc')->get();


Laravel querylog:
SQL:
1
2
[2016-04-28 19:40:19] log.INFO: select * from `trainings` where `owner_id` = ? order by `due_date` desc {"bindings":[2],"time":0.54} []
[2016-04-28 19:40:19] log.INFO: select * from `users` where `users`.`id` in (?, ?, ?, ?, ?, ?, ?) {"bindings":[10,8,3,4,5,6,9],"time":0.48} []


Als iemand bijvoorbeeld 20 trainingen heeft in een overzicht, dan zou ik 2 queries nodig hebben voor de 'trainings' en 'creator' info, maar om de status te verzamelen krijg je max 2 queries per training -> nog eens 40!

Training model
PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Training extends Model {

    protected $table = "trainings";

    public $timestamps = true;

    // Relations
    public function owner() {
        return $this->belongsTo('App\Models\User', 'owner_id');
    }

    public function creator() {
        return $this->belongsTo('App\Models\User', 'creator_id');
    }

    public function trainingSteps() {
        return $this->hasMany('App\Models\TrainingStep');
    }
    
}


TrainingStep model
PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class TrainingStep extends Model {

    use \Rutorika\Sortable\SortableTrait;

    protected $table = "training_steps";

    public $timestamps = true;

    // Relations
    public function owner() {
        return $this->training->owner;
    }

    public function training() {
        return $this->belongsTo('App\Models\Training');
    }

    public function exercise() {
        return $this->belongsTo('App\Models\Exercise');
    }

}


Ik ben nog niet zo sterk in pure SQL en geavanceerde database dingen omdat ik tot nu toe in Eloquent het meeste kon voltooien; je kan immers niet alles in 1 x leren :/

Kan iemand mij in de juist richting sturen hoe ik dit zou kunnen aanpakken? Is dit iets wat je in SQL kan oplossen?

Acties:
  • 0 Henk 'm!

  • The Eagle
  • Registratie: Januari 2002
  • Laatst online: 00:09

The Eagle

I wear my sunglasses at night

Ja hoor, dat kan prima in SQL :)
En qua logic: in principe wil je alle logic die nodig is, bij iedere save nalopen. Kan best wel eens zijn namelijk dat er ook nog andere dingen nagelopen moeten worden bij een save, kan dan ook op die plek.
Verder moet je je qua logic even afvragen of je een harde volgordelijkheid wilt hanteren, of een losse. Oftewel: als ik 6 oefeningen in een training heb die ik niet in volgorde doe, is het trainingsprogramma dan ook compleet? Zo ja, dan kun je voltooien als alle onderdelen true zijn. Is er wel een volgordelijkheid noodzakelijk, dan update je training pas als het max (id) van de training_steps de status true krijgt.

En in dit geval kun je natuurlijk ook een trucje gebruiken om te kijken of je moet updaten; als er eentje in training_steps nog op false staat mag je training niet op true gezet worden. Stukje pseudocode voor beide gevallen:
Indien volgordelijkheid niet verplicht:
code:
1
2
3
4
Completed = "Select count(*) from training_steps where id =$id and status='false';"
If completed = 0 then
Exec "update training set completed=true where id=$id;";
Endif;


Indien wel volgordelijkheid verplicht:
code:
1
2
3
4
Completed = "Select status from training_steps where id =$id and training_id= (select max(training_id) from training_steps where id=$id);"
If completed = true then
Exec "update training set completed=true where id=$id;";
Endif;

In dit laatste geval kijk je met een subquery dus even wat het hoogste training_id is, want dat moet afgerond zijn voor de rest afgerond is. Uiteraard moet je dan ergens anders nog even de logica bouwen die checkt waar je gebleven bent en weigert oefening 6 voor oefening 4 te doen. Maar dat is een soortgelijke query :)

* The Eagle een aantal jaren hardcore ERP HR en Financial systemen heeft lopen progselen, dit is voor mij vrij simpele logica :P

Al is het nieuws nog zo slecht, het wordt leuker als je het op zijn Brabants zegt :)


Acties:
  • 0 Henk 'm!

  • Edwin88
  • Registratie: Januari 2005
  • Laatst online: 03-10 23:05
The Eagle schreef op donderdag 28 april 2016 @ 22:26:
Ja hoor, dat kan prima in SQL :)
En qua logic: in principe wil je alle logic die nodig is, bij iedere save nalopen. Kan best wel eens zijn namelijk dat er ook nog andere dingen nagelopen moeten worden bij een save, kan dan ook op die plek.
Dus eigenlijk gebruik je geen subqueries om de status op te halen als je de training zelf ophaalt, maar update je de 'status' van de training elke keer als je een step completed of incomplete afvinkt?

De volgorde maakt niet uit (daar willen we flexibel in zijn), wel had ik bedacht om een aantal/totaal counter te hebben. Maar nu ik daar over nadenk; zo belangrijk is dat eigenlijk niet. Een atleet maakt in principe zijn training binnen 1 sessie af, dus in realiteit zullen ze een aantal/totaal count niet eens zo vaak zien.

Als ik dan simpelweg de training voltooi als alle subtrainingen voltooid zijn, is het qua queries lekker snel en ook simpel te houden.

Als het zo simpel is, is het dan haalbaar om in bijvoorbeeld een subquery voor elke training de min-status op te halen (als die 1 is dan is ie compleet want geen 0 status in steps)? Is dat dan beter dan handmatig de status flag up to date te houden?

Acties:
  • 0 Henk 'm!

  • ajakkes
  • Registratie: Maart 2004
  • Laatst online: 16-05 22:32

ajakkes

👑

Ik denk dat er situaties kunnen gaan zijn waarbij de training status 1 is en de steps nog niet allemaal 1. Daarnaast haal je heel vaak de training status op en voltooi je maar af en toe een step. Daarom kan je die status best hard op slaan. Het aantal afgeronde en totaal aantal steps is overigens ook met een 1 subquery op te halen.

👑


Acties:
  • 0 Henk 'm!

  • The Eagle
  • Registratie: Januari 2002
  • Laatst online: 00:09

The Eagle

I wear my sunglasses at night

Edwin88 schreef op vrijdag 29 april 2016 @ 06:55:
[...]


Dus eigenlijk gebruik je geen subqueries om de status op te halen als je de training zelf ophaalt, maar update je de 'status' van de training elke keer als je een step completed of incomplete afvinkt?
Zoiets idd. Soort van deferred processing: een gewijzigde status wordt pas opgeslagen als iemand op save klikt, en op dat moment kan ook andere evaluatiecode af gaan die met die save te maken heeft. De atleet inschrijven voor de volgende training bijvoorbeeld ;)
De volgorde maakt niet uit (daar willen we flexibel in zijn), wel had ik bedacht om een aantal/totaal counter te hebben. Maar nu ik daar over nadenk; zo belangrijk is dat eigenlijk niet. Een atleet maakt in principe zijn training binnen 1 sessie af, dus in realiteit zullen ze een aantal/totaal count niet eens zo vaak zien.

Als ik dan simpelweg de training voltooi als alle subtrainingen voltooid zijn, is het qua queries lekker snel en ook simpel te houden.
Aangenomen dat je met een index werkt ga je het verschil bij dergelijke kleine aantallen ws niet eens merken, tenzij de tabel heel groot is en je je indexen vernaggeld :P
Als het zo simpel is, is het dan haalbaar om in bijvoorbeeld een subquery voor elke training de min-status op te halen (als die 1 is dan is ie compleet want geen 0 status in steps)? Is dat dan beter dan handmatig de status flag up to date te houden?
Je kunt een boolean met true of false (of Y en N) indelen (character veld) of met 0 en 1 (numeric field). Een num(1) heeft het voordeel dat je ook met wiskundige functies aan de slag kunt. De modulus zie je veel gebruikt. Maar dat soort bewerkingen zijn relatief duur want je hebt altijd een extra bewerking op je child records. Simpelweg zoeken naar een false of N waarde is dan goedkoper :)

Tot slot: natuurlijk kun je met een enkel statement de hele zut ophalen en het resultaat evalueren. Maar voor de simpelheid en uitbreidbaarheid zou ik gewoon lekker wat weinig kostende queries bouwen. Houdt de code ook een stuk onderhoudbaarder :)

Al is het nieuws nog zo slecht, het wordt leuker als je het op zijn Brabants zegt :)

Pagina: 1