Au-delà de l’ORM: construction de SQL personnalisé

Dans ce tutoriel, nous allons construire une archive de date à l'aide de requêtes SQL personnalisées et présenter la classe SQLQuery.

 

 

Dépôt de code à télécharger

 

Dans le didacticiel précédent, nous avons activé la plupart des filtres de la barre latérale de notre section Guides de voyage. Nous avons toutefois omis le filtre d’archivage des dates, car il introduit une certaine complexité. Nous allons maintenant plonger dans cette complexité et la faire fonctionner.

 

Ajout de liens de filtres de date au modèle

En regardant le modèle, nous devons d’abord générer une liste de toutes les combinaisons mois / année distinctes pour tous les articles. Commençons par travailler en arrière et nous voulons que le résultat final soit sur le modèle.

app/templates/SilverStripe/Lessons/Layout/ArticleHolder.ss

 

  <!-- BEGIN ARCHIVES ACCORDION -->
    <h2 class="section-title">Archives</h2>
    <div id="accordion" class="panel-group blog-accordion">
        <div class="panel">
        <!--
            <div class="panel-heading">
                <div class="panel-title">
                    <a data-toggle="collapse" data-parent="#accordion" href="#collapseOne" class="">
                        <i class="fa fa-chevron-right"></i> 2014 (15)
                    </a>
                </div>
            </div>
        -->
            <div id="collapseOne" class="panel-collapse collapse in">
                <div class="panel-body">
                    <ul>
                    <% loop $ArchiveDates %>
                        <li><a href="$Link">$MonthName $Year ($ArticleCount)</a></li>
                    <% end_loop %>
                    </ul>
                </div>
            </div>
        </div>  
    </div>
    <!-- END  ARCHIVES ACCORDION -->

 

Tout d’abord, ces dates ont été regroupées par année. Nous avons commenté cela pour l’instant. Nous pourrons en parler dans un prochain tutoriel sur des listes groupées. Dans la boucle, chaque entrée de la date a une $Linkméthode qui ira à la liste des articles filtrés, $MonthNameet les $Yearpropriétés, et une $ArticleCountpropriété.

C’est bien beau, mais quels sont ces objets?

Dans le didacticiel précédent, nous avons traité du traitement des données de modèle arbitraires, ce qui constitue un cas d’utilisation parfait. Ces objets d’archive de date doivent être itérables dans une boucle et requièrent des propriétés dynamiques. Nous devrons les définir comme de simples ArrayDataobjets.

Construisons cette liste dans le modèle de notre ArticleHoldertype de page.

app/src/ArticleHolder.php

 

//...
use SilverStripe\ORM\ArrayList;
class ArticleHolder extends Page
{
    //...

    public function ArchiveDates()
    {
        $list = ArrayList::create();

        return $list;
    }

Exécuter une requête SQL personnalisée

Nous allons devoir lancer une requête assez spécifique sur la base de données pour obtenir tous les couples mois / année distincts, ce qui repousse réellement les limites et l’aspect pratique de l’ORM. Dans de rares cas tels que celui-ci, nous pouvons exécuter du SQL arbitraire en utilisant DB::query().

app / src / ArticleHolder.php

 

//...
use SilverStripe\ORM\ArrayList;
use SilverStripe\Versioned\Versioned;
use SilverStripe\ORM\Queries\SQLSelect;

class ArticleHolder extends Page
{
    public function ArchiveDates()
    {
        $list = ArrayList::create();
        $stage = Versioned::get_stage();        
    $baseTable = ArticlePage::getSchema()->tableName(ArticlePage::class);
    $tableName = $stage === Versioned::LIVE ? "{$baseTable}_Live" : $baseTable;

    $query = SQLSelect::create()
        ->setSelect([])
        ->selectField("DATE_FORMAT(`Date`,'%Y_%M_%m')", "DateString")
        ->setFrom($tableName)
        ->setOrderBy("DateString", "ASC")
        ->setDistinct(true);

    $result = $query->execute();

 

Pour travailler en dehors de l’ORM, nous pouvons utiliser la SilverStripe\ORM\Queries\SQLSelectclasse pour construire de manière procédurale une chaîne de sélection de SQL. Nous passons un tableau vide au constructeur, car par défaut, il sélectionnera *. Nous construisons ensuite la requête en utilisant des méthodes auto-descriptives, pouvant être chaînées.

Le principal avantage de cette couche d’abstraction est qu’elle est indépendante de la plate-forme. Ainsi, si vous modifiez un jour une plate-forme de base de données, vous n’aurez besoin de mettre à jour aucune syntaxe. Toutes les requêtes de sélection finissent dans SQLSelectSiteTree::get()est juste un niveau d’abstraction supérieur qui construit un SQLSelectobjet. Pour construire une requête vraiment personnalisée, nous allons simplement plus loin dans la chaîne alimentaire, pour ainsi dire.

Nous obtenons le nom de la table pour ArticlePagede la DataObjectSchemaclasse. Cette classe contient beaucoup d’informations précieuses pour l’introspection des abstractions de l’ORM. Vous pouvez lui demander le nom de la table pour une classe donnée, obtenir la colonne de la base de données pour un champ donné, obtenir tous les champs de la base de données pour une classe donnée, et bien plus encore. Dans ce cas, nous obtenons le nom de la table à partir du schéma. Les noms de table sont configurables par l’utilisateur, mais par défaut, le modèle simple consiste à remplacer les barres obliques inverses dans le nom de classe complet par des traits de soulignement. Dans ce cas, SilverStripe_Lessons_ArticlePageest retourné par la tableNamefonction.

Un inconvénient majeur de travailler en dehors de l’ORM est que nous ne pouvons plus prendre de versioning pour acquis. Nous devons être explicites sur la table à partir de laquelle nous voulons sélectionner. Il est donc impératif de vérifier l’état actuel et d’appliquer le suffixe nécessaire à la table, par exemple ArticlePage_Live . Encore une fois, il est rare que vous ayez à traiter de tels problèmes.

Ne vous inquiétez pas trop si cette requête est sur votre tête. Ce n’est pas souvent que nous devons faire des choses comme ça. Cette requête crée une SKU pour chaque article contenant son année, son numéro de mois et son nom de mois, séparés par des traits de soulignement, comme suit:

2015_05_May

Nous utilisons ensuite la setDistinct()méthode pour nous assurer de n’en obtenir qu’un seul.

Si vous vous demandez pourquoi nous avons besoin du nom et du numéro du mois. L’année et le nombre de mois suffisent à satisfaire le DISTINCTdrapeau. La réponse est que nous n’en avons pas vraiment besoin , mais cela nous aidera plus tard. Nous obtenons le nom du mois uniquement à des fins sémantiques, pour gagner du temps lorsque nous créons les liens sur le modèle. Le nom du mois convivial est nécessaire pour le texte du lien, mais le numéro du mois correspond à ce dont nous avons besoin pour l’URL.

Il ne reste plus maintenant qu’à parcourir les résultats de la base de données pour créer notre liste finale d’objets de date.

app / src / ArticleHolder.php

 

        if ($result) {
            while($record = $result->nextRecord()) {
                list($year, $monthName, $monthNumber) = explode('_', $record['DateString']);

                $list->push(ArrayData::create([
                    'Year' => $year,
                    'MonthName' => $monthName,
                    'MonthNumber' => $monthNumber,
                    'Link' => $this->Link("date/$year/$monthNumber"),
                    'ArticleCount' => ArticlePage::get()->where([
                            "DATE_FORMAT(\"Date\",'%Y_%m')" => "{$year}_{$monthNumber}",
                            "\"ParentID\"" => $this->ID
                        ])->count()
                ]));
            }
        }

 

Nous parcourons chaque enregistrement en utilisant la nextRecord()méthode. Pour chaque enregistrement, nous décomposons l’UGS en ses variables constitutives – l’année, le numéro du mois et le nom du mois – et nous les affectons aux propriétés d’un ArrayDataobjet. Nous créons également un lien vers l’ date/$year/$monthNumberitinéraire que nous avons créé ArticleHolder. Enfin, nous lançons une requête ArticlePagepour obtenir le nombre d’articles correspondant à cette date. Notez que dans ce cas, nous pouvons simplement faire correspondre le numéro d’année et de mois.

Notez que la where()méthode nous permet des requêtes paramétrées. Le raccourci de 'fieldName' => 'value'doit être utilisé chaque fois que possible pour vous assurer que vos requêtes sont protégées contre l’injection.

Voici la ArchiveDates()fonction complète :

app / src / ArticleHolder.php

 

    public function ArchiveDates()
    {
        $list = ArrayList::create();
        $stage = Versioned::get_stage();        
    $baseTable = ArticlePage::getSchema()->tableName(ArticlePage::class);
    $tableName = $stage === Versioned::LIVE ? "{$baseTable}_Live" : $baseTable;

    $query = SQLSelect::create()
        ->setSelect([])
        ->selectField("DATE_FORMAT(`Date`,'%Y_%M_%m')", "DateString")
        ->setFrom($tableName)
        ->setOrderBy("DateString", "ASC")
        ->setDistinct(true);

    $result = $query->execute();

        if ($result) {
            while($record = $result->nextRecord()) {
                list($year, $monthName, $monthNumber) = explode('_', $record['DateString']);

                $list->push(ArrayData::create([
                    'Year' => $year,
                    'MonthName' => $monthName,
                    'MonthNumber' => $monthNumber,
                    'Link' => $this->Link("date/$year/$monthNumber"),
                    'ArticleCount' => ArticlePage::get()->where([
                            "DATE_FORMAT(\"Date\",'%Y_%m')" => "{$year}_{$monthNumber}",
                            "\"ParentID\"" => $this->ID
                        ])->count()
                ]));
            }
        }

        return $list;

 

Bon, lève-toi, marche. Avoir une boisson (non alcoolisée). Ensuite, actualisez la page pour voir les fruits de votre travail.

 

Application du filtre de date dans le contrôleur

La dernière chose à faire pour que l’archivage des dates fonctionne est de configurer l’action du contrôleur pour gérer les date/$year/$monthitinéraires entrants .

app / src / ArticleHolderController.php

class ArticleHolderController extends PageController
{

    //...

    public function date(HTTPRequest $r)
    {
        $year = $r->param('ID');
        $month = $r->param('OtherID');

        if (!$year) return $this->httpError(404);

        $startDate = $month ? "{$year}-{$month}-01" : "{$year}-01-01";

        if (strtotime($startDate) === false) {
            return $this->httpError(404, 'Invalid date');
        } 
    }

 

Nous allons commencer par effectuer un contrôle de sécurité pour nous assurer qu’au moins une année est dans l’URL. Ensuite, nous créerons une date de début correspondant au premier du mois ou au premier de l’année. Si pour une raison quelconque les valeurs d’année ou de mois ne sont pas valides et ne réussissent pas le strtotime()test, une erreur HTTP est générée.

Nous allons maintenant créer la limite pour la date de fin et exécuter la requête.

app / src / ArticleHolderController.php

 

        $adder = $month ? '+1 month' : '+1 year';
        $endDate = date('Y-m-d', strtotime(
            $adder, 
                strtotime($startDate)
        ));

        $this->articleList = $this->articleList->filter([
            'Date:GreaterThanOrEqual' => $startDate,
            'Date:LessThan' => $endDate 
        ]);

        return [
            'StartDate' => DBField::create_field('Datetime', $startDate),
            'EndDate' => DBField::create_field('Datetime', $endDate)
        ];

 

Un détail essentiel de cette fonction est que nous renvoyons les SilverStripe\ORM\FieldType\DBFieldobjets appropriés au modèle. Si vous vous souvenez des premiers tutoriels, les contrôleurs ne renvoient pas simplement des valeurs scalaires au modèle. Ce sont en fait des objets intelligents de première classe. Par défaut, ils sont jetés comme des Textobjets, donc nous allons être plus explicites et veiller à ce que StartDateet EndDatesont exprimés en tant que dates. Cela nous donnera la possibilité de les formater sur le modèle.

Vous pouvez obtenir le même résultat de manière plus déclarative en utilisant le $castingréglage de votre contrôleur. Nous discuterons de cela dans un prochain tutoriel et en finirons un peu.

Pour l’instant, voici l’ date()action complète du contrôleur:

app / src / ArticleHolderController.php

 

//...
use SilverStripe\ORM\FieldType\DBField;

class ArticleHolderController extends PageController
{

    //...

    public function date(HTTPRequest $r)
    {
        $year = $r->param('ID');
        $month = $r->param('OtherID');

        if (!$year) return $this->httpError(404);

        $startDate = $month ? "{$year}-{$month}-01" : "{$year}-01-01";

        if (strtotime($startDate) === false) {
            return $this->httpError(404, 'Invalid date');
        }

        $adder = $month ? '+1 month' : '+1 year';
        $endDate = date('Y-m-d', strtotime(
            $adder, 
                strtotime($startDate)
        ));

        $this->articleList = $this->articleList->filter([
            'Date:GreaterThanOrEqual' => $startDate,
            'Date:LessThan' => $endDate 
        ]);

        return [
            'StartDate' => DBField::create_field('Datetime', $startDate),
            'EndDate' => DBField::create_field('Datetime', $endDate)
        ];

    }

    //...

 

Actualisez le navigateur et essayez de cliquer sur certains des liens d’archivage des dates et vérifiez que vous obtenez les résultats escomptés.

La dernière chose à faire est d’insérer nos en-têtes de filtre dans les listes pour montrer à l’utilisateur l’état de la liste. Chaque action du contrôleur renvoie ses propres variables de modèle personnalisées que nous pouvons vérifier.

app / templates / SilverStripe / Lessons / Layout / ArticleHolder.ss

 

    <div id="blog-listing" class="list-style clearfix">
        <div class="row">
            <% if $SelectedRegion %>
                <h3>Region: $SelectedRegion.Title</h3>
            <% else_if $SelectedCategory %>
                <h3>Category: $SelectedCategory.Title</h3>
            <% else_if $StartDate %>
                <h3>Showing $StartDate.Date to $EndDate.Date</h3>
            <% end_if %>

 

Nous allons également ajouter les dates aux articles eux-mêmes, en supprimant les dates statiques qui y figurent maintenant.

app / templates / SilverStripe / Lessons / Layout / ArticleHolder.ss

 

  <div class="info-blog">
    <ul class="top-info">
      <li><i class="fa fa-calendar"></i> $Date.Nice</li>
      <li><i class="fa fa-comments-o"></i> 2</li>
      <li><i class="fa fa-tags"></i> $CategoriesList</li>
    </ul>

 

C’est ici qu’avoir des SilverStripe\ORM\FieldType\Datetimeobjets appropriés est vraiment pratique, car nous pouvons formater la date directement sur le modèle.

TOUT VOIR Ajouter une remarque
VOUS
Ajoutez votre commentaire
 
© Academy EdTech. 2019 All rights reserved.
X