Noelia  Graham

Noelia Graham

1660513680

Recherche De Base Et En Texte Intégral Avec Django Et Postgres

Contrairement aux bases de données relationnelles, la recherche plein texte n'est pas standardisée. Il existe plusieurs options open source comme ElasticSearch, Solr et Xapian. ElasticSearch est probablement la solution la plus populaire ; cependant, il est compliqué à configurer et à entretenir. De plus, si vous ne profitez pas de certaines des fonctionnalités avancées offertes par ElasticSearch, vous devez vous en tenir aux capacités de recherche en texte intégral que de nombreuses bases de données relationnelles (comme Postgres, MySQL, SQLite) et non relationnelles (comme MongoDB et CouchDB ) offrir. Postgres en particulier est bien adapté à la recherche en texte intégral. Django le prend également en charge immédiatement.

Pour la grande majorité de vos applications Django, vous devriez, à tout le moins, commencer par tirer parti de la recherche en texte intégral de Postgres avant de vous tourner vers une solution plus puissante comme ElasticSearch ou Solr.

Dans ce didacticiel, vous apprendrez à ajouter une recherche de base et en texte intégral à une application Django avec Postgres. Vous optimiserez également la recherche en texte intégral en ajoutant un champ vectoriel de recherche et un index de base de données.

Objectifs

À la fin de ce didacticiel, vous serez en mesure de :

  1. Configurer la fonctionnalité de recherche de base dans une application Django avec le module d'objet Q
  2. Ajouter une recherche en texte intégral à une application Django
  3. Triez les résultats de recherche en texte intégral par pertinence à l'aide de techniques de radicalisation, de classement et de pondération
  4. Ajouter un aperçu à vos résultats de recherche
  5. Optimisez la recherche en texte intégral avec un champ vectoriel de recherche et un index de base de données

Configuration et aperçu du projet

Clonez la branche de base à partir du référentiel django-search :

$ git clone https://github.com/testdrivenio/django-search --branch base --single-branch
$ cd django-search

Vous utiliserez Docker pour simplifier la configuration et l'exécution de Postgres avec Django.

À partir de la racine du projet, créez les images et lancez les conteneurs Docker :

$ docker-compose up -d --build

Ensuite, appliquez les migrations et créez un superutilisateur :

$ docker-compose exec web python manage.py makemigrations
$ docker-compose exec web python manage.py migrate
$ docker-compose exec web python manage.py createsuperuser

Une fois cela fait, accédez à http://127.0.0.1:8011/quotes/ pour vous assurer que l'application fonctionne comme prévu. Vous devriez voir ce qui suit :

Page d'accueil du devis

Vous voulez apprendre à travailler avec Django et Postgres ? Consultez l' article Dockeriser Django avec Postgres, Gunicorn et Nginx .

Prenez note du Quotemodèle dans quotes/models.py :

from django.db import models

class Quote(models.Model):
    name = models.CharField(max_length=250)
    quote = models.TextField(max_length=1000)

    def __str__(self):
        return self.quote

Ensuite, exécutez la commande de gestion suivante pour ajouter 10 000 devis à la base de données :

$ docker-compose exec web python manage.py add_quotes

Cela prendra quelques minutes. Une fois cela fait, accédez à http://127.0.0.1:8011/quotes/ pour voir les données.

La sortie de la vue est mise en cache pendant cinq minutes, vous pouvez donc commenter le fichier @method_decoratorin quotes/views.py pour charger les guillemets. Assurez-vous de supprimer le commentaire une fois terminé.

Page d'accueil du devis

Dans le fichier quotes/templates/quote.html , vous avez un formulaire basique avec un champ de saisie de recherche :

<form action="{% url 'search_results' %}" method="get">
  <input
    type="search"
    name="q"
    placeholder="Search by name or quote..."
    class="form-control"
  />
</form>

Lors de la soumission, le formulaire envoie les données au backend. Une GETrequête est utilisée plutôt qu'une POSTafin que nous ayons accès à la chaîne de requête à la fois dans l'URL et dans la vue Django, permettant aux utilisateurs de partager les résultats de la recherche sous forme de liens.

Avant d'aller plus loin, jetez un coup d'œil à la structure du projet et au reste du code.

Recherche de base

En ce qui concerne la recherche, avec Django, vous commencerez généralement par effectuer des requêtes de recherche avec containsou icontainspour des correspondances exactes. L' objet Q peut également être utilisé pour ajouter des opérateurs logiques AND ( &) ou OR ( ).|

Par exemple, en utilisant l'opérateur OR, remplacez la valeur par SearchResultsListdéfaut de 's QuerySetdans quotes/views.py comme ceci :

class SearchResultsList(ListView):
    model = Quote
    context_object_name = "quotes"
    template_name = "search.html"

    def get_queryset(self):
        query = self.request.GET.get("q")
        return Quote.objects.filter(
            Q(name__icontains=query) | Q(quote__icontains=query)
        )

Ici, nous avons utilisé la méthode de filtrage pour filtrer les champs ou name. quoteDe plus, nous avons également utilisé l' extension icontains pour vérifier si la requête est présente dans les champs nameou quote(insensible à la casse). Un résultat positif sera renvoyé si une correspondance est trouvée.

N'oubliez pas l'importation :

from django.db.models import Q

Essaye le:

Page de recherche

Pour les petits ensembles de données, c'est un excellent moyen d'ajouter des fonctionnalités de recherche de base à votre application. Si vous avez affaire à un ensemble de données volumineux ou si vous souhaitez une fonctionnalité de recherche qui ressemble à un moteur de recherche Internet, vous souhaiterez passer à la recherche en texte intégral.

Recherche en texte intégral

La recherche de base que nous avons vue précédemment présente plusieurs limites, en particulier lorsque vous souhaitez effectuer des recherches complexes.

Comme mentionné, avec la recherche de base, vous ne pouvez effectuer que des correspondances exactes.

Une autre limitation est celle des mots vides . Les mots vides sont des mots tels que "un", "un" et "le". Ces mots sont courants et insuffisamment significatifs, ils doivent donc être ignorés. Pour tester, essayez de rechercher un mot précédé de "le". Disons que vous avez recherché "le milieu". Dans ce cas, vous ne verrez que les résultats pour "le milieu", donc vous ne verrez aucun résultat contenant le mot "moyen" sans "le" avant.

Supposons que vous ayez ces deux phrases :

  1. Je suis au milieu.
  2. Tu n'aimes pas le collège.

Vous obtiendrez les éléments suivants renvoyés avec chaque type de recherche :

RequêteRecherche de baseRecherche en texte intégral
"le milieu"première1 et 2
"milieu"1 et 21 et 2

Un autre problème est celui d'ignorer des mots similaires. Avec la recherche de base, seules les correspondances exactes sont renvoyées. Cependant, avec la recherche plein texte, les mots similaires sont pris en compte. Pour tester, essayez de trouver des mots similaires comme "poney" et "poneys". Avec la recherche de base, si vous recherchez "poney", vous ne verrez pas les résultats contenant "poneys" - et vice versa.

Dites que vous avez ces deux phrases.

  1. je suis un poney.
  2. Vous n'aimez pas les poneys

Vous obtiendrez les éléments suivants renvoyés avec chaque type de recherche :

RequêteRecherche de baseRecherche en texte intégral
"poney"première1 et 2
"poneys"21 et 2

Avec la recherche en texte intégral, ces deux problèmes sont atténués. Cependant, gardez à l'esprit qu'en fonction de votre objectif, la recherche en texte intégral peut en fait diminuer la précision (qualité) et le rappel (quantité de résultats pertinents). En règle générale, la recherche de texte intégral est moins précise que la recherche de base, car la recherche de base donne des correspondances exactes. Cela dit, si vous effectuez une recherche dans de grands ensembles de données avec de gros blocs de texte, la recherche en texte intégral est préférable car elle est généralement beaucoup plus rapide.

La recherche en texte intégral est une technique de recherche avancée qui examine tous les mots de chaque document stocké en essayant de faire correspondre les critères de recherche. De plus, avec la recherche en texte intégral, vous pouvez utiliser des racines spécifiques à la langue sur les mots indexés. Par exemple, les mots "drives", "drove" et "driven" seront enregistrés sous le mot concept unique "drive". La radicalisation est le processus de réduction des mots à leur racine, leur base ou leur racine.

Il suffit de dire que la recherche plein texte n'est pas parfaite. Il est susceptible de récupérer de nombreux documents qui ne sont pas pertinents (faux positifs) pour la requête de recherche prévue. Cependant, certaines techniques basées sur des algorithmes bayésiens peuvent aider à réduire ces problèmes.

Pour profiter de la recherche plein texte de Postgres avec Django, ajoutez django.contrib.postgresà votre INSTALLED_APPSliste :

INSTALLED_APPS = [
    ...

    "django.contrib.postgres",  # new
]

Examinons ensuite deux exemples rapides de recherche plein texte, sur un seul champ et sur plusieurs champs.

Recherche de champ unique

Mettez à jour la get_querysetfonction sous la SearchResultsListfonction d'affichage comme ceci :

class SearchResultsList(ListView):
    model = Quote
    context_object_name = "quotes"
    template_name = "search.html"

    def get_queryset(self):
        query = self.request.GET.get("q")
        return Quote.objects.filter(quote__search=query)

Ici, nous configurons la recherche en texte intégral sur un seul champ - le champ de citation.

Page de recherche

Comme vous pouvez le voir, il prend en compte les mots similaires. Dans l'exemple ci-dessus, "poneys" et "poney" sont traités comme des mots similaires.

Recherche multi-champs

Pour effectuer une recherche sur plusieurs champs et sur des modèles associés, vous pouvez utiliser la SearchVectorclasse.

Encore une fois, mettez à jour SearchResultsList:

class SearchResultsList(ListView):
    model = Quote
    context_object_name = "quotes"
    template_name = "search.html"

    def get_queryset(self):
        query = self.request.GET.get("q")
        return Quote.objects.annotate(search=SearchVector("name", "quote")).filter(
            search=query
        )

Pour effectuer une recherche sur plusieurs champs, vous annotez le jeu de requêtes à l'aide d'un SearchVector. Le vecteur correspond aux données que vous recherchez, qui ont été converties en un formulaire facile à rechercher. Dans l'exemple ci-dessus, ces données sont les champs nameet de votre base de données.quote

Assurez-vous d'ajouter l'importation :

from django.contrib.postgres.search import SearchVector

Essayez quelques recherches.

Enracinement et classement

Dans cette section, vous combinerez plusieurs méthodes telles que SearchVector , SearchQuery et SearchRank pour produire une recherche très robuste qui utilise à la fois la radicalisation et le classement.

Encore une fois, le stemming est le processus de réduction des mots à leur forme de radical, de base ou de racine. Avec la racine, des mots comme "enfant" et "enfants" seront traités comme des mots similaires. Le classement, en revanche, nous permet de classer les résultats par pertinence.

Mise à jour SearchResultsList:

class SearchResultsList(ListView):
    model = Quote
    context_object_name = "quotes"
    template_name = "search.html"

    def get_queryset(self):
        query = self.request.GET.get("q")
        search_vector = SearchVector("name", "quote")
        search_query = SearchQuery(query)
        return (
            Quote.objects.annotate(
                search=search_vector, rank=SearchRank(search_vector, search_query)
            )
            .filter(search=search_query)
            .order_by("-rank")
        )

Qu'est-ce qu'il se passe ici?

  1. SearchVector- encore une fois, vous avez utilisé un vecteur de recherche pour effectuer une recherche dans plusieurs champs. Les données sont converties sous une autre forme puisque vous ne recherchez plus simplement le texte brut comme vous le faisiez lorsqu'il icontainsétait utilisé. Par conséquent, avec cela, vous pourrez facilement rechercher des pluriels. Par exemple, rechercher "flask" et "flasks" donnera la même recherche car ils sont, eh bien, fondamentalement la même chose.
  2. SearchQuery- traduit les mots qui nous sont fournis sous forme de requête à partir du formulaire, les fait passer par un algorithme de radicalisation, puis recherche des correspondances pour tous les termes résultants.
  3. SearchRank- permet de classer les résultats par pertinence. Il prend en compte la fréquence d'apparition des termes de la requête dans le document, la proximité des termes sur le document et l'importance de la partie du document où ils apparaissent.

Ajoutez les importations :

from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank

Page de recherche

Comparez les résultats de la recherche de base à ceux de la recherche en texte intégral. Il y a une nette différence. Dans la recherche de texte intégral, la requête avec les résultats les plus élevés est affichée en premier. C'est le pouvoir de SearchRank. La combinaison SearchVectorde , SearchQueryet SearchRankest un moyen rapide de produire une recherche beaucoup plus puissante et précise que la recherche de base.

Ajout de poids

La recherche en texte intégral nous donne la possibilité d'ajouter plus d'importance à certains champs de notre table dans la base de données par rapport à d'autres champs. Nous pouvons y parvenir en ajoutant des poids à nos requêtes.

Le poids doit être l'une des lettres suivantes D, C, B, A. Par défaut, ces poids font référence aux nombres 0,1, 0,2, 0,4 et 1,0, respectivement.

Mise à jour SearchResultsList:

class SearchResultsList(ListView):
    model = Quote
    context_object_name = "quotes"
    template_name = "search.html"

    def get_queryset(self):
        query = self.request.GET.get("q")
        search_vector = SearchVector("name", weight="B") + SearchVector(
            "quote", weight="A"
        )
        search_query = SearchQuery(query)
        return (
            Quote.objects.annotate(rank=SearchRank(search_vector, search_query))
            .filter(rank__gte=0.3)
            .order_by("-rank")
        )

Ici, vous avez ajouté des pondérations à l' SearchVectoraide des champs nameet . quoteDes pondérations de 0,4 et 1,0 ont été appliquées respectivement aux champs de nom et de citation. Par conséquent, les correspondances entre guillemets prévaudront sur les correspondances avec le contenu du nom. Enfin, vous avez filtré les résultats pour n'afficher que ceux qui sont supérieurs à 0,3.

Ajout d'un aperçu aux résultats de la recherche

Dans cette section, vous allez ajouter un petit aperçu de votre résultat de recherche via la méthode SearchHeadline . Cela mettra en surbrillance la requête de résultat de recherche.

Mettre à jour SearchResultsListà nouveau :

class SearchResultsList(ListView):
    model = Quote
    context_object_name = "quotes"
    template_name = "search.html"

    def get_queryset(self):
        query = self.request.GET.get("q")
        search_vector = SearchVector("name", "quote")
        search_query = SearchQuery(query)
        search_headline = SearchHeadline("quote", search_query)
        return Quote.objects.annotate(
            search=search_vector,
            rank=SearchRank(search_vector, search_query)
        ).annotate(headline=search_headline).filter(search=search_query).order_by("-rank")

Les SearchHeadlineprises dans le champ que vous souhaitez prévisualiser. Dans ce cas, ce sera le quotechamp avec la requête, qui sera en gras.

Assurez-vous d'ajouter l'importation :

from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank, SearchHeadline

Avant d'essayer certaines recherches, mettez à jour le fichier <li></li>in quotes/templates/search.html comme ceci :

<li>{{ quote.headline | safe }} - <b>By <i>{{ quote.name }}</i></b></li>

Désormais, au lieu d'afficher les citations comme vous le faisiez auparavant, seul un aperçu du champ de citation complet s'affiche avec la requête de recherche en surbrillance.

Amélioration des performances

La recherche en texte intégral est un processus intensif. Pour lutter contre le ralentissement des performances, vous pouvez :

  1. Enregistrez les vecteurs de recherche dans la base de données avec SearchVectorField . En d'autres termes, plutôt que de convertir les chaînes en vecteurs de recherche à la volée, nous allons créer un champ de base de données séparé contenant les vecteurs de recherche traités et mettre à jour le champ chaque fois qu'il y a une insertion ou une mise à jour des champs quoteou .name
  2. Créez un index de base de données , qui est une structure de données qui améliore la vitesse des processus de récupération de données sur une base de données. Cela accélère donc la requête. Postgres vous donne plusieurs index avec lesquels travailler qui pourraient être applicables à différentes situations. Le GinIndex est sans doute le plus populaire.

Pour en savoir plus sur les performances avec la recherche en texte intégral, consultez la section Performances de la documentation Django.

Champ vectoriel de recherche

Commencez par ajouter un nouveau champ SearchVectorField au modèleQuote dans quotes/models.py :

from django.contrib.postgres.search import SearchVectorField  # new
from django.db import models


class Quote(models.Model):
    name = models.CharField(max_length=250)
    quote = models.TextField(max_length=1000)
    search_vector = SearchVectorField(null=True)  # new

    def __str__(self):
        return self.quote

Créez le fichier de migration :

$ docker-compose exec web python manage.py makemigrations

Désormais, vous ne pouvez remplir ce champ que lorsque les objets quoteou nameexistent déjà dans la base de données. Ainsi, nous devons ajouter un déclencheur pour mettre à jour le search_vectorchamp chaque fois que les champs quoteou namesont mis à jour. Pour cela, créez un fichier de migration personnalisé dans "quotes/migrations" appelé 0003_search_vector_trigger.py :

from django.contrib.postgres.search import SearchVector
from django.db import migrations


def compute_search_vector(apps, schema_editor):
    Quote = apps.get_model("quotes", "Quote")
    Quote.objects.update(search_vector=SearchVector("name", "quote"))


class Migration(migrations.Migration):

    dependencies = [
        ("quotes", "0002_quote_search_vector"),
    ]

    operations = [
        migrations.RunSQL(
            sql="""
            CREATE TRIGGER search_vector_trigger
            BEFORE INSERT OR UPDATE OF name, quote, search_vector
            ON quotes_quote
            FOR EACH ROW EXECUTE PROCEDURE
            tsvector_update_trigger(
                search_vector, 'pg_catalog.english', name, quote
            );
            UPDATE quotes_quote SET search_vector = NULL;
            """,
            reverse_sql="""
            DROP TRIGGER IF EXISTS search_vector_trigger
            ON quotes_quote;
            """,
        ),
        migrations.RunPython(
            compute_search_vector, reverse_code=migrations.RunPython.noop
        ),
    ]

Selon la structure de votre projet, vous devrez peut-être mettre à jour le nom du fichier de migration précédent dans dependencies.

Appliquez les migrations :

$ docker-compose exec web python manage.py migrate

Pour utiliser le nouveau champ pour les recherches, procédez à la mise SearchResultsListà jour :

class SearchResultsList(ListView):
    model = Quote
    context_object_name = "quotes"
    template_name = "search.html"

    def get_queryset(self):
        query = self.request.GET.get("q")
        return Quote.objects.filter(search_vector=query)

Mettez à jour à nouveau le fichier <li></li>in quotes/templates/search.html :

<li>{{ quote.quote | safe }} - <b>By <i>{{ quote.name }}</i></b></li>

Indice

Enfin, configurons un index fonctionnel, GinIndex .

Mettez à jour le Quotemodèle :

from django.contrib.postgres.indexes import GinIndex  # new
from django.contrib.postgres.search import SearchVectorField
from django.db import models


class Quote(models.Model):
    name = models.CharField(max_length=250)
    quote = models.TextField(max_length=1000)
    search_vector = SearchVectorField(null=True)

    def __str__(self):
        return self.quote

    # new
    class Meta:
        indexes = [
            GinIndex(fields=["search_vector"]),
        ]

Créez et appliquez les migrations une dernière fois :

$ docker-compose exec web python manage.py makemigrations
$ docker-compose exec web python manage.py migrate

Testez-le.

Conclusion

Dans ce didacticiel, vous avez été guidé dans l'ajout d'une recherche de base et de texte intégral à une application Django. Nous avons également examiné comment optimiser la fonctionnalité de recherche en texte intégral en ajoutant un champ vectoriel de recherche et un index de base de données.

Récupérez le code complet du référentiel django-search .

Source :  https://testdrive.io

#django #postgre 

Recherche De Base Et En Texte Intégral Avec Django Et Postgres
Neil  Morgan

Neil Morgan

1660498920

Pesquisa Básica E De Texto Completo Com Django E Postgres

Ao contrário dos bancos de dados relacionais, a pesquisa de texto completo não é padronizada. Existem várias opções de código aberto, como ElasticSearch, Solr e Xapian. O ElasticSearch é provavelmente a solução mais popular; no entanto, é complicado de configurar e manter. Além disso, se você não estiver aproveitando alguns dos recursos avançados que o ElasticSearch oferece, você deve ficar com os recursos de pesquisa de texto completo que muitos bancos de dados relacionais (como Postgres, MySQL, SQLite) e não relacionais (como MongoDB e CouchDB ) oferta. O Postgres, em particular, é adequado para pesquisa de texto completo. O Django também oferece suporte pronto para uso.

Para a grande maioria de seus aplicativos Django, você deve, no mínimo, começar aproveitando a pesquisa de texto completo do Postgres antes de procurar uma solução mais poderosa como ElasticSearch ou Solr.

Neste tutorial, você aprenderá como adicionar pesquisa básica e de texto completo a um aplicativo Django com o Postgres. Você também otimizará a pesquisa de texto completo adicionando um campo de vetor de pesquisa e um índice de banco de dados.

Objetivos

Ao final deste tutorial, você será capaz de:

  1. Configure a funcionalidade de pesquisa básica em um aplicativo Django com o módulo de objeto Q
  2. Adicionar pesquisa de texto completo a um aplicativo Django
  3. Classifique os resultados da pesquisa de texto completo por relevância usando técnicas de lematização, classificação e ponderação
  4. Adicione uma visualização aos seus resultados de pesquisa
  5. Otimize a pesquisa de texto completo com um campo de vetor de pesquisa e um índice de banco de dados

Configuração e visão geral do projeto

Clone o branch base do repositório django-search :

$ git clone https://github.com/testdrivenio/django-search --branch base --single-branch
$ cd django-search

Você usará o Docker para simplificar a configuração e execução do Postgres junto com o Django.

Na raiz do projeto, crie as imagens e ative os contêineres do Docker:

$ docker-compose up -d --build

Em seguida, aplique as migrações e crie um superusuário:

$ docker-compose exec web python manage.py makemigrations
$ docker-compose exec web python manage.py migrate
$ docker-compose exec web python manage.py createsuperuser

Depois de concluído, navegue até http://127.0.0.1:8011/quotes/ para garantir que o aplicativo funcione conforme o esperado. Você deve ver o seguinte:

Página inicial de cotação

Quer aprender a trabalhar com Django e Postgres? Confira o artigo Dockerizing Django com Postgres, Gunicorn e Nginx .

Anote o Quotemodelo em quotes/models.py :

from django.db import models

class Quote(models.Model):
    name = models.CharField(max_length=250)
    quote = models.TextField(max_length=1000)

    def __str__(self):
        return self.quote

Em seguida, execute o seguinte comando de gerenciamento para adicionar 10.000 cotações ao banco de dados:

$ docker-compose exec web python manage.py add_quotes

Isso levará alguns minutos. Uma vez feito, navegue até http://127.0.0.1:8011/quotes/ para ver os dados.

A saída da visualização é armazenada em cache por cinco minutos, portanto, você pode comentar @method_decoratorem quotes/views.py para carregar as aspas. Certifique-se de remover o comentário uma vez feito.

Página inicial de cotação

No arquivo quotes/templates/quote.html , você tem um formulário básico com um campo de entrada de pesquisa:

<form action="{% url 'search_results' %}" method="get">
  <input
    type="search"
    name="q"
    placeholder="Search by name or quote..."
    class="form-control"
  />
</form>

Ao enviar, o formulário envia os dados para o back-end. Uma GETsolicitação é usada em vez de uma POSTpara que tenhamos acesso à string de consulta tanto na URL quanto na visualização do Django, permitindo que os usuários compartilhem os resultados da pesquisa como links.

Antes de prosseguir, dê uma olhada rápida na estrutura do projeto e no restante do código.

Pesquisa básica

Quando se trata de pesquisa, com o Django, você normalmente começará realizando consultas de pesquisa com containsou icontainspara correspondências exatas. O objeto Q também pode ser usado para adicionar operadores lógicos AND ( &) ou OR ( ).|

Por exemplo, usando o operador OR, substitua o SearchResultsListpadrão de 's QuerySetem quotes/views.py assim:

class SearchResultsList(ListView):
    model = Quote
    context_object_name = "quotes"
    template_name = "search.html"

    def get_queryset(self):
        query = self.request.GET.get("q")
        return Quote.objects.filter(
            Q(name__icontains=query) | Q(quote__icontains=query)
        )

Aqui, usamos o método de filtro para filtrar os campos ou name. quoteAlém disso, também usamos a extensão icontains para verificar se a consulta está presente nos campos nameou quote(não diferencia maiúsculas de minúsculas). Um resultado positivo será retornado se uma correspondência for encontrada.

Não esqueça da importação:

from django.db.models import Q

Experimente:

Página de pesquisa

Para conjuntos de dados pequenos, essa é uma ótima maneira de adicionar funcionalidades básicas de pesquisa ao seu aplicativo. Se você estiver lidando com um grande conjunto de dados ou quiser uma funcionalidade de pesquisa que pareça um mecanismo de pesquisa da Internet, convém mudar para a pesquisa de texto completo.

Pesquisa de texto completo

A pesquisa básica que vimos anteriormente tem várias limitações, especialmente quando você deseja realizar pesquisas complexas.

Como mencionado, com a pesquisa básica, você só pode realizar correspondências exatas.

Outra limitação é a das palavras de parada . Palavras de parada são palavras como "a", "an" e "the". Essas palavras são comuns e pouco significativas, portanto, devem ser ignoradas. Para testar, tente pesquisar uma palavra com "the" na frente. Digamos que você pesquisou por "no meio". Nesse caso, você verá apenas resultados para "o meio", portanto não verá resultados que tenham a palavra "meio" sem "o" antes dela.

Digamos que você tenha essas duas frases:

  1. Eu estou no meio.
  2. Você não gosta do ensino médio.

Você obterá o seguinte retorno com cada tipo de pesquisa:

ConsultaPesquisa básicaPesquisa de texto completo
"o meio"primeiro1 e 2
"meio"1 e 21 e 2

Outra questão é a de ignorar palavras semelhantes. Com a pesquisa básica, apenas as correspondências exatas são retornadas. No entanto, com a pesquisa de texto completo, palavras semelhantes são contabilizadas. Para testar, tente encontrar algumas palavras semelhantes como "pônei" e "pôneis". Com a pesquisa básica, se você pesquisar por "pônei", não verá resultados que contenham "pôneis" - e vice-versa.

Digamos que você tenha essas duas frases.

  1. Eu sou um pônei.
  2. Você não gosta de pôneis

Você obterá o seguinte retorno com cada tipo de pesquisa:

ConsultaPesquisa básicaPesquisa de texto completo
"pónei"primeiro1 e 2
"pôneis"21 e 2

Com a pesquisa de texto completo, esses dois problemas são atenuados. No entanto, lembre-se de que, dependendo do seu objetivo, a pesquisa de texto completo pode diminuir a precisão (qualidade) e a recuperação (quantidade de resultados relevantes). Normalmente, a pesquisa de texto completo é menos precisa do que a pesquisa básica, pois a pesquisa básica produz correspondências exatas. Dito isso, se você estiver pesquisando em grandes conjuntos de dados com grandes blocos de texto, a pesquisa de texto completo é preferível, pois geralmente é muito mais rápida.

A pesquisa de texto completo é uma técnica de pesquisa avançada que examina todas as palavras em cada documento armazenado enquanto tenta corresponder aos critérios de pesquisa. Além disso, com a pesquisa de texto completo, você pode empregar derivação específica do idioma nas palavras que estão sendo indexadas. Por exemplo, as palavras "drives", "drive" e "driven" serão registradas sob a palavra de conceito único "drive". Stemming é o processo de reduzir palavras à sua raiz, base ou forma de raiz.

Basta dizer que a pesquisa de texto completo não é perfeita. É provável que recupere muitos documentos que não são relevantes (falsos positivos) para a consulta de pesquisa pretendida. No entanto, existem algumas técnicas baseadas em algoritmos Bayesianos que podem ajudar a reduzir tais problemas.

Para aproveitar a pesquisa de texto completo do Postgres com o Django, adicione django.contrib.postgresà sua INSTALLED_APPSlista:

INSTALLED_APPS = [
    ...

    "django.contrib.postgres",  # new
]

Em seguida, vamos ver dois exemplos rápidos de pesquisa de texto completo, em um único campo e em vários campos.

Pesquisa de campo único

Atualize a get_querysetfunção na função de SearchResultsListvisualização da seguinte forma:

class SearchResultsList(ListView):
    model = Quote
    context_object_name = "quotes"
    template_name = "search.html"

    def get_queryset(self):
        query = self.request.GET.get("q")
        return Quote.objects.filter(quote__search=query)

Aqui, configuramos a pesquisa de texto completo em um único campo -- o campo de cotação.

Página de pesquisa

Como você pode ver, leva em consideração palavras semelhantes. No exemplo acima, "pôneis" e "pônei" são tratados como palavras semelhantes.

Pesquisa de vários campos

Para pesquisar em vários campos e em modelos relacionados, você pode usar a SearchVectorclasse.

Novamente, atualize SearchResultsList:

class SearchResultsList(ListView):
    model = Quote
    context_object_name = "quotes"
    template_name = "search.html"

    def get_queryset(self):
        query = self.request.GET.get("q")
        return Quote.objects.annotate(search=SearchVector("name", "quote")).filter(
            search=query
        )

Para pesquisar em vários campos, você anota o conjunto de consultas usando um SearchVector. O vetor são os dados que você está procurando, que foram convertidos em um formulário fácil de pesquisar. No exemplo acima, esses dados são os campos namee em seu banco de dados.quote

Certifique-se de adicionar a importação:

from django.contrib.postgres.search import SearchVector

Tente algumas pesquisas.

Derivação e classificação

Nesta seção, você combinará vários métodos, como SearchVector , SearchQuery e SearchRank para produzir uma pesquisa muito robusta que usa lematização e classificação.

Novamente, stemming é o processo de reduzir palavras ao seu radical, base ou raiz. Com a derivação, palavras como "criança" e "crianças" serão tratadas como palavras semelhantes. O ranking, por outro lado, nos permite ordenar os resultados por relevância.

Atualização SearchResultsList:

class SearchResultsList(ListView):
    model = Quote
    context_object_name = "quotes"
    template_name = "search.html"

    def get_queryset(self):
        query = self.request.GET.get("q")
        search_vector = SearchVector("name", "quote")
        search_query = SearchQuery(query)
        return (
            Quote.objects.annotate(
                search=search_vector, rank=SearchRank(search_vector, search_query)
            )
            .filter(search=search_query)
            .order_by("-rank")
        )

O que está acontecendo aqui?

  1. SearchVector- novamente você usou um vetor de pesquisa para pesquisar em vários campos. Os dados são convertidos em outro formulário, pois você não está mais apenas pesquisando o texto bruto como fazia quando icontainsera usado. Portanto, com isso, você poderá pesquisar plurais facilmente. Por exemplo, pesquisar por "frasco" e "frascos" resultará na mesma pesquisa porque eles são basicamente a mesma coisa.
  2. SearchQuery- traduz as palavras fornecidas a nós como uma consulta do formulário, passa-as por um algoritmo de derivação e, em seguida, procura correspondências para todos os termos resultantes.
  3. SearchRank- nos permite ordenar os resultados por relevância. Ele leva em consideração a frequência com que os termos de consulta aparecem no documento, quão próximos os termos estão no documento e quão importante é a parte do documento onde eles ocorrem.

Adicione as importações:

from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank

Página de pesquisa

Compare os resultados da pesquisa básica com os da pesquisa de texto completo. Há uma clara diferença. Na pesquisa de texto completo, a consulta com os resultados mais altos é mostrada primeiro. Este é o poder de SearchRank. Combinar SearchVector, SearchQuery, e SearchRanké uma maneira rápida de produzir uma pesquisa muito mais poderosa e precisa do que a pesquisa básica.

Adicionando Pesos

A pesquisa de texto completo nos dá a capacidade de adicionar mais importância a alguns campos em nossa tabela no banco de dados em relação a outros campos. Podemos conseguir isso adicionando pesos às nossas consultas.

O peso deve ser uma das seguintes letras D, C, B, A. Por padrão, esses pesos referem-se aos números 0,1, 0,2, 0,4 e 1,0, respectivamente.

Atualização SearchResultsList:

class SearchResultsList(ListView):
    model = Quote
    context_object_name = "quotes"
    template_name = "search.html"

    def get_queryset(self):
        query = self.request.GET.get("q")
        search_vector = SearchVector("name", weight="B") + SearchVector(
            "quote", weight="A"
        )
        search_query = SearchQuery(query)
        return (
            Quote.objects.annotate(rank=SearchRank(search_vector, search_query))
            .filter(rank__gte=0.3)
            .order_by("-rank")
        )

Aqui, você adicionou pesos ao SearchVectorusando os campos namee . quotePesos de 0,4 e 1,0 foram aplicados aos campos de nome e cotação, respectivamente. Portanto, as correspondências de cotação prevalecerão sobre as correspondências de conteúdo de nome. Por fim, você filtrou os resultados para exibir apenas os maiores que 0,3.

Adicionando uma visualização aos resultados da pesquisa

Nesta seção, você adicionará uma pequena visualização do resultado da pesquisa por meio do método SearchHeadline . Isso destacará a consulta do resultado da pesquisa.

Atualize SearchResultsListnovamente:

class SearchResultsList(ListView):
    model = Quote
    context_object_name = "quotes"
    template_name = "search.html"

    def get_queryset(self):
        query = self.request.GET.get("q")
        search_vector = SearchVector("name", "quote")
        search_query = SearchQuery(query)
        search_headline = SearchHeadline("quote", search_query)
        return Quote.objects.annotate(
            search=search_vector,
            rank=SearchRank(search_vector, search_query)
        ).annotate(headline=search_headline).filter(search=search_query).order_by("-rank")

As SearchHeadlinetomadas no campo que você deseja visualizar. Nesse caso, este será o quotecampo junto com a consulta, que estará em negrito.

Certifique-se de adicionar a importação:

from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank, SearchHeadline

Antes de tentar algumas pesquisas, atualize o <li></li>in quotes/templates/search.html da seguinte forma:

<li>{{ quote.headline | safe }} - <b>By <i>{{ quote.name }}</i></b></li>

Agora, em vez de mostrar as cotações como você fez antes, apenas uma visualização do campo de cotação completo é exibida junto com a consulta de pesquisa destacada.

Melhorando o desempenho

A pesquisa de texto completo é um processo intensivo. Para combater o desempenho lento, você pode:

  1. Salve os vetores de pesquisa no banco de dados com SearchVectorField . Em outras palavras, em vez de converter as strings em vetores de pesquisa dinamicamente, criaremos um campo de banco de dados separado que contém os vetores de pesquisa processados ​​e atualizaremos o campo sempre que houver uma inserção ou atualização nos campos quoteou .name
  2. Crie um índice de banco de dados , que é uma estrutura de dados que aumenta a velocidade dos processos de recuperação de dados em um banco de dados. Portanto, agiliza a consulta. O Postgres oferece vários índices para trabalhar que podem ser aplicáveis ​​a diferentes situações. O GinIndex é sem dúvida o mais popular.

Para saber mais sobre desempenho com pesquisa de texto completo, revise a seção Desempenho dos documentos do Django.

Campo de vetor de pesquisa

Comece adicionando um novo campo SearchVectorField ao Quotemodelo em quotes/models.py :

from django.contrib.postgres.search import SearchVectorField  # new
from django.db import models


class Quote(models.Model):
    name = models.CharField(max_length=250)
    quote = models.TextField(max_length=1000)
    search_vector = SearchVectorField(null=True)  # new

    def __str__(self):
        return self.quote

Crie o arquivo de migração:

$ docker-compose exec web python manage.py makemigrations

Agora, você só poderá preencher este campo quando os objetos quoteou já existirem no banco de dados. nameAssim, precisamos adicionar um gatilho para atualizar o search_vectorcampo sempre que os campos quoteou nameforem atualizados. Para conseguir isso, crie um arquivo de migração personalizado em "quotes/migrations" chamado 0003_search_vector_trigger.py :

from django.contrib.postgres.search import SearchVector
from django.db import migrations


def compute_search_vector(apps, schema_editor):
    Quote = apps.get_model("quotes", "Quote")
    Quote.objects.update(search_vector=SearchVector("name", "quote"))


class Migration(migrations.Migration):

    dependencies = [
        ("quotes", "0002_quote_search_vector"),
    ]

    operations = [
        migrations.RunSQL(
            sql="""
            CREATE TRIGGER search_vector_trigger
            BEFORE INSERT OR UPDATE OF name, quote, search_vector
            ON quotes_quote
            FOR EACH ROW EXECUTE PROCEDURE
            tsvector_update_trigger(
                search_vector, 'pg_catalog.english', name, quote
            );
            UPDATE quotes_quote SET search_vector = NULL;
            """,
            reverse_sql="""
            DROP TRIGGER IF EXISTS search_vector_trigger
            ON quotes_quote;
            """,
        ),
        migrations.RunPython(
            compute_search_vector, reverse_code=migrations.RunPython.noop
        ),
    ]

Dependendo da estrutura do seu projeto, pode ser necessário atualizar o nome do arquivo de migração anterior no formato dependencies.

Aplique as migrações:

$ docker-compose exec web python manage.py migrate

Para usar o novo campo para pesquisas, atualize SearchResultsListassim:

class SearchResultsList(ListView):
    model = Quote
    context_object_name = "quotes"
    template_name = "search.html"

    def get_queryset(self):
        query = self.request.GET.get("q")
        return Quote.objects.filter(search_vector=query)

Atualize <li></li>entre aspas/templates/search.html novamente:

<li>{{ quote.quote | safe }} - <b>By <i>{{ quote.name }}</i></b></li>

Índice

Por fim, vamos configurar um índice funcional, GinIndex .

Atualize o Quotemodelo:

from django.contrib.postgres.indexes import GinIndex  # new
from django.contrib.postgres.search import SearchVectorField
from django.db import models


class Quote(models.Model):
    name = models.CharField(max_length=250)
    quote = models.TextField(max_length=1000)
    search_vector = SearchVectorField(null=True)

    def __str__(self):
        return self.quote

    # new
    class Meta:
        indexes = [
            GinIndex(fields=["search_vector"]),
        ]

Crie e aplique as migrações uma última vez:

$ docker-compose exec web python manage.py makemigrations
$ docker-compose exec web python manage.py migrate

Teste-o.

Conclusão

Neste tutorial, você foi guiado pela adição de pesquisa básica e de texto completo a um aplicativo Django. Também analisamos como otimizar a funcionalidade de pesquisa de texto completo adicionando um campo de vetor de pesquisa e um índice de banco de dados.

Pegue o código completo do repositório django-search .

Fonte:  https://testdrive.io

#django #postgre 

Pesquisa Básica E De Texto Completo Com Django E Postgres

Как использовать Vault для создания учетных данных Postgres для Flask

В этом руководстве мы рассмотрим быстрый реальный пример использования хранилища Hashicorp и Consul для создания динамических учетных данных Postgres для веб-приложения Flask.

Предпосылки

Перед началом у вас должно быть:

  1. Базовые навыки работы с секретами в Vault и Consul.
  2. Экземпляр Vault, развернутый с серверной частью хранилища .
  3. Развернут сервер Postgres.
  4. Раньше работал с Flask и Docker.

Начиная

Начнем с простого веб-приложения Flask.

Если вы хотите продолжить, скопируйте репозиторий vault-consul-flask , а затем проверьте ветку v1 :

$ git clone https://github.com/testdrivenio/vault-consul-flask --branch v1 --single-branch
$ cd vault-consul-flask

Взгляните на код:

├── .gitignore
├── Dockerfile
├── README.md
├── docker-compose.yml
├── manage.py
├── project
│   ├── __init__.py
│   ├── api
│   │   ├── __init__.py
│   │   ├── main.py
│   │   ├── models.py
│   │   └── users.py
│   └── config.py
└── requirements.txt

По сути, чтобы это приложение работало, нам нужно добавить следующие переменные среды в файл .env (что мы вскоре и сделаем):

  1. DB_USER
  2. DB_PASSWORD
  3. DB_SERVER

проект/config.py :

import os

USER = os.environ.get('DB_USER')
PASSWORD = os.environ.get('DB_PASSWORD')
SERVER = os.environ.get('DB_SERVER')


class ProductionConfig():
    """Production configuration"""
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SQLALCHEMY_DATABASE_URI = f'postgresql://{USER}:{PASSWORD}@{SERVER}:5432/users_db'

Настройка хранилища

Опять же, если вы хотите продолжить, у вас должен быть развернут экземпляр Vault с серверной частью хранилища . Этот экземпляр также должен быть инициализирован и распечатан. Хотите быстро запустить кластер? Запустите скрипт deploy.sh из vault-consul-swarm , чтобы развернуть кластер Vault и Consul в трех дроплетах DigitalOcean. Подготовка и развертывание займет менее пяти минут!

Сначала войдите в Vault (при необходимости), а затем включите серверную часть секретов базы данных из интерфейса командной строки Vault :

$ vault secrets enable database

Success! Enabled the database secrets engine at: database/

Добавьте соединение Postgres вместе с информацией о подключаемом модуле базы данных :

$ vault write database/config/users_db \
    plugin_name="postgresql-database-plugin" \
    connection_url="postgresql://{{username}}:{{password}}@<ENDPOINT>:5432/users_db" \
    allowed_roles="mynewrole" \
    username="<USERNAME>" \
    password="<PASSWORD>"

Вы заметили, что в URL есть шаблоны для usernameи passwordв нем? Это используется для предотвращения прямого доступа для чтения к паролю и включения ротации учетных данных.

Обязательно обновите конечную точку базы данных, а также имя пользователя и пароль. Например:

$ vault write database/config/users_db \
    plugin_name="postgresql-database-plugin" \
    connection_url="postgresql://{{username}}:{{password}}@users-db.c7vzuyfvhlgz.us-east-1.rds.amazonaws.com:5432/users_db" \
    allowed_roles="mynewrole" \
    username="vault" \
    password="lOfon7BA3uzZzxGGv36j"

Это создало новый путь секретов в «database/config/users_db»:

$ vault list database/config

Keys
----
users_db

Затем создайте новую роль с именем mynewrole:

$ vault write database/roles/mynewrole \
    db_name=users_db \
    creation_statements="CREATE ROLE \"{{name}}\" \
        WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
        GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
    default_ttl="1h" \
    max_ttl="24h"

Success! Data written to: database/roles/mynewrole

Здесь мы сопоставили mynewroleимя в Vault с оператором SQL, который при запуске создаст нового пользователя со всеми разрешениями в базе данных. Имейте в виду, что это еще не создало нового пользователя. Обратите внимание на значение по умолчанию и максимальное значение TTL.

Теперь мы готовы создавать новых пользователей.

Создание учетных данных

Взгляните на то, какие пользователи у вас есть psql:

$ \du

Создайте новый файл с именем run.sh в корне проекта:

#!/bin/sh

rm -f .env

echo DB_SERVER=<DB_ENDPOINT> >> .env

user=$(curl  -H "X-Vault-Token: $VAULT_TOKEN" \
        -X GET http://<VAULT_ENDPOINT>:8200/v1/database/creds/mynewrole)
echo DB_USER=$(echo $user | jq -r .data.username) >> .env
echo DB_PASSWORD=$(echo $user | jq -r .data.password) >> .env

docker-compose up -d --build

Таким образом, это вызовет Vault API для создания нового набора учетных данных из /credsконечной точки. Последующий ответ анализируется с помощью JQ, и учетные данные добавляются в файл .env . Обязательно обновите конечные точки базы данных ( DB_ENDPOINT) и Vault ( VAULT_ENDPOINT).

Добавьте VAULT_TOKENпеременную среды:

$ export VAULT_TOKEN=<YOUR_VAULT_TOKEN>

Создайте образ и запустите контейнер:

$ sh run.sh

Убедитесь, что переменные среды были успешно добавлены:

$ docker-compose exec web env

Вы также должны увидеть этого пользователя в базе данных:

Role name                                   | Attributes                                  | Member of
--------------------------------------------+---------------------------------------------+----------
 v-root-mynewrol-jC8Imdx2sMTZj03-1533704364 | Password valid until 2018-08-08 05:59:29+00 | {}

usersСоздайте и заполните таблицу базы данных :

$ docker-compose run web python manage.py recreate-db
$ docker-compose run web python manage.py seed-db

Проверьте это в браузере по адресу http://localhost:5000/users :

{
  "status": "success",
  "users": [{
    "active": true,
    "admin": false,
    "email": "michael@notreal.com",
    "id": 1,
    "username": "michael"
  }]
}

Снесите контейнеры, как только закончите:

$ docker-compose down

Вывод

Вот и все!

Помните, что в этом примере учетные данные действительны только в течение часа. Это идеально подходит для коротких, динамичных, разовых задач. Если у вас есть более длинные задачи, вы можете настроить задание cron для запуска сценария run.sh каждый час для получения новых учетных данных. Просто имейте в виду, что максимальный TTL установлен на 24 часа.

Вы также можете рассмотреть возможность использования envconsul для размещения учетных данных в среде для Flask. Он может даже перезапустить Flask при обновлении учетных данных.

Вы можете найти окончательный код в репозитории vault-consul-flask .

Источник:  https://testdriven.io

#flask #vault #postgre 

Как использовать Vault для создания учетных данных Postgres для Flask
Minh  Nguyet

Minh Nguyet

1657301820

Cách Xây Dựng ứng Dụng Fullstack Với Next.js, Prisma, Postgres

Trong bài viết này, chúng ta sẽ tìm hiểu cách xây dựng ứng dụng Full-stack bằng Next.js, Prisma, Postgres và Fastify. Chúng tôi sẽ xây dựng một ứng dụng demo quản lý chấm công để quản lý việc chấm công của nhân viên. Quy trình của ứng dụng rất đơn giản: người dùng quản trị đăng nhập, tạo bảng chấm công trong ngày, sau đó mọi nhân viên đăng nhập và đăng xuất khỏi bảng chấm công.

Next.js là gì?

Next.js là một khung công tác React linh hoạt cung cấp cho bạn các khối xây dựng để tạo các ứng dụng web nhanh chóng. Nó thường được gọi là fullstack React framework vì nó cho phép có cả ứng dụng frontend và backend trên cùng một codebase hoạt động như vậy với các chức năng serverless.

Prisma là gì?

Prisma là một ORM mã nguồn mở, Node.js và Typescript giúp đơn giản hóa đáng kể mô hình dữ liệu, di chuyển và truy cập dữ liệu cho cơ sở dữ liệu SQL. Tại thời điểm viết bài này, Prisma hỗ trợ các hệ quản trị cơ sở dữ liệu sau: PostgreSQL, MySQL, MariaDB, SQLite, AWS Aurora, Microsoft SQL Server, Azure SQL và MongoDB. Bạn cũng có thể muốn nhấp vào đây để xem danh sách tất cả các hệ thống quản lý cơ sở dữ liệu được hỗ trợ.

Postgres là gì?

Postgres còn được gọi là PostgreSQL và nó là một hệ quản trị cơ sở dữ liệu quan hệ mã nguồn mở và miễn phí. Nó là một tập hợp siêu ngôn ngữ SQL và nó có nhiều tính năng cho phép các nhà phát triển lưu trữ và chia tỷ lệ khối lượng công việc dữ liệu phức tạp một cách an toàn.

Điều kiện tiên quyết

Hướng dẫn này là một hướng dẫn trình diễn thực hành. Do đó, tốt nhất là bạn đã cài đặt những thứ sau trên máy tính của mình để làm theo:

Mã cho hướng dẫn này có sẵn ở đây trên Github, vì vậy hãy sao chép nó và làm theo.

Thiết lập dự án

Hãy bắt đầu bằng cách thiết lập ứng dụng Next.js của chúng tôi. Để bắt đầu, hãy chạy lệnh bên dưới.

npx create-next-app@latest

Chờ cài đặt hoàn tất, sau đó chạy lệnh bên dưới để cài đặt các phụ thuộc của chúng tôi.

yarn add fastify fastify-nextjs iron-session @prisma/client
yarn add prisma nodemon --dev

Chờ cho quá trình cài đặt hoàn tất.

Thiết lập Next.js và Fastify

Theo mặc định, Next.js không sử dụng Fastify làm máy chủ của nó. Để sử dụng Fastify để phân phối ứng dụng Next.js của chúng tôi, hãy chỉnh sửa trường script trong package.jsontệp bằng đoạn mã bên dưới.

"scripts": {
"dev": "nodemon server.js",
"build": "next build",
"start": "next start",
"lint": "next lint"
}

Tạo máy chủ Fastify của chúng tôi

Bây giờ chúng ta hãy tạo một server.jstệp. Tệp này là điểm nhập của ứng dụng của chúng tôi và sau đó chúng tôi thêm tệp require('fastify-nextjs')để bao gồm plugin hiển thị API Next.js trong fastify để xử lý kết xuất.

Mở server.jstệp và thêm các đoạn mã bên dưới:

const fastify = require('fastify')()
async function noOpParser(req, payload) {
return payload;
}
fastify.register(require('fastify-nextjs')).after(() => {
fastify.addContentTypeParser('text/plain', noOpParser);
fastify.addContentTypeParser('application/json', noOpParser);
fastify.next('/*')
fastify.next('/api/*', { method: 'ALL' });
})
fastify.listen(3000, err => {
if (err) throw err
console.log('Server listening on <http://localhost:3000>')
})

Trong đoạn mã trên, chúng tôi sử dụng fastify-nextjsplugin hiển thị API Next.js trong Fastify để xử lý kết xuất cho chúng tôi. Sau đó, chúng tôi phân tích cú pháp các yêu cầu đến bằng noOpParserhàm làm cho phần thân yêu cầu có sẵn cho các trình xử lý tuyến API Next.js của chúng tôi và chúng tôi xác định hai tuyến cho ứng dụng của mình bằng [fastify.next](<http://fastify.next>lệnh. Sau đó, chúng tôi tạo máy chủ Fastify của mình và làm cho nó lắng nghe cổng 3000.

Bây giờ, hãy tiếp tục và chạy ứng dụng bằng yarn devlệnh: ứng dụng sẽ được chạy localhost:3000.

Thiết lập Prisma

Đầu tiên, hãy chạy lệnh sau để thiết lập Prisma cơ bản:

npx prisma init

Lệnh trên sẽ tạo một thư mục prima với một schema.prismatập tin. Đây là tệp cấu hình Prisma chính của bạn, tệp này sẽ chứa lược đồ cơ sở dữ liệu của bạn. Ngoài ra, một .envtệp sẽ được thêm vào thư mục gốc của dự án. Mở .envtệp và thay thế URL kết nối giả bằng URL kết nối của cơ sở dữ liệu PostgreSQL của bạn.

Thay thế mã trong prisma/schema.prismatệp bằng mã sau:

datasource db {
  url      = env("DATABASE_URL")
  provider = "postgresql"
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  email     String   @unique
  name      String
  password  String
  role      Role     @default(EMPLOYEE)
  attendance     Attendance[]
  AttendanceSheet AttendanceSheet[]
}

model AttendanceSheet {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  createdBy    User?    @relation(fields: [userId], references: [id])
  userId  Int?
}

model Attendance {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  signIn    Boolean @default(true)
  signOut   Boolean
  signInTime    DateTime @default(now())
  signOutTime   DateTime 
  user    User?    @relation(fields: [userId], references: [id])
  userId  Int?
}

enum Role {
  EMPLOYEE
  ADMIN
}

Trong đoạn mã trên, chúng tôi đã tạo Người dùng, Bảng điểm và Mô hình chấm công, xác định mối quan hệ giữa mỗi mô hình.

Tiếp theo, tạo các bảng này trong cơ sở dữ liệu. Chạy lệnh sau:

npx prisma db push

Sau khi chạy lệnh trên, bạn sẽ thấy đầu ra như được hiển thị trong ảnh chụp màn hình bên dưới trong thiết bị đầu cuối của bạn:

Tạo các chức năng tiện ích

Sau khi thiết lập xong Prisma, chúng ta hãy tạo ba chức năng tiện ích sẽ được sử dụng trong ứng dụng của chúng tôi theo thời gian.

Mở tệp lib / parseBody.js và thêm đoạn mã sau. Hàm này phân tích cú pháp phần thân yêu cầu thành JSON:

export const parseBody = (body) => {
if (typeof body === "string") return JSON.parse(body)
return body
}

Mở tệp lib / request.js và thêm đoạn mã sau. Chức năng này gửi một yêu cầu ĐĂNG.

export async function postData(url = '', data='') {
const response = await fetch(url, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify(data)
});
return response.json();
}

Mở tệp /lib/request.js và thêm đoạn mã sau. Hàm này trả về một đối tượng thuộc tính phiên cho phiên sắt-session sắt.

export const sessionCookie = () => {
return ({
cookieName: "auth",
password: process.env.SESSION_PASSWORD,
// secure: true should be used in production (HTTPS) but can't be used in development (HTTP)
cookieOptions: {
secure: process.env.NODE_ENV === "production",
},
})
}

Tiếp theo, thêm SESSION_PASSWORDvào tệp .env: nó phải là một chuỗi có ít nhất 32 ký tự.

Tạo kiểu cho ứng dụng

Với các chức năng Tiện ích của chúng tôi đã hoàn thành, hãy thêm một số kiểu vào ứng dụng. Chúng tôi đang sử dụng mô-đun css cho ứng dụng này, vì vậy hãy mở styles/Home.modules.csstệp và thêm đoạn mã bên dưới:

.container {
  padding: 0 2rem;
}

.main {
  min-height: 100vh;
  padding: 4rem 0;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.login {
  width: 450px;
}

.login input {
  width: 100%;
  height: 50px;
  margin: 4px;
}

.login button {
  width: 100%;
  height: 50px;
  margin: 4px;
}

.dashboard {
  display: grid;
  grid-template-columns: 3fr 9fr;
  grid-template-rows: 1fr;
  grid-column-gap: 0px;
  grid-row-gap: 0px;
  height: calc(100vh - 60px);
}

.navbar {
  height: 60px;
  background-color: black;
}

Tạo thành phần thanh bên

Với việc tạo kiểu xong, hãy tạo thành phần thanh bên để giúp chúng tôi điều hướng đến các trang khác nhau trên trang tổng quan ứng dụng của chúng tôi. Mở tệp thành phần / SideBar.js và dán đoạn mã bên dưới.

import Link from 'next/link'
import { useRouter } from 'next/router'
import styles from '../styles/SideBar.module.css'

const SideBar = () => {

    const router = useRouter()

    const logout = async () => {

        try {

            const response = await fetch('/api/logout', {
                method: 'GET', 
                credentials: 'same-origin', 
            });

            if(response.status === 200)  router.push('/')

        } catch (e) {
            alert(e)
        }
  
    }
      

    return (
        <nav className={styles.sidebar}>

            <ul>

                <li> <Link href="/dashboard"> Dashboard</Link> </li>

                <li> <Link href="/dashboard/attendance"> Attendance </Link> </li>

                <li> <Link href="/dashboard/attendance-sheet"> Attendance Sheet </Link> </li>

                <li onClick={logout}> Logout </li>

            </ul>

        </nav>
    )

}

export default SideBar

Trang đăng nhập

Bây giờ, hãy mở tệp page / index.js, xóa tất cả mã ở đó và thêm đoạn mã sau. Đoạn mã dưới đây sẽ gửi một yêu cầu đăng bài với email và mật khẩu được cung cấp qua biểu mẫu tới localhost: 3000 / api / đường đăng nhập. Khi thông tin xác thực được xác thực, nó sẽ gọi router.push('/dashboard')phương thức chuyển hướng người dùng đến localhost: 3000 / api / dashboard:

import Head from 'next/head'
import { postData } from '../lib/request';
import styles from '../styles/Home.module.css'
import { useState } from 'react';
import { useRouter } from 'next/router'

export default function Home({posts}) {

  const [data, setData] = useState({email: null, password: null});

  const router = useRouter()

  const submit = (e) => {
    e.preventDefault()

    if(data.email && data.password) {
      postData('/api/login', data).then(data => {
        console.log(data); 

        if (data.status === "success") router.push('/dashboard')
      
      });
    }

  }

  return (
    <div className={styles.container}>
      <Head>
        <title>Login</title>
        <meta name="description" content="Login" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>

        <form  className={styles.login}>

          <input 
            type={"text"} 
            placeholder="Enter Your Email" 
            onChange={(e) => setData({...data, email: e.target.value})} />

          <input 
            type={"password"}  
            placeholder="Enter Your Password"
            onChange={(e) => setData({...data, password: e.target.value})} />

          <button onClick={submit}>Login</button>

        </form>
        
      </main>

    </div>
  )
}

Thiết lập tuyến API đăng nhập

Bây giờ, hãy mở tệp trang / api / login.js và thêm đoạn mã sau. Chúng tôi sẽ sử dụng PrismaClientđể thực hiện các truy vấn cơ sở dữ liệu của mình và withIronSessionApiRoutelà hàm iron-session để xử lý các phiên của người dùng trong các ứng dụng RESTful.

Lộ trình này xử lý yêu cầu POST đăng nhập đối với localhost: 3000 / api / login và tạo cookie xác thực sau khi người dùng được xác thực.

import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { parseBody } from '../../lib/parseBody';
import { sessionCookie } from '../../lib/session';

export default withIronSessionApiRoute(
    async function loginRoute(req, res) {

      const { email, password } = parseBody(req.body)

      const prisma = new PrismaClient()

      // By unique identifier
      const user = await prisma.user.findUnique({
        where: {
        email
      },})

      if(user.password === password) {

        // get user from database then:
        user.password = undefined
        req.session.user = user
        await req.session.save();

        return res.send({ status: 'success', data: user });

      };

    res.send({ status: 'error', message: "incorrect email or password" });

  },
  sessionCookie(),
);

Thiết lập tuyến API đăng xuất

Mở tệp / page / api / logout và thêm đoạn mã bên dưới. Tuyến này xử lý các yêu cầu GET tới localhost: 3000 / api / logout đăng xuất người dùng bằng cách hủy cookie phiên.

import { withIronSessionApiRoute } from "iron-session/next";
import { sessionCookie } from "../../lib/session";

export default withIronSessionApiRoute(
  function logoutRoute(req, res, session) {
    req.session.destroy();
    res.send({ status: "success" });
  },
  sessionCookie()
);

Tạo Trang Bảng điều khiển

Trang này cung cấp giao diện để người dùng đăng nhập và đăng xuất khỏi bảng điểm danh. Quản trị viên cũng có thể tạo một bảng điểm danh. Mở tệp trang / dashboard / index.js và thêm đoạn mã bên dưới.

import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { useState, useCallback } from "react";
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";
import { postData } from "../../lib/request";

export default function Page(props) {

  const [attendanceSheet, setState] = useState(JSON.parse(props.attendanceSheet));

  const sign = useCallback((action="") => {

    const body = {
      attendanceSheetId: attendanceSheet[0]?.id,
      action
    }

    postData("/api/sign-attendance", body).then(data => {

      if (data.status === "success") {

        setState(prevState => {

          const newState = [...prevState]

          newState[0].attendance[0] = data.data

          return newState

        })
     
      }

    })

  }, [attendanceSheet])

  const createAttendance = useCallback(() => {

    postData("/api/create-attendance").then(data => {

      if (data.status === "success") {
        alert("New Attendance Sheet Created")
        setState([{...data.data, attendance:[]}])
      }

    })

  }, [])

  return (
    <div>

      <Head>
        <title>Attendance Management Dashboard</title>
        <meta name="description" content="dashboard" />
      </Head>

      <div className={styles.navbar}></div>

      <main className={styles.dashboard}>

        <SideBar />

        <div className={dashboard.users}>

          {
            props.isAdmin && <button className={dashboard.create} onClick={createAttendance}>Create Attendance Sheet</button>
          }
            
          { attendanceSheet.length > 0 &&

            <table className={dashboard.table}>
              <thead>
                <tr> 
                  <th>Id</th> <th>Created At</th> <th>Sign In</th> <th>Sign Out</th> 
                </tr>
              </thead>

              <tbody>
                <tr>
                  <td>{attendanceSheet[0]?.id}</td>
                  <td>{attendanceSheet[0]?.createdAt}</td>

                  {
                    attendanceSheet[0]?.attendance.length != 0 ? 
                      <>
                        <td>{attendanceSheet[0]?.attendance[0]?.signInTime}</td>
                        <td>{
                          attendanceSheet[0]?.attendance[0]?.signOut ? 
                          attendanceSheet[0]?.attendance[0]?.signOutTime: <button onClick={() => sign("sign-out")}> Sign Out </button> }</td>
                      </>
                      :
                      <>
                        <td> <button onClick={() => sign()}> Sign In </button> </td>
                        <td>{""}</td>
                      </>
                  }
                </tr>
              </tbody>

            </table>

          }
          
        </div>

      </main>

    </div>
  )
}

Chúng tôi sử dụng getServerSidePropsđể tạo dữ liệu trang và withIronSessionSsrlà hàm phiên sắt để làm việc với các trang được hiển thị phía máy chủ. Trong đoạn mã sau, chúng tôi truy vấn hàng cuối cùng của bảng tham dự với một hàng từ bảng tham dự, trong đó giá trị userIdbằng với Id người dùng được lưu trữ trên phiên người dùng. Chúng tôi cũng kiểm tra xem người dùng có phải là QUẢN TRỊ VIÊN không.

export const getServerSideProps = withIronSessionSsr( async ({req}) => {

  const user = req.session.user

  const prisma = new PrismaClient()

  const attendanceSheet = await prisma.attendanceSheet.findMany({  
    take: 1,
    orderBy: {
      id: 'desc',
    },
    include: { 
      attendance: {
        where: {
          userId: user.id
        },
      }
    }
  })

  return {
    props: {
      attendanceSheet: JSON.stringify(attendanceSheet),
      isAdmin: user.role === "ADMIN"
    }
  }

}, sessionCookie())

Thiết lập lộ trình tạo API chuyên cần

Mở tệp page / api / create-secure.js và thêm đoạn mã bên dưới.

import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionCookie } from '../../lib/session';

  
export default withIronSessionApiRoute( async function handler(req, res) {

    const prisma = new PrismaClient()

    const user = req.session.user

    const attendanceSheet = await prisma.attendanceSheet.create({
        data: {
          userId: user.id,
        },
    })

    res.json({status: "success", data: attendanceSheet});
    
}, sessionCookie())

Thiết lập lộ trình API chấm công đăng ký

Tuyến đường này xử lý yêu cầu API POST của chúng tôi tới localhost: 3000 / api / sign -casting. Tuyến chấp nhận yêu cầu POST, trong khi attendanceSheetIdactionđược sử dụng để đăng nhập và đăng xuất attendanceSheet.

Mở tệp /page/api/sign-attendance.js và thêm đoạn mã bên dưới.

import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { parseBody } from '../../lib/parseBody';
import { sessionCookie } from '../../lib/session';

  
export default withIronSessionApiRoute( async function handler(req, res) {

    const prisma = new PrismaClient()

    const {attendanceSheetId, action} = parseBody(req.body)

    const user = req.session.user

    const attendance = await prisma.attendance.findMany({
        where: {
            userId: user.id,
            attendanceSheetId: attendanceSheetId
        }
    })

    //check if atendance have been created
    if (attendance.length === 0) {
        const attendance = await prisma.attendance.create({
            data: {
                userId: user.id,
                attendanceSheetId: attendanceSheetId,
                signIn: true,
                signOut: false,
                signOutTime: new Date()
            },
        })   

        return res.json({status: "success", data: attendance});

    } else if (action === "sign-out") {
        await prisma.attendance.updateMany({
            where: {
                userId: user.id,
                attendanceSheetId: attendanceSheetId
            },
            data: {
              signOut: true,
              signOutTime: new Date()
            },
        })

        return res.json({status: "success", data: { ...attendance[0], signOut: true, signOutTime: new Date()}});
    }

    res.json({status: "success", data: attendance});
    
}, sessionCookie())

Tạo trang điểm danh

Trang kết xuất phía máy chủ này hiển thị tất cả các trang tham dự cho một người dùng đã đăng nhập. Mở tệp /page/dashboard/attendance.js và thêm đoạn mã bên dưới.

import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";

export default function Page(props) {

  const data = JSON.parse(props.attendanceSheet)

  return (
    <div>

      <Head>
        <title>Attendance Management Dashboard</title>
        <meta name="description" content="dashboard" />
      </Head>

      <div className={styles.navbar}></div>

      <main className={styles.dashboard}>

        <SideBar />

        <div className={dashboard.users}>

        <table className={dashboard.table}>

          <thead>

            <tr> 
              <th> Attendance Id</th> <th>Date</th> 
              <th>Sign In Time</th> <th>Sign Out Time</th> 
            </tr> 

          </thead>

            <tbody>

              {
                data.map(data =>   {

                  const {id, createdAt, attendance } = data

  
                  return (
                    <tr key={id}> 

                      <td>{id}</td> <td>{createdAt}</td>  

                      { attendance.length === 0 ? 
                      
                        (
                          <>
                            <td>You did not Sign In</td>
                            <td>You did not Sign Out</td>
                          </>
                        )
                        :
                        (
                          <>
                            <td>{attendance[0]?.signInTime}</td>
                            <td>{attendance[0]?.signOut ? attendance[0]?.signOutTime : "You did not Sign Out"}</td>
                          </>
                        )
                        
                      }
              
                    </tr>
                  )

                })

              }  

            </tbody>

          </table>

        </div>

      </main>

    </div>
  )
}

Trong đoạn mã bên dưới, chúng tôi truy vấn tất cả các hàng từ attendanceSheetbảng và cũng tìm nạp lượt tham dự userIdbằng với id người dùng được lưu trữ trong phiên người dùng.

export const getServerSideProps = withIronSessionSsr( async ({req}) => {

  const user = req.session.user

  const prisma = new PrismaClient()
  
  const attendanceSheet = await prisma.attendanceSheet.findMany({
    orderBy: {
      id: 'desc',
    },
    include: { 
      attendance: {
        where: {
          userId: user.id
        },
      }
    }
  })

  return {
    props: {
      attendanceSheet: JSON.stringify(attendanceSheet),
    }
  }

}, sessionCookie())

Tạo trang bảng điểm danh

Trang kết xuất phía máy chủ này hiển thị tất cả các phiếu chấm công và Nhân viên đã đăng nhập vào bảng chấm công đó. Mở tệp /page/dashboard/attendance.js và thêm đoạn mã bên dưới.

import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";

export default function Page(props) {

  const data = JSON.parse(props.attendanceSheet)

  return (
    <div>

      <Head>
        <title>Attendance Management Dashboard</title>
        <meta name="description" content="dashboard" />
      </Head>

      <div className={styles.navbar}></div>

      <main className={styles.dashboard}>

        <SideBar />

        <div className={dashboard.users}>

        {
          data?.map(data => {

            const {id, createdAt, attendance } = data

            return (
              <>

                <table key={data.id} className={dashboard.table}>

                  <thead>
                    
                    <tr> 
                      <th> Attendance Id</th> <th>Date</th> 
                      <th> Name </th> <th> Email </th> <th> Role </th>
                      <th>Sign In Time</th> <th>Sign Out Time</th> 
                    </tr> 

                  </thead>

                  <tbody>

                    {
                      (attendance.length === 0)  &&
                      (
                        <>
                        <tr><td> {id} </td> <td>{createdAt}</td> <td colSpan={5}> No User signed this sheet</td></tr>
                        </>
                      )
                    }

                    {
                      attendance.map(data => {

                        const {name, email, role} = data.user

                      
                        return (
                          <tr key={id}> 

                            <td>{id}</td> <td>{createdAt}</td>  

                            <td>{name}</td> <td>{email}</td>

                            <td>{role}</td>

                            <td>{data.signInTime}</td>

                            <td>{data.signOut ? attendance[0]?.signOutTime: "User did not Sign Out"}</td>  
                    
                          </tr>
                        )

                      })

                    }  

                  </tbody>
                  
                </table>
              </>
            )
          })
          
          }

        </div>

      </main>

    </div>
  )
}

Trong đoạn mã bên dưới, chúng tôi truy vấn tất cả các hàng từ các attendanceSheetbảng và cũng tìm nạp thông tin tham dự bằng cách chọn tên, email và vai trò.

export const getServerSideProps = withIronSessionSsr(async () => {

  const prisma = new PrismaClient()

  const attendanceSheet = await prisma.attendanceSheet.findMany({
    orderBy: {
      id: 'desc',
    },
    include: { 
      attendance: {
        include: { 
          user: {
            select: {
              name: true, 
              email: true, 
              role: true
            }
          }
        }
      },
    },

  })

  return {
    props: {
      attendanceSheet: JSON.stringify(attendanceSheet),
    }
  }

}, sessionCookie())

Thử nghiệm ứng dụng

Đầu tiên, chúng ta phải thêm Người dùng vào cơ sở dữ liệu của mình. Chúng tôi sẽ làm điều này với Prisma Studio. Để khởi động Prisma studio, hãy chạy lệnh dưới đây:

npx prisma studio

Trang chỉ mục Prisma trông như thế này:

Trang chỉ mục Prisma


Để tạo người dùng cơ sở dữ liệu có vai trò QUẢN TRỊ và nhiều người dùng có vai trò NHÂN VIÊN, hãy truy cập trang này:

Thêm người dùng

Nhấp vào Thêm bản ghi, sau đó điền vào các trường bắt buộc: mật khẩu, tên, email và vai trò. Khi bạn đã hoàn tất, hãy nhấp vào nút Lưu 1 thay đổi màu xanh lục. Lưu ý rằng để đơn giản, chúng tôi đã không băm mật khẩu.

Khởi động máy chủ với yarn dev. Thao tác này khởi động máy chủ và chạy ứng dụng trên [localhost: 3000] (<http: // localhost: 3000>), trang đăng nhập được hiển thị bên dưới.

Trang đăng nhập

Đăng nhập bằng người dùng có vai trò QUẢN TRỊ vì chỉ người dùng quản trị mới có thể tạo bảng điểm danh. Sau khi đăng nhập thành công, ứng dụng sẽ chuyển hướng bạn đến trang tổng quan của bạn.

Bấm vào nút Tạo Bảng điểm danh để tạo bảng điểm danh, sau đó đợi yêu cầu kết thúc thì bảng điểm danh sẽ hiện ra. Bảng điều khiển người dùng được hiển thị bên dưới.

Tạo Bảng điểm danh

Bảng điểm danh được hiển thị bên dưới, nhấp vào nút Sign In để đăng nhập. Sau khi đăng nhập thành công, thời gian đăng nhập sẽ được hiển thị và nút Sign Out sẽ hiển thị. Nhấp vào nút Sign Out để đăng xuất và lặp lại quá trình này nhiều lần với những người dùng khác nhau.

Đăng nhập thành công

Tiếp theo, nhấp vào liên kết Điểm danh trong thanh bên để xem điểm danh của người dùng. Các kết quả phải khớp với những kết quả được hiển thị bên dưới:

Trang điểm danh

Tiếp theo nhấp vào liên kết Bảng điểm danh trên thanh bên để xem người dùng tham dự tất cả người dùng. Kết quả được hiển thị bên dưới:

Trang bảng điểm danh

Sự kết luận

Trong bài viết này, bạn đã học cách sử dụng máy chủ Fastify tùy chỉnh với Next.js. Bạn cũng đã tìm hiểu về Prisma và Prisma studio. Tôi đã hướng dẫn bạn cách kết nối Prisma với cơ sở dữ liệu Postgres và cách tạo, đọc và cập nhật cơ sở dữ liệu bằng ứng dụng khách Prisma và studio Prisma.

Liên kết: https://arctype.com/blog/fullstack-nextjs-postgres-fastify/

#fullstack #nextjs #postgre #fastify

Cách Xây Dựng ứng Dụng Fullstack Với Next.js, Prisma, Postgres

Как создать полнофункциональное приложение с помощью Next.js, Prisma

В этой статье мы узнаем, как создать полнофункциональное приложение с использованием Next.js, Prisma, Postgres и Fastify. Мы создадим демонстрационное приложение для управления посещаемостью, которое будет управлять посещаемостью сотрудников. Процесс работы приложения прост: пользователь с правами администратора входит в систему, создает лист посещаемости на день, затем каждый сотрудник входит и выходит из листа посещаемости.

Что такое Next.js?

Next.js — это гибкая среда React, которая дает вам строительные блоки для создания быстрых веб-приложений. Его часто называют фреймворком React с полным стеком, поскольку он позволяет использовать как интерфейсные, так и серверные приложения в одной кодовой базе, используя бессерверные функции.

Что такое Призма?

Prisma — это ORM с открытым исходным кодом, Node.js и Typescript, который значительно упрощает моделирование данных, миграцию и доступ к данным для баз данных SQL. На момент написания этой статьи Prisma поддерживает следующие системы управления базами данных: PostgreSQL, MySQL, MariaDB, SQLite, AWS Aurora, Microsoft SQL Server, Azure SQL и MongoDB. Вы также можете щелкнуть здесь, чтобы увидеть список всех поддерживаемых систем управления базами данных.

Что такое Постгрес?

Postgres также известен как PostgreSQL и представляет собой бесплатную систему управления реляционными базами данных с открытым исходным кодом. Это надмножество языка SQL, обладающее множеством функций, которые позволяют разработчикам безопасно хранить и масштабировать сложные рабочие нагрузки данных.

Предпосылки

Этот учебник представляет собой практическое демонстрационное руководство. Поэтому было бы лучше, если бы на вашем компьютере было установлено следующее:

Код для этого руководства доступен здесь , на Github, так что не стесняйтесь клонировать его и следовать дальше.

Настройка проекта

Начнем с настройки нашего приложения Next.js. Чтобы начать, выполните команду ниже.

npx create-next-app@latest

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

yarn add fastify fastify-nextjs iron-session @prisma/client
yarn add prisma nodemon --dev

Дождитесь завершения установки.

Настройка Next.js и Fastify

По умолчанию Next.js не использует Fastify в качестве своего сервера. Чтобы использовать Fastify для обслуживания нашего приложения Next.js, отредактируйте поле scripts в package.jsonфайле с помощью приведенного ниже фрагмента кода.

"scripts": {
"dev": "nodemon server.js",
"build": "next build",
"start": "next start",
"lint": "next lint"
}

Создание нашего сервера Fastify

Теперь давайте создадим server.jsфайл. Этот файл является точкой входа нашего приложения, а затем мы добавляем, require('fastify-nextjs')чтобы включить плагин, который предоставляет API Next.js в fastify для обработки рендеринга.

Откройте server.jsфайл и добавьте фрагменты кода ниже:

const fastify = require('fastify')()
async function noOpParser(req, payload) {
return payload;
}
fastify.register(require('fastify-nextjs')).after(() => {
fastify.addContentTypeParser('text/plain', noOpParser);
fastify.addContentTypeParser('application/json', noOpParser);
fastify.next('/*')
fastify.next('/api/*', { method: 'ALL' });
})
fastify.listen(3000, err => {
if (err) throw err
console.log('Server listening on <http://localhost:3000>')
})

В приведенном выше фрагменте кода мы используем fastify-nextjsплагин, предоставляющий API Next.js в Fastify, который обрабатывает рендеринг для нас. Затем мы анализируем входящие запросы с помощью noOpParserфункции, которая делает тело запроса доступным для наших обработчиков маршрутов API Next.js, и мы определяем два маршрута для нашего приложения с помощью [fastify.next](<http://fastify.next>команды. Затем мы создаем наш сервер Fastify и заставляем его слушать порт 3000.

Теперь запустите приложение с помощью yarn devкоманды: приложение будет работать на localhost:3000.

Настройка призмы

Сначала выполните следующую команду, чтобы получить базовую настройку Prisma:

npx prisma init

Приведенная выше команда создаст каталог prisma с schema.prismaфайлом. Это ваш основной файл конфигурации Prisma, который будет содержать схему вашей базы данных. Также .envфайл будет добавлен в корень проекта. Откройте .envфайл и замените фиктивный URL-адрес подключения на URL-адрес подключения вашей базы данных PostgreSQL.

Замените код в prisma/schema.prismaфайле следующим:

datasource db {
  url      = env("DATABASE_URL")
  provider = "postgresql"
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  email     String   @unique
  name      String
  password  String
  role      Role     @default(EMPLOYEE)
  attendance     Attendance[]
  AttendanceSheet AttendanceSheet[]
}

model AttendanceSheet {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  createdBy    User?    @relation(fields: [userId], references: [id])
  userId  Int?
}

model Attendance {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  signIn    Boolean @default(true)
  signOut   Boolean
  signInTime    DateTime @default(now())
  signOutTime   DateTime 
  user    User?    @relation(fields: [userId], references: [id])
  userId  Int?
}

enum Role {
  EMPLOYEE
  ADMIN
}

В приведенном выше фрагменте кода мы создали пользователя, таблицу посещаемости и модель посещаемости, определяя отношения между каждой моделью.

Затем создайте эти таблицы в базе данных. Выполните следующую команду:

npx prisma db push

После выполнения приведенной выше команды вы должны увидеть вывод, как показано на снимке экрана ниже, в вашем терминале:

Создание служебных функций

Закончив настройку Prisma, давайте создадим три служебные функции, которые будут время от времени использоваться в нашем приложении.

Откройте файл lib/parseBody.js и добавьте следующий фрагмент кода. Эта функция анализирует тело запроса в формате JSON:

export const parseBody = (body) => {
if (typeof body === "string") return JSON.parse(body)
return body
}

Откройте файл lib/request.js и добавьте следующий фрагмент кода. Эта функция отправляет запрос POST.

export async function postData(url = '', data='') {
const response = await fetch(url, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify(data)
});
return response.json();
}

Откройте файл /lib/request.js и добавьте следующий фрагмент кода. Эта функция возвращает объект свойств сеанса для железной сессии iron-session.

export const sessionCookie = () => {
return ({
cookieName: "auth",
password: process.env.SESSION_PASSWORD,
// secure: true should be used in production (HTTPS) but can't be used in development (HTTP)
cookieOptions: {
secure: process.env.NODE_ENV === "production",
},
})
}

Далее добавляем SESSION_PASSWORDв файл .env: это должна быть строка не менее 32 символов.

Стилизация приложения

Закончив работу с нашими служебными функциями, давайте добавим в приложение несколько стилей. Мы используем модули css для этого приложения, поэтому откройте styles/Home.modules.cssфайл и добавьте фрагмент кода ниже:

.container {
  padding: 0 2rem;
}

.main {
  min-height: 100vh;
  padding: 4rem 0;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.login {
  width: 450px;
}

.login input {
  width: 100%;
  height: 50px;
  margin: 4px;
}

.login button {
  width: 100%;
  height: 50px;
  margin: 4px;
}

.dashboard {
  display: grid;
  grid-template-columns: 3fr 9fr;
  grid-template-rows: 1fr;
  grid-column-gap: 0px;
  grid-row-gap: 0px;
  height: calc(100vh - 60px);
}

.navbar {
  height: 60px;
  background-color: black;
}

Создайте компонент боковой панели

Создав стиль, давайте создадим компонент боковой панели, который поможет нам переходить на разные страницы на панели инструментов нашего приложения. Откройте файл components/SideBar.js и вставьте приведенный ниже фрагмент кода.

import Link from 'next/link'
import { useRouter } from 'next/router'
import styles from '../styles/SideBar.module.css'

const SideBar = () => {

    const router = useRouter()

    const logout = async () => {

        try {

            const response = await fetch('/api/logout', {
                method: 'GET', 
                credentials: 'same-origin', 
            });

            if(response.status === 200)  router.push('/')

        } catch (e) {
            alert(e)
        }
  
    }
      

    return (
        <nav className={styles.sidebar}>

            <ul>

                <li> <Link href="/dashboard"> Dashboard</Link> </li>

                <li> <Link href="/dashboard/attendance"> Attendance </Link> </li>

                <li> <Link href="/dashboard/attendance-sheet"> Attendance Sheet </Link> </li>

                <li onClick={logout}> Logout </li>

            </ul>

        </nav>
    )

}

export default SideBar

Страница авторизации

Теперь откройте файл page/index.js, удалите весь код и добавьте следующий фрагмент кода. Приведенный ниже код отправляет почтовый запрос с адресом электронной почты и паролем, указанными в форме, на маршрут localhost:3000/api/login. После проверки учетных данных вызывается router.push('/dashboard')метод, который перенаправляет пользователя на localhost:3000/api/dashboard:

import Head from 'next/head'
import { postData } from '../lib/request';
import styles from '../styles/Home.module.css'
import { useState } from 'react';
import { useRouter } from 'next/router'

export default function Home({posts}) {

  const [data, setData] = useState({email: null, password: null});

  const router = useRouter()

  const submit = (e) => {
    e.preventDefault()

    if(data.email && data.password) {
      postData('/api/login', data).then(data => {
        console.log(data); 

        if (data.status === "success") router.push('/dashboard')
      
      });
    }

  }

  return (
    <div className={styles.container}>
      <Head>
        <title>Login</title>
        <meta name="description" content="Login" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>

        <form  className={styles.login}>

          <input 
            type={"text"} 
            placeholder="Enter Your Email" 
            onChange={(e) => setData({...data, email: e.target.value})} />

          <input 
            type={"password"}  
            placeholder="Enter Your Password"
            onChange={(e) => setData({...data, password: e.target.value})} />

          <button onClick={submit}>Login</button>

        </form>
        
      </main>

    </div>
  )
}

Настройка маршрута API входа

Теперь откройте файл page/api/login.js и добавьте следующий фрагмент кода. Мы будем использовать PrismaClientдля выполнения запросов к базе данных, и withIronSessionApiRouteэто функция Iron-Session для обработки пользовательских сеансов в приложениях RESTful.

Этот маршрут обрабатывает POST-запрос входа в систему по адресу localhost:3000/api/login и создает файлы cookie для аутентификации после аутентификации пользователя.

import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { parseBody } from '../../lib/parseBody';
import { sessionCookie } from '../../lib/session';

export default withIronSessionApiRoute(
    async function loginRoute(req, res) {

      const { email, password } = parseBody(req.body)

      const prisma = new PrismaClient()

      // By unique identifier
      const user = await prisma.user.findUnique({
        where: {
        email
      },})

      if(user.password === password) {

        // get user from database then:
        user.password = undefined
        req.session.user = user
        await req.session.save();

        return res.send({ status: 'success', data: user });

      };

    res.send({ status: 'error', message: "incorrect email or password" });

  },
  sessionCookie(),
);

Настройка маршрута API выхода

Откройте файл /page/api/logout и добавьте приведенный ниже фрагмент кода. Этот маршрут обрабатывает запросы GET к localhost:3000/api/logout, которые выводят пользователей из системы, уничтожая файлы cookie сеанса.

import { withIronSessionApiRoute } from "iron-session/next";
import { sessionCookie } from "../../lib/session";

export default withIronSessionApiRoute(
  function logoutRoute(req, res, session) {
    req.session.destroy();
    res.send({ status: "success" });
  },
  sessionCookie()
);

Создание страницы панели мониторинга

Эта страница предоставляет пользователям интерфейс для входа и выхода из листа посещаемости. Администраторы также могут создать лист посещаемости. Откройте файл page/dashboard/index.js и добавьте приведенный ниже фрагмент кода.

import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { useState, useCallback } from "react";
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";
import { postData } from "../../lib/request";

export default function Page(props) {

  const [attendanceSheet, setState] = useState(JSON.parse(props.attendanceSheet));

  const sign = useCallback((action="") => {

    const body = {
      attendanceSheetId: attendanceSheet[0]?.id,
      action
    }

    postData("/api/sign-attendance", body).then(data => {

      if (data.status === "success") {

        setState(prevState => {

          const newState = [...prevState]

          newState[0].attendance[0] = data.data

          return newState

        })
     
      }

    })

  }, [attendanceSheet])

  const createAttendance = useCallback(() => {

    postData("/api/create-attendance").then(data => {

      if (data.status === "success") {
        alert("New Attendance Sheet Created")
        setState([{...data.data, attendance:[]}])
      }

    })

  }, [])

  return (
    <div>

      <Head>
        <title>Attendance Management Dashboard</title>
        <meta name="description" content="dashboard" />
      </Head>

      <div className={styles.navbar}></div>

      <main className={styles.dashboard}>

        <SideBar />

        <div className={dashboard.users}>

          {
            props.isAdmin && <button className={dashboard.create} onClick={createAttendance}>Create Attendance Sheet</button>
          }
            
          { attendanceSheet.length > 0 &&

            <table className={dashboard.table}>
              <thead>
                <tr> 
                  <th>Id</th> <th>Created At</th> <th>Sign In</th> <th>Sign Out</th> 
                </tr>
              </thead>

              <tbody>
                <tr>
                  <td>{attendanceSheet[0]?.id}</td>
                  <td>{attendanceSheet[0]?.createdAt}</td>

                  {
                    attendanceSheet[0]?.attendance.length != 0 ? 
                      <>
                        <td>{attendanceSheet[0]?.attendance[0]?.signInTime}</td>
                        <td>{
                          attendanceSheet[0]?.attendance[0]?.signOut ? 
                          attendanceSheet[0]?.attendance[0]?.signOutTime: <button onClick={() => sign("sign-out")}> Sign Out </button> }</td>
                      </>
                      :
                      <>
                        <td> <button onClick={() => sign()}> Sign In </button> </td>
                        <td>{""}</td>
                      </>
                  }
                </tr>
              </tbody>

            </table>

          }
          
        </div>

      </main>

    </div>
  )
}

Мы используем getServerSidePropsдля генерации данных страницы, и withIronSessionSsrэто функция сеанса железа для работы со страницами, отображаемыми на стороне сервера. В следующем фрагменте кода мы запрашиваем последнюю строку таблицы посещаемости со строкой из таблицы посещаемости, где userIdравно идентификатору пользователя, хранящемуся в пользовательском сеансе. Мы также проверяем, является ли пользователь администратором.

export const getServerSideProps = withIronSessionSsr( async ({req}) => {

  const user = req.session.user

  const prisma = new PrismaClient()

  const attendanceSheet = await prisma.attendanceSheet.findMany({  
    take: 1,
    orderBy: {
      id: 'desc',
    },
    include: { 
      attendance: {
        where: {
          userId: user.id
        },
      }
    }
  })

  return {
    props: {
      attendanceSheet: JSON.stringify(attendanceSheet),
      isAdmin: user.role === "ADMIN"
    }
  }

}, sessionCookie())

Настройка маршрута Create Attendance API

Откройте файл page/api/create-attendance.js и добавьте приведенный ниже фрагмент кода.

import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionCookie } from '../../lib/session';

  
export default withIronSessionApiRoute( async function handler(req, res) {

    const prisma = new PrismaClient()

    const user = req.session.user

    const attendanceSheet = await prisma.attendanceSheet.create({
        data: {
          userId: user.id,
        },
    })

    res.json({status: "success", data: attendanceSheet});
    
}, sessionCookie())

Настройка маршрута API посещаемости входа

Этот маршрут обрабатывает наш POST-запрос API к localhost:3000/api/sign-attendance. Маршрут принимает запрос POST, а attendanceSheetIdи actionиспользуются для входа и выхода из attendanceSheet.

Откройте файл /page/api/sign-attendance.js и добавьте приведенный ниже фрагмент кода.

import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { parseBody } from '../../lib/parseBody';
import { sessionCookie } from '../../lib/session';

  
export default withIronSessionApiRoute( async function handler(req, res) {

    const prisma = new PrismaClient()

    const {attendanceSheetId, action} = parseBody(req.body)

    const user = req.session.user

    const attendance = await prisma.attendance.findMany({
        where: {
            userId: user.id,
            attendanceSheetId: attendanceSheetId
        }
    })

    //check if atendance have been created
    if (attendance.length === 0) {
        const attendance = await prisma.attendance.create({
            data: {
                userId: user.id,
                attendanceSheetId: attendanceSheetId,
                signIn: true,
                signOut: false,
                signOutTime: new Date()
            },
        })   

        return res.json({status: "success", data: attendance});

    } else if (action === "sign-out") {
        await prisma.attendance.updateMany({
            where: {
                userId: user.id,
                attendanceSheetId: attendanceSheetId
            },
            data: {
              signOut: true,
              signOutTime: new Date()
            },
        })

        return res.json({status: "success", data: { ...attendance[0], signOut: true, signOutTime: new Date()}});
    }

    res.json({status: "success", data: attendance});
    
}, sessionCookie())

Создание страницы посещаемости

Эта страница, отображаемая на стороне сервера, показывает все листы посещаемости для вошедшего в систему пользователя. Откройте файл /page/dashboard/attendance.js и добавьте приведенный ниже фрагмент кода.

import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";

export default function Page(props) {

  const data = JSON.parse(props.attendanceSheet)

  return (
    <div>

      <Head>
        <title>Attendance Management Dashboard</title>
        <meta name="description" content="dashboard" />
      </Head>

      <div className={styles.navbar}></div>

      <main className={styles.dashboard}>

        <SideBar />

        <div className={dashboard.users}>

        <table className={dashboard.table}>

          <thead>

            <tr> 
              <th> Attendance Id</th> <th>Date</th> 
              <th>Sign In Time</th> <th>Sign Out Time</th> 
            </tr> 

          </thead>

            <tbody>

              {
                data.map(data =>   {

                  const {id, createdAt, attendance } = data

  
                  return (
                    <tr key={id}> 

                      <td>{id}</td> <td>{createdAt}</td>  

                      { attendance.length === 0 ? 
                      
                        (
                          <>
                            <td>You did not Sign In</td>
                            <td>You did not Sign Out</td>
                          </>
                        )
                        :
                        (
                          <>
                            <td>{attendance[0]?.signInTime}</td>
                            <td>{attendance[0]?.signOut ? attendance[0]?.signOutTime : "You did not Sign Out"}</td>
                          </>
                        )
                        
                      }
              
                    </tr>
                  )

                })

              }  

            </tbody>

          </table>

        </div>

      </main>

    </div>
  )
}

В приведенном ниже фрагменте кода мы запрашиваем все строки из attendanceSheetтаблицы, а также получаем посещаемость, где userIdравно идентификатору пользователя, хранящемуся в сеансе пользователя.

export const getServerSideProps = withIronSessionSsr( async ({req}) => {

  const user = req.session.user

  const prisma = new PrismaClient()
  
  const attendanceSheet = await prisma.attendanceSheet.findMany({
    orderBy: {
      id: 'desc',
    },
    include: { 
      attendance: {
        where: {
          userId: user.id
        },
      }
    }
  })

  return {
    props: {
      attendanceSheet: JSON.stringify(attendanceSheet),
    }
  }

}, sessionCookie())

Создание страницы листа посещаемости

На этой странице, отображаемой на стороне сервера, показаны все листы посещаемости и сотрудники, которые вошли в этот лист посещаемости. Откройте файл /page/dashboard/attendance.js и добавьте приведенный ниже фрагмент кода.

import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";

export default function Page(props) {

  const data = JSON.parse(props.attendanceSheet)

  return (
    <div>

      <Head>
        <title>Attendance Management Dashboard</title>
        <meta name="description" content="dashboard" />
      </Head>

      <div className={styles.navbar}></div>

      <main className={styles.dashboard}>

        <SideBar />

        <div className={dashboard.users}>

        {
          data?.map(data => {

            const {id, createdAt, attendance } = data

            return (
              <>

                <table key={data.id} className={dashboard.table}>

                  <thead>
                    
                    <tr> 
                      <th> Attendance Id</th> <th>Date</th> 
                      <th> Name </th> <th> Email </th> <th> Role </th>
                      <th>Sign In Time</th> <th>Sign Out Time</th> 
                    </tr> 

                  </thead>

                  <tbody>

                    {
                      (attendance.length === 0)  &&
                      (
                        <>
                        <tr><td> {id} </td> <td>{createdAt}</td> <td colSpan={5}> No User signed this sheet</td></tr>
                        </>
                      )
                    }

                    {
                      attendance.map(data => {

                        const {name, email, role} = data.user

                      
                        return (
                          <tr key={id}> 

                            <td>{id}</td> <td>{createdAt}</td>  

                            <td>{name}</td> <td>{email}</td>

                            <td>{role}</td>

                            <td>{data.signInTime}</td>

                            <td>{data.signOut ? attendance[0]?.signOutTime: "User did not Sign Out"}</td>  
                    
                          </tr>
                        )

                      })

                    }  

                  </tbody>
                  
                </table>
              </>
            )
          })
          
          }

        </div>

      </main>

    </div>
  )
}

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

export const getServerSideProps = withIronSessionSsr(async () => {

  const prisma = new PrismaClient()

  const attendanceSheet = await prisma.attendanceSheet.findMany({
    orderBy: {
      id: 'desc',
    },
    include: { 
      attendance: {
        include: { 
          user: {
            select: {
              name: true, 
              email: true, 
              role: true
            }
          }
        }
      },
    },

  })

  return {
    props: {
      attendanceSheet: JSON.stringify(attendanceSheet),
    }
  }

}, sessionCookie())

Тестирование приложения

Во-первых, мы должны добавить пользователей в нашу базу данных. Мы собираемся сделать это с помощью Prisma Studio. Чтобы запустить Prisma studio, выполните следующую команду:

npx prisma studio

Индексная страница Prisma выглядит так:

Индексная страница Prisma


Чтобы создать пользователя базы данных с ролью ADMIN и нескольких пользователей с ролью EMPLOYEE, перейдите на эту страницу:

Добавление пользователя

Нажмите «Добавить запись», затем заполните необходимые поля: пароль, имя, адрес электронной почты и роль. Когда вы закончите, нажмите зеленую кнопку «Сохранить 1 изменение». Обратите внимание, что для простоты мы не хэшируем пароль.

Запустите сервер с помощью yarn dev. Это запускает сервер и запускает приложение на [localhost:3000](<http://localhost:3000>), страница входа показана ниже.

Страница входа

Войдите в систему с помощью пользователя с ролью ADMIN, поскольку только пользователи с правами администратора могут создавать листы посещаемости. После успешного входа приложение перенаправит вас на панель инструментов.

Нажмите кнопку «Создать лист посещаемости», чтобы создать лист посещаемости, затем подождите, пока запрос завершится, и появится лист посещаемости. Панель управления пользователя показана ниже.

Создание листа посещаемости

Лист посещаемости показан ниже, нажмите кнопку «Войти», чтобы войти в систему. После успешного входа отобразится время входа и кнопка «Выйти». Нажмите кнопку «Выход», чтобы выйти, и повторите этот процесс много раз с разными пользователями.

Успешный вход

Затем нажмите ссылку «Посещаемость» на боковой панели, чтобы просмотреть посещаемость пользователей. Результаты должны совпадать с показанными ниже:

Страница посещаемости

Затем нажмите ссылку «Лист посещаемости» на боковой панели, чтобы просмотреть посещаемость всех пользователей. Результаты показаны ниже:

Страница листа посещаемости

Вывод

В этой статье вы узнали, как использовать собственный сервер Fastify с Next.js. Вы также узнали о Prisma и студии Prisma. Я рассказал вам, как подключить Prisma к базе данных Postgres и как создавать, читать и обновлять базу данных с помощью клиента Prisma и студии Prisma.

Ссылка: https://arctype.com/blog/fullstack-nextjs-postgres-fastify/

#fullstack #nextjs #postgre #fastify

Как создать полнофункциональное приложение с помощью Next.js, Prisma
高橋  陽子

高橋 陽子

1657280057

打開選項 如何使用 Next.js、Prisma、Postgres 構建全棧應用程序

在本文中,我們將學習如何使用 Next.js、Prisma、Postgres 和 Fastify 構建全棧應用程序。我們將構建一個考勤管理演示應用程序來管理員工的考勤。該應用程序的流程很簡單:管理用戶登錄,創建當天的考勤表,然後每個員工登錄和退出考勤表。

Next.js 是什麼?

Next.js 是一個靈活的 React 框架,它為您提供構建塊來創建快速的 Web 應用程序。它通常被稱為全棧 React 框架,因為它使得前端和後端應用程序可以在同一個代碼庫上使用無服務器功能來實現。

什麼是棱鏡?

Prisma 是一個開源的 Node.js 和 Typescript ORM,它極大地簡化了 SQL 數據庫的數據建模、遷移和數據訪問。在撰寫本文時,Prisma 支持以下數據庫管理系統:PostgreSQL、MySQL、MariaDB、SQLite、AWS Aurora、Microsoft SQL Server、Azure SQL 和 MongoDB。您可能還想單擊此處查看所有受支持的數據庫管理系統的列表。

什麼是 Postgres?

Postgres 也稱為 PostgreSQL,它是一個免費和開源的關係數據庫管理系統。它是 SQL 語言的超集,它具有許多功能,允許開發人員安全地存儲和擴展複雜的數據工作負載。

先決條件

本教程是一個動手演示教程。因此,最好在您的計算機上安裝以下內容以進行後續操作:

本教程的代碼可Github 上找到,因此請隨意克隆它並繼續學習。

項目設置

讓我們從設置 Next.js 應用程序開始。要開始,請運行以下命令。

npx create-next-app@latest

等待安裝完成,然後運行下面的命令來安裝我們的依賴項。

yarn add fastify fastify-nextjs iron-session @prisma/client
yarn add prisma nodemon --dev

等待安裝完成。

設置 Next.js 和 Fastify

默認情況下,Next.js 不使用 Fastify 作為其服務器。要使用 Fastify 為我們的 Next.js 應用程序提供服務,請package.json使用下面的代碼片段編輯文件中的腳本字段。

"scripts": {
"dev": "nodemon server.js",
"build": "next build",
"start": "next start",
"lint": "next lint"
}

創建我們的 Fastify 服務器

現在讓我們創建一個server.js文件。該文件是我們應用程序的入口點,然後我們添加require('fastify-nextjs')以包含插件,該插件在 fastify 中公開 Next.js API 以處理渲染。

打開server.js文件,並添加以下代碼片段:

const fastify = require('fastify')()
async function noOpParser(req, payload) {
return payload;
}
fastify.register(require('fastify-nextjs')).after(() => {
fastify.addContentTypeParser('text/plain', noOpParser);
fastify.addContentTypeParser('application/json', noOpParser);
fastify.next('/*')
fastify.next('/api/*', { method: 'ALL' });
})
fastify.listen(3000, err => {
if (err) throw err
console.log('Server listening on <http://localhost:3000>')
})

在上面的代碼片段中,我們使用了fastify-nextjs在 Fastify 中公開 Next.js API 的插件,它為我們處理渲染。然後我們使用函數解析傳入的請求,使請求主體可用於我們的 Next.js API 路由處理程序,並使用命令noOpParser為我們的應用程序定義兩個路由。[fastify.next](<http://fastify.next>然後我們創建我們的 Fastify 服務器並讓它監聽 3000 端口。

現在繼續使用yarn dev命令運行應用程序:應用程序將在localhost:3000.

棱鏡設置

首先,運行以下命令以獲取基本的 Prisma 設置:

npx prisma init

上面的命令將創建一個包含schema.prisma文件的 prisma 目錄。這是您的主要 Prisma 配置文件,它將包含您的數據庫模式。此外,.env將在項目的根目錄中添加一個文件。打開.env文件並將虛擬連接 URL 替換為 PostgreSQL 數據庫的連接 URL。

將文件中的代碼替換為prisma/schema.prisma以下內容:

datasource db {
  url      = env("DATABASE_URL")
  provider = "postgresql"
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  email     String   @unique
  name      String
  password  String
  role      Role     @default(EMPLOYEE)
  attendance     Attendance[]
  AttendanceSheet AttendanceSheet[]
}

model AttendanceSheet {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  createdBy    User?    @relation(fields: [userId], references: [id])
  userId  Int?
}

model Attendance {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  signIn    Boolean @default(true)
  signOut   Boolean
  signInTime    DateTime @default(now())
  signOutTime   DateTime 
  user    User?    @relation(fields: [userId], references: [id])
  userId  Int?
}

enum Role {
  EMPLOYEE
  ADMIN
}

在上面的代碼片段中,我們創建了一個 User、AttendanceSheet 和 Attendance Model,定義了每個模型之間的關係。

接下來,在數據庫中創建這些表。運行以下命令:

npx prisma db push

運行上述命令後,您應該會在終端中看到如下屏幕截圖所示的輸出:

創建實用函數

完成 Prisma 設置後,讓我們創建三個實用程序函數,它們將不時在我們的應用程序中使用。

打開 lib/parseBody.js 文件並添加以下代碼片段。此函數將請求正文解析為 JSON:

export const parseBody = (body) => {
if (typeof body === "string") return JSON.parse(body)
return body
}

打開 lib/request.js 文件並添加以下代碼片段。該函數發送一個 POST 請求。

export async function postData(url = '', data='') {
const response = await fetch(url, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify(data)
});
return response.json();
}

打開 /lib/request.js 文件並添加以下代碼片段。此函數返回 Iron 會話 iron-session 的會話屬性對象。

export const sessionCookie = () => {
return ({
cookieName: "auth",
password: process.env.SESSION_PASSWORD,
// secure: true should be used in production (HTTPS) but can't be used in development (HTTP)
cookieOptions: {
secure: process.env.NODE_ENV === "production",
},
})
}

接下來,添加SESSION_PASSWORD到 .env 文件:它應該是至少 32 個字符的字符串。

為應用程序設計樣式

完成我們的實用程序功能後,讓我們為應用程序添加一些樣式。我們正在為這個應用程序使用 css 模塊,所以打開styles/Home.modules.css文件並添加下面的代碼片段:

.container {
  padding: 0 2rem;
}

.main {
  min-height: 100vh;
  padding: 4rem 0;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.login {
  width: 450px;
}

.login input {
  width: 100%;
  height: 50px;
  margin: 4px;
}

.login button {
  width: 100%;
  height: 50px;
  margin: 4px;
}

.dashboard {
  display: grid;
  grid-template-columns: 3fr 9fr;
  grid-template-rows: 1fr;
  grid-column-gap: 0px;
  grid-row-gap: 0px;
  height: calc(100vh - 60px);
}

.navbar {
  height: 60px;
  background-color: black;
}

創建側邊欄組件

完成樣式後,讓我們創建側邊欄組件來幫助我們導航到應用儀表板上的不同頁面。打開 components/SideBar.js 文件,然後粘貼下面的代碼片段。

import Link from 'next/link'
import { useRouter } from 'next/router'
import styles from '../styles/SideBar.module.css'

const SideBar = () => {

    const router = useRouter()

    const logout = async () => {

        try {

            const response = await fetch('/api/logout', {
                method: 'GET', 
                credentials: 'same-origin', 
            });

            if(response.status === 200)  router.push('/')

        } catch (e) {
            alert(e)
        }
  
    }
      

    return (
        <nav className={styles.sidebar}>

            <ul>

                <li> <Link href="/dashboard"> Dashboard</Link> </li>

                <li> <Link href="/dashboard/attendance"> Attendance </Link> </li>

                <li> <Link href="/dashboard/attendance-sheet"> Attendance Sheet </Link> </li>

                <li onClick={logout}> Logout </li>

            </ul>

        </nav>
    )

}

export default SideBar

登錄頁面

現在打開 page/index.js 文件,刪除那裡的所有代碼並添加以下代碼片段。下面的代碼將使用通過表單提供的電子郵件和密碼發送到 localhost:3000/api/login 路由的發布請求。驗證憑據後,它會調用router.push('/dashboard')將用戶重定向到 localhost:3000/api/dashboard 的方法:

import Head from 'next/head'
import { postData } from '../lib/request';
import styles from '../styles/Home.module.css'
import { useState } from 'react';
import { useRouter } from 'next/router'

export default function Home({posts}) {

  const [data, setData] = useState({email: null, password: null});

  const router = useRouter()

  const submit = (e) => {
    e.preventDefault()

    if(data.email && data.password) {
      postData('/api/login', data).then(data => {
        console.log(data); 

        if (data.status === "success") router.push('/dashboard')
      
      });
    }

  }

  return (
    <div className={styles.container}>
      <Head>
        <title>Login</title>
        <meta name="description" content="Login" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>

        <form  className={styles.login}>

          <input 
            type={"text"} 
            placeholder="Enter Your Email" 
            onChange={(e) => setData({...data, email: e.target.value})} />

          <input 
            type={"password"}  
            placeholder="Enter Your Password"
            onChange={(e) => setData({...data, password: e.target.value})} />

          <button onClick={submit}>Login</button>

        </form>
        
      </main>

    </div>
  )
}

設置登錄 API 路由

現在打開 page/api/login.js 文件並添加以下代碼片段。我們將使用它PrismaClient來進行數據庫查詢,並且withIronSessionApiRoute是用於在 RESTful 應用程序中處理用戶會話的鐵會話功能。

此路由處理對 localhost:3000/api/login 的登錄 POST 請求,並在用戶通過身份驗證後生成身份驗證 cookie。

import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { parseBody } from '../../lib/parseBody';
import { sessionCookie } from '../../lib/session';

export default withIronSessionApiRoute(
    async function loginRoute(req, res) {

      const { email, password } = parseBody(req.body)

      const prisma = new PrismaClient()

      // By unique identifier
      const user = await prisma.user.findUnique({
        where: {
        email
      },})

      if(user.password === password) {

        // get user from database then:
        user.password = undefined
        req.session.user = user
        await req.session.save();

        return res.send({ status: 'success', data: user });

      };

    res.send({ status: 'error', message: "incorrect email or password" });

  },
  sessionCookie(),
);

設置註銷 API 路由

打開 /page/api/logout 文件並添加下面的代碼片段。此路由處理對 localhost:3000/api/logout 的 GET 請求,該請求通過銷毀會話 cookie 來註銷用戶。

import { withIronSessionApiRoute } from "iron-session/next";
import { sessionCookie } from "../../lib/session";

export default withIronSessionApiRoute(
  function logoutRoute(req, res, session) {
    req.session.destroy();
    res.send({ status: "success" });
  },
  sessionCookie()
);

創建儀表板頁面

該頁面為用戶提供了簽到和退出考勤表的界面。管理員還可以創建考勤表。打開 page/dashboard/index.js 文件並添加下面的代碼片段。

import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { useState, useCallback } from "react";
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";
import { postData } from "../../lib/request";

export default function Page(props) {

  const [attendanceSheet, setState] = useState(JSON.parse(props.attendanceSheet));

  const sign = useCallback((action="") => {

    const body = {
      attendanceSheetId: attendanceSheet[0]?.id,
      action
    }

    postData("/api/sign-attendance", body).then(data => {

      if (data.status === "success") {

        setState(prevState => {

          const newState = [...prevState]

          newState[0].attendance[0] = data.data

          return newState

        })
     
      }

    })

  }, [attendanceSheet])

  const createAttendance = useCallback(() => {

    postData("/api/create-attendance").then(data => {

      if (data.status === "success") {
        alert("New Attendance Sheet Created")
        setState([{...data.data, attendance:[]}])
      }

    })

  }, [])

  return (
    <div>

      <Head>
        <title>Attendance Management Dashboard</title>
        <meta name="description" content="dashboard" />
      </Head>

      <div className={styles.navbar}></div>

      <main className={styles.dashboard}>

        <SideBar />

        <div className={dashboard.users}>

          {
            props.isAdmin && <button className={dashboard.create} onClick={createAttendance}>Create Attendance Sheet</button>
          }
            
          { attendanceSheet.length > 0 &&

            <table className={dashboard.table}>
              <thead>
                <tr> 
                  <th>Id</th> <th>Created At</th> <th>Sign In</th> <th>Sign Out</th> 
                </tr>
              </thead>

              <tbody>
                <tr>
                  <td>{attendanceSheet[0]?.id}</td>
                  <td>{attendanceSheet[0]?.createdAt}</td>

                  {
                    attendanceSheet[0]?.attendance.length != 0 ? 
                      <>
                        <td>{attendanceSheet[0]?.attendance[0]?.signInTime}</td>
                        <td>{
                          attendanceSheet[0]?.attendance[0]?.signOut ? 
                          attendanceSheet[0]?.attendance[0]?.signOutTime: <button onClick={() => sign("sign-out")}> Sign Out </button> }</td>
                      </>
                      :
                      <>
                        <td> <button onClick={() => sign()}> Sign In </button> </td>
                        <td>{""}</td>
                      </>
                  }
                </tr>
              </tbody>

            </table>

          }
          
        </div>

      </main>

    </div>
  )
}

我們使用getServerSideProps來生成頁面數據,並且withIronSessionSsr是用於處理服務器端渲染頁面的 iron-session 函數。在下面的代碼片段中,我們使用出勤表中的一行查詢出勤表的最後一行,其中userId等於存儲在用戶會話中的用戶 ID。我們還檢查用戶是否是管理員。

export const getServerSideProps = withIronSessionSsr( async ({req}) => {

  const user = req.session.user

  const prisma = new PrismaClient()

  const attendanceSheet = await prisma.attendanceSheet.findMany({  
    take: 1,
    orderBy: {
      id: 'desc',
    },
    include: { 
      attendance: {
        where: {
          userId: user.id
        },
      }
    }
  })

  return {
    props: {
      attendanceSheet: JSON.stringify(attendanceSheet),
      isAdmin: user.role === "ADMIN"
    }
  }

}, sessionCookie())

設置創建出勤 API 路由

打開 page/api/create-attendance.js 文件並添加下面的代碼片段。

import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionCookie } from '../../lib/session';

  
export default withIronSessionApiRoute( async function handler(req, res) {

    const prisma = new PrismaClient()

    const user = req.session.user

    const attendanceSheet = await prisma.attendanceSheet.create({
        data: {
          userId: user.id,
        },
    })

    res.json({status: "success", data: attendanceSheet});
    
}, sessionCookie())

設置簽到 API 路由

該路由處理我們對 localhost:3000/api/sign-attendance 的 API POST 請求。該路由接受 POST 請求,而attendanceSheetIdaction用於登錄和註銷attendanceSheet

打開 /page/api/sign-attendance.js 文件並添加下面的代碼片段。

import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { parseBody } from '../../lib/parseBody';
import { sessionCookie } from '../../lib/session';

  
export default withIronSessionApiRoute( async function handler(req, res) {

    const prisma = new PrismaClient()

    const {attendanceSheetId, action} = parseBody(req.body)

    const user = req.session.user

    const attendance = await prisma.attendance.findMany({
        where: {
            userId: user.id,
            attendanceSheetId: attendanceSheetId
        }
    })

    //check if atendance have been created
    if (attendance.length === 0) {
        const attendance = await prisma.attendance.create({
            data: {
                userId: user.id,
                attendanceSheetId: attendanceSheetId,
                signIn: true,
                signOut: false,
                signOutTime: new Date()
            },
        })   

        return res.json({status: "success", data: attendance});

    } else if (action === "sign-out") {
        await prisma.attendance.updateMany({
            where: {
                userId: user.id,
                attendanceSheetId: attendanceSheetId
            },
            data: {
              signOut: true,
              signOutTime: new Date()
            },
        })

        return res.json({status: "success", data: { ...attendance[0], signOut: true, signOutTime: new Date()}});
    }

    res.json({status: "success", data: attendance});
    
}, sessionCookie())

創建考勤頁面

此服務器端呈現的頁面顯示登錄用戶的所有考勤表。打開 /page/dashboard/attendance.js 文件並添加下面的代碼片段。

import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";

export default function Page(props) {

  const data = JSON.parse(props.attendanceSheet)

  return (
    <div>

      <Head>
        <title>Attendance Management Dashboard</title>
        <meta name="description" content="dashboard" />
      </Head>

      <div className={styles.navbar}></div>

      <main className={styles.dashboard}>

        <SideBar />

        <div className={dashboard.users}>

        <table className={dashboard.table}>

          <thead>

            <tr> 
              <th> Attendance Id</th> <th>Date</th> 
              <th>Sign In Time</th> <th>Sign Out Time</th> 
            </tr> 

          </thead>

            <tbody>

              {
                data.map(data =>   {

                  const {id, createdAt, attendance } = data

  
                  return (
                    <tr key={id}> 

                      <td>{id}</td> <td>{createdAt}</td>  

                      { attendance.length === 0 ? 
                      
                        (
                          <>
                            <td>You did not Sign In</td>
                            <td>You did not Sign Out</td>
                          </>
                        )
                        :
                        (
                          <>
                            <td>{attendance[0]?.signInTime}</td>
                            <td>{attendance[0]?.signOut ? attendance[0]?.signOutTime : "You did not Sign Out"}</td>
                          </>
                        )
                        
                      }
              
                    </tr>
                  )

                })

              }  

            </tbody>

          </table>

        </div>

      </main>

    </div>
  )
}

在下面的代碼片段中,我們從表中查詢所有行,attendanceSheet並獲取與userId存儲在用戶會話中的用戶 ID 相同的出勤率。

export const getServerSideProps = withIronSessionSsr( async ({req}) => {

  const user = req.session.user

  const prisma = new PrismaClient()
  
  const attendanceSheet = await prisma.attendanceSheet.findMany({
    orderBy: {
      id: 'desc',
    },
    include: { 
      attendance: {
        where: {
          userId: user.id
        },
      }
    }
  })

  return {
    props: {
      attendanceSheet: JSON.stringify(attendanceSheet),
    }
  }

}, sessionCookie())

創建考勤表頁面

此服務器端呈現的頁面顯示所有考勤表和登錄到該考勤表的員工。打開 /page/dashboard/attendance.js 文件並添加下面的代碼片段。

import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";

export default function Page(props) {

  const data = JSON.parse(props.attendanceSheet)

  return (
    <div>

      <Head>
        <title>Attendance Management Dashboard</title>
        <meta name="description" content="dashboard" />
      </Head>

      <div className={styles.navbar}></div>

      <main className={styles.dashboard}>

        <SideBar />

        <div className={dashboard.users}>

        {
          data?.map(data => {

            const {id, createdAt, attendance } = data

            return (
              <>

                <table key={data.id} className={dashboard.table}>

                  <thead>
                    
                    <tr> 
                      <th> Attendance Id</th> <th>Date</th> 
                      <th> Name </th> <th> Email </th> <th> Role </th>
                      <th>Sign In Time</th> <th>Sign Out Time</th> 
                    </tr> 

                  </thead>

                  <tbody>

                    {
                      (attendance.length === 0)  &&
                      (
                        <>
                        <tr><td> {id} </td> <td>{createdAt}</td> <td colSpan={5}> No User signed this sheet</td></tr>
                        </>
                      )
                    }

                    {
                      attendance.map(data => {

                        const {name, email, role} = data.user

                      
                        return (
                          <tr key={id}> 

                            <td>{id}</td> <td>{createdAt}</td>  

                            <td>{name}</td> <td>{email}</td>

                            <td>{role}</td>

                            <td>{data.signInTime}</td>

                            <td>{data.signOut ? attendance[0]?.signOutTime: "User did not Sign Out"}</td>  
                    
                          </tr>
                        )

                      })

                    }  

                  </tbody>
                  
                </table>
              </>
            )
          })
          
          }

        </div>

      </main>

    </div>
  )
}

在下面的代碼片段中,我們從表中查詢所有行,attendanceSheet並選擇姓名、電子郵件和角色來獲取出勤率。

export const getServerSideProps = withIronSessionSsr(async () => {

  const prisma = new PrismaClient()

  const attendanceSheet = await prisma.attendanceSheet.findMany({
    orderBy: {
      id: 'desc',
    },
    include: { 
      attendance: {
        include: { 
          user: {
            select: {
              name: true, 
              email: true, 
              role: true
            }
          }
        }
      },
    },

  })

  return {
    props: {
      attendanceSheet: JSON.stringify(attendanceSheet),
    }
  }

}, sessionCookie())

測試應用程序

首先,我們必須將用戶添加到我們的數據庫中。我們將使用 Prisma Studio 來做到這一點。要啟動 Prisma studio,請運行以下命令:

npx prisma studio

Prisma 索引頁面如下所示:

棱鏡索引頁面


要創建具有 ADMIN 角色的數據庫用戶和具有 EMPLOYEE 角色的多個用戶,請轉到此頁面:

添加用戶

單擊添加記錄,然後填寫必填字段:密碼、姓名、電子郵件和角色。完成後,單擊綠色的 Save 1 change 按鈕。請注意,為簡單起見,我們沒有散列密碼。

使用 啟動服務器yarn dev。這將啟動服務器並在 [localhost:3000](<http://localhost:3000>) 上運行應用程序,登錄頁面如下所示。

登錄頁面

使用具有 ADMIN 角色的用戶登錄,因為只有管理用戶才能創建考勤表。登錄成功後,該應用程序會將您重定向到儀表板。

點擊創建考勤表按鈕創建考勤表,然後等待請求完成,考勤表就會出現。用戶儀表板如下所示。

創建考勤表

考勤表如下圖,點擊Sign In按鈕進行簽到。簽到成功後,會顯示簽到時間,並且會出現Sign Out按鈕。單擊“退出”按鈕退出,並對不同的用戶重複此過程多次。

成功登錄

接下來單擊側邊欄中的出勤鏈接以查看用戶的出勤情況。結果應與下面顯示的匹配:

出勤頁面

接下來點擊側邊欄的考勤錶鍊接,查看所有用戶的考勤情況。結果如下所示:

考勤表頁面

結論

在本文中,您學習瞭如何將自定義 Fastify 服務器與 Next.js 一起使用。您還了解了 Prisma 和 Prisma studio。我已經向您介紹瞭如何將 Prisma 連接到 Postgres 數據庫,以及如何使用 Prisma 客戶端和 Prisma studio 創建、讀取和更新數據庫。

鏈接:https ://arctype.com/blog/fullstack-nextjs-postgres-fastify/

#fullstack #nextjs #postgre #fastify

 打開選項 如何使用 Next.js、Prisma、Postgres 構建全棧應用程序
Thierry  Perret

Thierry Perret

1657269207

Comment Créer Une Application Complète Avec Next.js, Prisma, Postgres

Dans cet article, nous allons apprendre à créer une application Full-stack à l'aide de Next.js, Prisma, Postgres et Fastify. Nous allons créer une application de démonstration de gestion des présences qui gère les présences des employés. Le déroulement de l'application est simple : un utilisateur administratif se connecte, crée une feuille de présence pour la journée, puis chaque employé se connecte et se déconnecte de la feuille de présence.

Qu'est-ce que Next.js ?

Next.js est un framework React flexible qui vous donne des éléments de base pour créer des applications Web rapides. Il est souvent appelé le framework React fullstack car il permet d'avoir des applications frontend et backend sur la même base de code avec des fonctions sans serveur.

Qu'est-ce que Prisma ?

Prisma est un ORM open-source, Node.js et Typescript qui simplifie considérablement la modélisation des données, les migrations et l'accès aux données pour les bases de données SQL. Au moment de la rédaction de cet article, Prisma prend en charge les systèmes de gestion de base de données suivants : PostgreSQL, MySQL, MariaDB, SQLite, AWS Aurora, Microsoft SQL Server, Azure SQL et MongoDB. Vous pouvez également cliquer ici pour voir la liste de tous les systèmes de gestion de base de données pris en charge.

Qu'est-ce que Postgres ?

Postgres est également connu sous le nom de PostgreSQL et c'est un système de gestion de base de données relationnelle libre et open source. Il s'agit d'un sur-ensemble du langage SQL, et il possède de nombreuses fonctionnalités qui permettent aux développeurs de stocker et de mettre à l'échelle en toute sécurité des charges de travail de données complexes.

Conditions préalables

Ce didacticiel est un didacticiel de démonstration pratique. Par conséquent, il serait préférable que les éléments suivants soient installés sur votre ordinateur pour suivre :

Le code de ce tutoriel est disponible ici sur Github, alors n'hésitez pas à le cloner et à suivre.

Configuration du projet

Commençons par configurer notre application Next.js. Pour commencer, exécutez la commande ci-dessous.

npx create-next-app@latest

Attendez que l'installation soit terminée, puis exécutez la commande ci-dessous pour installer nos dépendances.

yarn add fastify fastify-nextjs iron-session @prisma/client
yarn add prisma nodemon --dev

Attendez que l'installation soit terminée.

Configurer Next.js et Fastify

Par défaut, Next.js n'utilise pas Fastify comme serveur. Pour utiliser Fastify pour servir notre application Next.js, modifiez le champ des scripts dans le package.jsonfichier avec l'extrait de code ci-dessous.

"scripts": {
"dev": "nodemon server.js",
"build": "next build",
"start": "next start",
"lint": "next lint"
}

Création de notre serveur Fastify

Créons maintenant un server.jsfichier. Ce fichier est le point d'entrée de notre application, puis nous ajoutons le require('fastify-nextjs')pour inclure le plugin qui expose l'API Next.js dans fastify pour gérer le rendu.

Ouvrez le server.jsfichier et ajoutez les extraits de code ci-dessous :

const fastify = require('fastify')()
async function noOpParser(req, payload) {
return payload;
}
fastify.register(require('fastify-nextjs')).after(() => {
fastify.addContentTypeParser('text/plain', noOpParser);
fastify.addContentTypeParser('application/json', noOpParser);
fastify.next('/*')
fastify.next('/api/*', { method: 'ALL' });
})
fastify.listen(3000, err => {
if (err) throw err
console.log('Server listening on <http://localhost:3000>')
})

Dans l'extrait de code ci-dessus, nous utilisons le fastify-nextjsplugin qui a exposé l'API Next.js dans Fastify qui gère le rendu pour nous. Ensuite, nous analysons les requêtes entrantes avec la noOpParserfonction qui met le corps de la requête à la disposition de nos gestionnaires de routes API Next.js et nous définissons deux routes pour notre application avec la [fastify.next](<http://fastify.next>commande. Ensuite, nous créons notre serveur Fastify et le faisons écouter le port 3000.

Maintenant, lancez l'application avec la yarn devcommande : l'application s'exécutera sur localhost:3000.

Configuration de Prisma

Tout d'abord, exécutez la commande suivante pour obtenir une configuration de base de Prisma :

npx prisma init

La commande ci-dessus créera un répertoire prisma avec un schema.prismafichier. Il s'agit de votre fichier de configuration Prisma principal qui contiendra le schéma de votre base de données. De plus, un .envfichier sera ajouté à la racine du projet. Ouvrez le .envfichier et remplacez l'URL de connexion factice par l'URL de connexion de votre base de données PostgreSQL.

Remplacez le code dans le prisma/schema.prismafichier par ce qui suit :

datasource db {
  url      = env("DATABASE_URL")
  provider = "postgresql"
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  email     String   @unique
  name      String
  password  String
  role      Role     @default(EMPLOYEE)
  attendance     Attendance[]
  AttendanceSheet AttendanceSheet[]
}

model AttendanceSheet {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  createdBy    User?    @relation(fields: [userId], references: [id])
  userId  Int?
}

model Attendance {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  signIn    Boolean @default(true)
  signOut   Boolean
  signInTime    DateTime @default(now())
  signOutTime   DateTime 
  user    User?    @relation(fields: [userId], references: [id])
  userId  Int?
}

enum Role {
  EMPLOYEE
  ADMIN
}

Dans l'extrait de code ci-dessus, nous avons créé un utilisateur, une feuille de présence et un modèle de présence, définissant les relations entre chaque modèle.

Ensuite, créez ces tables dans la base de données. Exécutez la commande suivante :

npx prisma db push

Après avoir exécuté la commande ci-dessus, vous devriez voir la sortie comme indiqué dans la capture d'écran ci-dessous dans votre terminal :

Création de fonctions utilitaires

Une fois la configuration de Prisma terminée, créons trois fonctions utilitaires qui seront utilisées de temps à autre dans notre application.

Ouvrez le fichier lib/parseBody.js et ajoutez l'extrait de code suivant. Cette fonction analyse le corps de la requête en JSON :

export const parseBody = (body) => {
if (typeof body === "string") return JSON.parse(body)
return body
}

Ouvrez le fichier lib/request.js et ajoutez l'extrait de code suivant. Cette fonction envoie une requête POST.

export async function postData(url = '', data='') {
const response = await fetch(url, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify(data)
});
return response.json();
}

Ouvrez le fichier /lib/request.js et ajoutez l'extrait de code suivant. Cette fonction renvoie un objet de propriétés de session pour iron session iron-session.

export const sessionCookie = () => {
return ({
cookieName: "auth",
password: process.env.SESSION_PASSWORD,
// secure: true should be used in production (HTTPS) but can't be used in development (HTTP)
cookieOptions: {
secure: process.env.NODE_ENV === "production",
},
})
}

Ensuite, ajoutez SESSION_PASSWORDau fichier .env : il doit s'agir d'une chaîne d'au moins 32 caractères.

Styliser l'application

Une fois nos fonctions utilitaires terminées, ajoutons quelques styles à l'application. Nous utilisons des modules CSS pour cette application, alors ouvrez le styles/Home.modules.cssfichier et ajoutez l'extrait de code ci-dessous :

.container {
  padding: 0 2rem;
}

.main {
  min-height: 100vh;
  padding: 4rem 0;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.login {
  width: 450px;
}

.login input {
  width: 100%;
  height: 50px;
  margin: 4px;
}

.login button {
  width: 100%;
  height: 50px;
  margin: 4px;
}

.dashboard {
  display: grid;
  grid-template-columns: 3fr 9fr;
  grid-template-rows: 1fr;
  grid-column-gap: 0px;
  grid-row-gap: 0px;
  height: calc(100vh - 60px);
}

.navbar {
  height: 60px;
  background-color: black;
}

Créer le composant de la barre latérale

Une fois notre style terminé, créons le composant de la barre latérale pour nous aider à naviguer vers différentes pages du tableau de bord de notre application. Ouvrez le fichier components/SideBar.js et collez l'extrait de code ci-dessous.

import Link from 'next/link'
import { useRouter } from 'next/router'
import styles from '../styles/SideBar.module.css'

const SideBar = () => {

    const router = useRouter()

    const logout = async () => {

        try {

            const response = await fetch('/api/logout', {
                method: 'GET', 
                credentials: 'same-origin', 
            });

            if(response.status === 200)  router.push('/')

        } catch (e) {
            alert(e)
        }
  
    }
      

    return (
        <nav className={styles.sidebar}>

            <ul>

                <li> <Link href="/dashboard"> Dashboard</Link> </li>

                <li> <Link href="/dashboard/attendance"> Attendance </Link> </li>

                <li> <Link href="/dashboard/attendance-sheet"> Attendance Sheet </Link> </li>

                <li onClick={logout}> Logout </li>

            </ul>

        </nav>
    )

}

export default SideBar

Page de connexion

Ouvrez maintenant le fichier page/index.js, supprimez tout le code qu'il contient et ajoutez l'extrait de code suivant. Le code ci-dessous envoie une demande de publication avec l'e-mail et le mot de passe fournis via le formulaire à la route localhost:3000/api/login. Une fois les informations d'identification validées, il appelle la router.push('/dashboard')méthode qui redirige l'utilisateur vers localhost:3000/api/dashboard :

import Head from 'next/head'
import { postData } from '../lib/request';
import styles from '../styles/Home.module.css'
import { useState } from 'react';
import { useRouter } from 'next/router'

export default function Home({posts}) {

  const [data, setData] = useState({email: null, password: null});

  const router = useRouter()

  const submit = (e) => {
    e.preventDefault()

    if(data.email && data.password) {
      postData('/api/login', data).then(data => {
        console.log(data); 

        if (data.status === "success") router.push('/dashboard')
      
      });
    }

  }

  return (
    <div className={styles.container}>
      <Head>
        <title>Login</title>
        <meta name="description" content="Login" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>

        <form  className={styles.login}>

          <input 
            type={"text"} 
            placeholder="Enter Your Email" 
            onChange={(e) => setData({...data, email: e.target.value})} />

          <input 
            type={"password"}  
            placeholder="Enter Your Password"
            onChange={(e) => setData({...data, password: e.target.value})} />

          <button onClick={submit}>Login</button>

        </form>
        
      </main>

    </div>
  )
}

Configuration de la route de l'API de connexion

Ouvrez maintenant le fichier page/api/login.js et ajoutez l'extrait de code suivant. Nous utiliserons PrismaClientpour effectuer nos requêtes de base de données, et withIronSessionApiRouteest la fonction iron-session pour gérer les sessions utilisateur dans les applications RESTful.

Cette route gère la requête POST de connexion vers localhost:3000/api/login et génère des cookies d'authentification une fois l'utilisateur authentifié.

import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { parseBody } from '../../lib/parseBody';
import { sessionCookie } from '../../lib/session';

export default withIronSessionApiRoute(
    async function loginRoute(req, res) {

      const { email, password } = parseBody(req.body)

      const prisma = new PrismaClient()

      // By unique identifier
      const user = await prisma.user.findUnique({
        where: {
        email
      },})

      if(user.password === password) {

        // get user from database then:
        user.password = undefined
        req.session.user = user
        await req.session.save();

        return res.send({ status: 'success', data: user });

      };

    res.send({ status: 'error', message: "incorrect email or password" });

  },
  sessionCookie(),
);

Configuration de la route de l'API de déconnexion

Ouvrez le fichier /page/api/logout et ajoutez l'extrait de code ci-dessous. Cette route gère les requêtes GET vers localhost:3000/api/logout qui déconnecte les utilisateurs en détruisant les cookies de session.

import { withIronSessionApiRoute } from "iron-session/next";
import { sessionCookie } from "../../lib/session";

export default withIronSessionApiRoute(
  function logoutRoute(req, res, session) {
    req.session.destroy();
    res.send({ status: "success" });
  },
  sessionCookie()
);

Création de la page du tableau de bord

Cette page fournit une interface permettant aux utilisateurs de se connecter et de se déconnecter de la feuille de présence. Les administrateurs peuvent également créer une feuille de présence. Ouvrez le fichier page/dashboard/index.js et ajoutez l'extrait de code ci-dessous.

import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { useState, useCallback } from "react";
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";
import { postData } from "../../lib/request";

export default function Page(props) {

  const [attendanceSheet, setState] = useState(JSON.parse(props.attendanceSheet));

  const sign = useCallback((action="") => {

    const body = {
      attendanceSheetId: attendanceSheet[0]?.id,
      action
    }

    postData("/api/sign-attendance", body).then(data => {

      if (data.status === "success") {

        setState(prevState => {

          const newState = [...prevState]

          newState[0].attendance[0] = data.data

          return newState

        })
     
      }

    })

  }, [attendanceSheet])

  const createAttendance = useCallback(() => {

    postData("/api/create-attendance").then(data => {

      if (data.status === "success") {
        alert("New Attendance Sheet Created")
        setState([{...data.data, attendance:[]}])
      }

    })

  }, [])

  return (
    <div>

      <Head>
        <title>Attendance Management Dashboard</title>
        <meta name="description" content="dashboard" />
      </Head>

      <div className={styles.navbar}></div>

      <main className={styles.dashboard}>

        <SideBar />

        <div className={dashboard.users}>

          {
            props.isAdmin && <button className={dashboard.create} onClick={createAttendance}>Create Attendance Sheet</button>
          }
            
          { attendanceSheet.length > 0 &&

            <table className={dashboard.table}>
              <thead>
                <tr> 
                  <th>Id</th> <th>Created At</th> <th>Sign In</th> <th>Sign Out</th> 
                </tr>
              </thead>

              <tbody>
                <tr>
                  <td>{attendanceSheet[0]?.id}</td>
                  <td>{attendanceSheet[0]?.createdAt}</td>

                  {
                    attendanceSheet[0]?.attendance.length != 0 ? 
                      <>
                        <td>{attendanceSheet[0]?.attendance[0]?.signInTime}</td>
                        <td>{
                          attendanceSheet[0]?.attendance[0]?.signOut ? 
                          attendanceSheet[0]?.attendance[0]?.signOutTime: <button onClick={() => sign("sign-out")}> Sign Out </button> }</td>
                      </>
                      :
                      <>
                        <td> <button onClick={() => sign()}> Sign In </button> </td>
                        <td>{""}</td>
                      </>
                  }
                </tr>
              </tbody>

            </table>

          }
          
        </div>

      </main>

    </div>
  )
}

Nous utilisons le getServerSidePropspour générer les données de la page, et withIronSessionSsrest la fonction iron-session pour travailler avec les pages rendues côté serveur. Dans l'extrait de code suivant, nous recherchons la dernière ligne de la table presenceSheet avec une ligne de la table de présence, où userIdest égal à l'ID utilisateur stocké sur la session utilisateur. Nous vérifions également si l'utilisateur est un ADMIN.

export const getServerSideProps = withIronSessionSsr( async ({req}) => {

  const user = req.session.user

  const prisma = new PrismaClient()

  const attendanceSheet = await prisma.attendanceSheet.findMany({  
    take: 1,
    orderBy: {
      id: 'desc',
    },
    include: { 
      attendance: {
        where: {
          userId: user.id
        },
      }
    }
  })

  return {
    props: {
      attendanceSheet: JSON.stringify(attendanceSheet),
      isAdmin: user.role === "ADMIN"
    }
  }

}, sessionCookie())

Configuration de la route de l'API de création de présence

Ouvrez le fichier page/api/create-attendance.js et ajoutez l'extrait de code ci-dessous.

import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionCookie } from '../../lib/session';

  
export default withIronSessionApiRoute( async function handler(req, res) {

    const prisma = new PrismaClient()

    const user = req.session.user

    const attendanceSheet = await prisma.attendanceSheet.create({
        data: {
          userId: user.id,
        },
    })

    res.json({status: "success", data: attendanceSheet});
    
}, sessionCookie())

Configuration de la route de l'API Sign Attendance

Cette route gère notre requête API POST à ​​localhost:3000/api/sign-attendance. La route accepte la requête POST, tandis que attendanceSheetIdet actionsont utilisés pour se connecter et se déconnecter de attendanceSheet.

Ouvrez le fichier /page/api/sign-attendance.js et ajoutez l'extrait de code ci-dessous.

import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { parseBody } from '../../lib/parseBody';
import { sessionCookie } from '../../lib/session';

  
export default withIronSessionApiRoute( async function handler(req, res) {

    const prisma = new PrismaClient()

    const {attendanceSheetId, action} = parseBody(req.body)

    const user = req.session.user

    const attendance = await prisma.attendance.findMany({
        where: {
            userId: user.id,
            attendanceSheetId: attendanceSheetId
        }
    })

    //check if atendance have been created
    if (attendance.length === 0) {
        const attendance = await prisma.attendance.create({
            data: {
                userId: user.id,
                attendanceSheetId: attendanceSheetId,
                signIn: true,
                signOut: false,
                signOutTime: new Date()
            },
        })   

        return res.json({status: "success", data: attendance});

    } else if (action === "sign-out") {
        await prisma.attendance.updateMany({
            where: {
                userId: user.id,
                attendanceSheetId: attendanceSheetId
            },
            data: {
              signOut: true,
              signOutTime: new Date()
            },
        })

        return res.json({status: "success", data: { ...attendance[0], signOut: true, signOutTime: new Date()}});
    }

    res.json({status: "success", data: attendance});
    
}, sessionCookie())

Création de la page de présence

Cette page rendue côté serveur affiche toutes les feuilles de présence d'un utilisateur connecté. Ouvrez le fichier /page/dashboard/attendance.js et ajoutez l'extrait de code ci-dessous.

import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";

export default function Page(props) {

  const data = JSON.parse(props.attendanceSheet)

  return (
    <div>

      <Head>
        <title>Attendance Management Dashboard</title>
        <meta name="description" content="dashboard" />
      </Head>

      <div className={styles.navbar}></div>

      <main className={styles.dashboard}>

        <SideBar />

        <div className={dashboard.users}>

        <table className={dashboard.table}>

          <thead>

            <tr> 
              <th> Attendance Id</th> <th>Date</th> 
              <th>Sign In Time</th> <th>Sign Out Time</th> 
            </tr> 

          </thead>

            <tbody>

              {
                data.map(data =>   {

                  const {id, createdAt, attendance } = data

  
                  return (
                    <tr key={id}> 

                      <td>{id}</td> <td>{createdAt}</td>  

                      { attendance.length === 0 ? 
                      
                        (
                          <>
                            <td>You did not Sign In</td>
                            <td>You did not Sign Out</td>
                          </>
                        )
                        :
                        (
                          <>
                            <td>{attendance[0]?.signInTime}</td>
                            <td>{attendance[0]?.signOut ? attendance[0]?.signOutTime : "You did not Sign Out"}</td>
                          </>
                        )
                        
                      }
              
                    </tr>
                  )

                })

              }  

            </tbody>

          </table>

        </div>

      </main>

    </div>
  )
}

Dans l'extrait de code ci-dessous, nous interrogeons toutes les lignes de la attendanceSheettable et récupérons également la présence où userIdest égal à l'ID utilisateur stocké dans la session utilisateur.

export const getServerSideProps = withIronSessionSsr( async ({req}) => {

  const user = req.session.user

  const prisma = new PrismaClient()
  
  const attendanceSheet = await prisma.attendanceSheet.findMany({
    orderBy: {
      id: 'desc',
    },
    include: { 
      attendance: {
        where: {
          userId: user.id
        },
      }
    }
  })

  return {
    props: {
      attendanceSheet: JSON.stringify(attendanceSheet),
    }
  }

}, sessionCookie())

Création de la page Feuille de présence

Cette page rendue côté serveur affiche toutes les feuilles de présence et les employés qui se sont connectés à cette feuille de présence. Ouvrez le fichier /page/dashboard/attendance.js et ajoutez l'extrait de code ci-dessous.

import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";

export default function Page(props) {

  const data = JSON.parse(props.attendanceSheet)

  return (
    <div>

      <Head>
        <title>Attendance Management Dashboard</title>
        <meta name="description" content="dashboard" />
      </Head>

      <div className={styles.navbar}></div>

      <main className={styles.dashboard}>

        <SideBar />

        <div className={dashboard.users}>

        {
          data?.map(data => {

            const {id, createdAt, attendance } = data

            return (
              <>

                <table key={data.id} className={dashboard.table}>

                  <thead>
                    
                    <tr> 
                      <th> Attendance Id</th> <th>Date</th> 
                      <th> Name </th> <th> Email </th> <th> Role </th>
                      <th>Sign In Time</th> <th>Sign Out Time</th> 
                    </tr> 

                  </thead>

                  <tbody>

                    {
                      (attendance.length === 0)  &&
                      (
                        <>
                        <tr><td> {id} </td> <td>{createdAt}</td> <td colSpan={5}> No User signed this sheet</td></tr>
                        </>
                      )
                    }

                    {
                      attendance.map(data => {

                        const {name, email, role} = data.user

                      
                        return (
                          <tr key={id}> 

                            <td>{id}</td> <td>{createdAt}</td>  

                            <td>{name}</td> <td>{email}</td>

                            <td>{role}</td>

                            <td>{data.signInTime}</td>

                            <td>{data.signOut ? attendance[0]?.signOutTime: "User did not Sign Out"}</td>  
                    
                          </tr>
                        )

                      })

                    }  

                  </tbody>
                  
                </table>
              </>
            )
          })
          
          }

        </div>

      </main>

    </div>
  )
}

Dans l'extrait de code ci-dessous, nous interrogeons toutes les lignes des attendanceSheettables et récupérons également la participation en sélectionnant le nom, l'e-mail et le rôle.

export const getServerSideProps = withIronSessionSsr(async () => {

  const prisma = new PrismaClient()

  const attendanceSheet = await prisma.attendanceSheet.findMany({
    orderBy: {
      id: 'desc',
    },
    include: { 
      attendance: {
        include: { 
          user: {
            select: {
              name: true, 
              email: true, 
              role: true
            }
          }
        }
      },
    },

  })

  return {
    props: {
      attendanceSheet: JSON.stringify(attendanceSheet),
    }
  }

}, sessionCookie())

Tester l'application

Tout d'abord, nous devons ajouter des utilisateurs à notre base de données. Nous allons le faire avec Prisma Studio. Pour démarrer Prisma studio, exécutez la commande ci-dessous :

npx prisma studio

La page d'index de Prisma ressemble à ceci :

La page d'index de Prisma


Pour créer un utilisateur de base de données avec un rôle ADMIN et plusieurs utilisateurs avec un rôle EMPLOYEE, rendez-vous sur cette page :

Ajout d'un utilisateur

Cliquez sur Ajouter un enregistrement, puis renseignez les champs obligatoires : mot de passe, nom, email et rôle. Une fois que vous avez terminé, cliquez sur le bouton vert Enregistrer 1 modification. Notez que pour plus de simplicité, nous n'avons pas haché le mot de passe.

Démarrez le serveur avec yarn dev. Cela démarre le serveur et exécute l'application sur [localhost:3000](<http://localhost:3000>) la page de connexion est affichée ci-dessous.

La page de connexion

Connectez-vous avec un utilisateur ayant un rôle ADMIN car seuls les utilisateurs administratifs peuvent créer des feuilles de présence. Une fois la connexion réussie, l'application vous redirigera vers votre tableau de bord.

Cliquez sur le bouton Créer une feuille de présence pour créer une feuille de présence, puis attendez que la demande se termine et la feuille de présence apparaîtra. Le tableau de bord de l'utilisateur est illustré ci-dessous.

Création d'une feuille de présence

La feuille de présence est affichée ci-dessous, cliquez sur le bouton Connexion pour vous connecter. Une fois la connexion réussie, l'heure de connexion sera affichée et le bouton Déconnexion sera visible. Cliquez sur le bouton Se déconnecter pour vous déconnecter et répétez ce processus plusieurs fois avec différents utilisateurs.

Connexion réussie

Cliquez ensuite sur le lien Présence dans la barre latérale pour afficher la présence des utilisateurs. Les résultats doivent correspondre à ceux indiqués ci-dessous :

la page de présence

Cliquez ensuite sur le lien Feuille de présence dans la barre latérale pour afficher la présence de tous les utilisateurs. Les résultats sont présentés ci-dessous :

La page Feuille de présence

Conclusion

Dans cet article, vous avez appris à utiliser un serveur Fastify personnalisé avec Next.js. Vous avez également découvert Prisma et le studio Prisma. Je vous ai expliqué comment connecter Prisma à une base de données Postgres et comment créer, lire et mettre à jour la base de données à l'aide du client Prisma et du studio Prisma.

Lien : https://arctype.com/blog/fullstack-nextjs-postgres-fastify/

#fullstack #nextjs #postgre #fastify

Comment Créer Une Application Complète Avec Next.js, Prisma, Postgres
Marisol  Kuhic

Marisol Kuhic

1656118800

A Complete Template Into React, Postgres and Various Web3 Integrations

WWW-REACT-POSTGRES

www-react-postgres

What is this for?

This template is for

  • making a React website
  • making a React web application with a database

If you are a beginner and you just want to make a simple React project with no database, try next-sass.

Why would I use this?

You want to...

  • use React.
  • use SASS, like the good old days.
  • use https://nextjs.org/ and dotenv for things like server side rendering and obfuscating secrets on a server.
    • You should never expose client secrets in the browser.
  • use Postgres 14 (latest as of June 19th, 2022) to manage local data or local authentication.
  • have templated SEO metatags.
  • get the minimum code involved to make a production website
  • [OPTIONAL] start with a Google Authentication example to create.
    • start with an example of "organizations", each organization is created with an e-mail's domain name.
  • [OPTIONAL] authenticate your Ethereum addresses from Metamask to build a DAPP or DAO. This example keeps a table of Ethereum addresses where you can store local information in the jsonb column.
    • You'll need your own strategy for joining your Ethereum address to your local account.
  • [OPTIONAL] authenticate your Solana address (public key) from Phantom to build a DAPP or DAO. This example keeps a table of Solana addresses where you can store local information in the jsonb column.
    • You'll need your own strategy for joining your Solana address to your local account.

Setup (MacOS)

All steps assume you have

  • installed Homebrew
  • installed iTerm, because you will need multiple terminal windows open.

Step 1

Clone this repository!

Step 2

Create an .env file in your project root.

JWT_SECRET=74b8b454-29a6-4282-bdec-7e2895c835eb
SERVICE_PASSWORD_SALT=\$2b\$10\$JBb8nz6IIrIXKeySeuY3aO
PASSWORD_ROUNDS=10
  • Generate your own SERVICE_PASSWORD_SALT with BCrypt.genSaltSync(10).
    • You need to use \ to escape the $ values as shown above. Also make sure you're using the correct amount of rounds.
  • Generate your own JWT_SECRET.

[OPTIONAL] Step 3

To get google auth support to work, add the following to your .env file in your project root directory.

GOOGLE_CLIENT_ID=GET_ME_FROM_GOOGLE
GOOGLE_CLIENT_SECRET=GET_ME_FROM_GOOGLE
GOOGLE_REDIRECT_URIS=http://localhost:3005/google-authentication
  • Obtain GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET from https://console.developers.google.com after you setup your application.
  • Enable People API. Otherwise Google Auth will not work for this example.
  • Use CMD+F to find GOOGLE_REDIRECT_URIS in @data/environment. Google needs this string for the Authorized redirect URIs setting. The default is: http://localhost:3005/google-authentication.

Step 4

Install Postgres 14 locally

brew uninstall postgresql
brew install postgresql
brew link postgresql --force

At the time of writing this (June 19th, 2022), postgresql is version 14.

If you see

# already linked, don't worry, nothing to worry about

Warning: Already linked: /usr/local/Cellar/postgresql/14.4
To relink, run:
  brew unlink postgresql && brew link postgresql

# run brew postgresql-upgrade-database

Postgres - FATAL: database files are incompatible with server

Everything is fine.

Next make sure NodeJS version 10+ is installed on your machine.

brew install node

Install dependencies

npm install
npm run dev

Step 5

Run Postgres 14.

I prefer the option to start and stop postgres through the command line.

  • If you have another way of doing things, just make sure your port is set to 1334.

In a seperate terminal tab run

postgres -D /usr/local/var/postgres -p 1334

Now your development environment is setup.

Step 5

You need to create a user named admin and database named wwwdb.

# Enter Postgres console
psql postgres -p 1334

# Create a new user for yourself
CREATE ROLE admin WITH LOGIN PASSWORD 'oblivion';

# Allow yourself to create databases
ALTER ROLE admin CREATEDB;

# You need to do this to install uuid-ossp in a later step
ALTER USER admin WITH SUPERUSER;

# Exit Postgres console
\q

# Log in as your new user.
psql postgres -p 1334 -U admin

# Create a database named: nptdb.
# If you change this, update knexfile.js
CREATE DATABASE wwwdb;

# Give your self privileges
GRANT ALL PRIVILEGES ON DATABASE wwwdb TO admin;

# List all of your databases
\list

# Connect to your newly created DB as a test
\connect wwwdb

# Exit Postgres console
\q

Step 6

Setup and install the necessary Postgres plugins. Aftewards seed the database with the necessary tables.

npm run script database-setup
npm run script database-seed

There is also npm run script database-drop if you just want to drop your tables for testing.

[OPTIONAL] Step 7

If you need to run a node script without running the node server, an example is provided for your convenience

npm run script example

Finish

View http://localhost:3005 in your browser. You should be able to use the full example end-to-end and modify the code however you like.

Production deployment

You will need to add production environment variables. If you set up your Postgres database on Render the values will look something like this

PRODUCTION_DATABASE_PORT=5432
PRODUCTION_DATABASE_HOST=oregon-postgres.render.com
PRODUCTION_DATABASE_NAME=yourdatabasename
PRODUCTION_DATABASE_USERNAME=yourdatabasename_user
PRODUCTION_DATABASE_PASSWORD=XXXXXXXXXXXXXXXXXXXXX

Then you will need to run production scripts

npm run production-script database-setup
npm run production-script database-seed

For deploying your new website, I recommend any of the following choices:

Questions?

Contact @wwwjim.


Author: jimmylee
Source code: https://github.com/jimmylee/www-react-postgres
License:

#react-native #react #postgre 

A Complete Template Into React, Postgres and Various Web3 Integrations

EFコアとPostgresを使用した.NETコアのRESTfulAPI

REST API は、サーバーと通信するために複数のクライアント(またはAPPS)が使用できるアプリケーションプログラミングインターフェイスです。

Rest APIは、アプリケーションに必要なデータを便利な形式(JSONやXMLなど)で保存および取得する一種のWebサービスです。

ステートレスであるため、Webサービスにアクセスするために依存するコードライブラリを必要としないため、開発者に大きな柔軟性を提供します。

RESTでサポートされている多くのプロトコルの中で、最も一般的なものはHTTPです。

HTTPRequestを使用してクライアントから要求が送信されると、対応する応答がHTTPResponseを使用してサーバーから送信されます。リクエストとレスポンスでサポートされている最も広く使用されている機械可読形式は、JSON(Javascript Object Notification)とXML(Extensible Markup Language)です。

RESTは、コンピューター科学者のROYFIELDINGによって作成されました。

REST APIを使用して、さまざまなアクションを実行できます。アクションに基づいて、関連する方法を使用する必要があります。以下は、RESTでサポートされている5つの方法です。

  1. GET-このメソッドは、データベース/サーバーからデータを取得するために使用されます。
  2. POST-このメソッドは、新しいレコードを作成するために使用されます。
  3. PUT-このメソッドは、レコードを変更/置換するために使用されます。レコード全体を置き換えます。
  4. PATCH-このメソッドは、レコードを変更/更新するために使用されます。レコードの一部を置き換えます。
  5. DELETE-このメソッドは、レコードを削除するために使用されます。

EfCoreとPostgresを使用したdotnetCoreのRESTfulApi

これを例で見てみましょう。私たちは、母親が十分な休息をとることが決してないことを知っています。しかし、これらの休むことのないママを例として取り上げ、RestAPIをどのように使用するかを見てみましょう。:)

生まれたばかりの赤ちゃんによる過度の要求の中で、おむつはリーダーボードで最初に位置します。

母親は赤ちゃんのためにすべてが最善であることを望んでいます。ですから、母親が赤ちゃんに最適なおむつを選びたいと思うのは明らかです。そこで、彼女はショッピングWebサイト(フリップカートを想定)にアクセスして、おむつを検索します。これにより、すべてのおむつのリストを取得するために、フリップカートのサーバーにHTTPリクエストが送信されます。FlipkartのサーバーはHTTP応答で応答します。これは、いくつかの基本的な詳細を含むおむつのリストを含むJSONオブジェクト(想定)になります。FlipkartのWebサイトは、この応答を読み取り、人間が読める形式に変換して、母親が見ることができるようにWebページに表示します。

彼女が生まれたばかりの赤ちゃんのために特定のおむつを選び、それを彼女のリストに追加した後。これにより、POSTリクエストが作成され、おむつのブランド、サイズ、数量、価格などを含む新しいレコードがフリップカートのデータベースに作成されます。

彼女の赤ちゃんは成長を続け、すぐに新生児のサイズを超えます。母親がまだおむつのブランドを気に入っていて、サイズを大きくしたいとします。彼女がしなければならないのは、新しいおむつのサイズを選択することだけです。彼女がおむつのサイズを新生児のサイズからサイズ1に更新すると、これによりPATCHメソッドがトリガーされ、他のすべては同じままで、おむつのサイズのみが変更されます。

母親が現在のブランドを変更し、別のブランドに切り替えることを決定することは非常に一般的です。ここで、母親はPUTリクエストを開始します。ここで、以前に選択されたブランドを含むデータ全体が変更され、新しく選択されたブランドに対応するデータに置き換えられます。

最後に、いくつかのGET、POST、PUT、およびPATCHを含む一連の実験の後、母親が子供をトイレトレーニングする時が来ました。彼女が子供を訓練することに成功した場合、おむつはもはや必要ありません。これにより、 DELETE要求がトリガーされます。

前提条件

  1. Visual Studio 2022
  2. .Net Core 6:ASP.NET Coreは、Microsoftによって開発されたASP.NETの新しいバージョンです。これは、WebアプリケーションとAPIを開発するためのオープンソースフレームワークであり、Windows、Mac、またはLinuxで実行できます。Asp.Net Coreは、最新のクラウドベースのインターネット接続アプリケーションを構築するためのクロスプラットフォーム、高性能、オープンソースフレームワークです。
  3. Entity Frameworkコア:Entity Framework(EF)コアは、人気のあるEntityFrameworkデータアクセステクノロジの軽量で拡張可能な オープンソース のクロスプラットフォームバージョンです。
  4. Postgresデータベース:PostgreSQLは強力なオープンソースのオブジェクトリレーショナルデータベースシステムであり、30年以上にわたって活発に開発されており、信頼性、機能の堅牢性、パフォーマンスで高い評価を得ています。

API作成手順

ステップ1

Visual Studio 2022を開き、asp.netコアwebapiプロジェクトを作成します。

ステップ2

NugetからNpgsql.EntityFrameworkCore.PostgreSQLとMicrosoft.EntityFrameworkCore.Toolsをインストールします

ステップ3

Product.csとOrder.csを作成します

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
[Table("product")]
public class Product {
    [Key, Required]
    public int id {
        get;
        set;
    }
    [Required]
    public string ? name {
        get;
        set;
    }
    public string ? brand {
        get;
        set;
    }
    public string ? size {
        get;
        set;
    }
    public decimal price {
        get;
        set;
    }
    public virtual ICollection < Order > orders {
        get;
        set;
    }
}
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
[Table("order")]
public class Order {
    [Key, Required]
    public int id {
        get;
        set;
    }
    public int product_id {
        get;
        set;
    }
    [Required]
    public string ? name {
        get;
        set;
    }
    public string ? address {
        get;
        set;
    }
    public string ? phone {
        get;
        set;
    }
    public DateTime createdon {
        get;
        set;
    }
    public virtual Product product {
        get;
        set;
    }
}

ステップ4

DbContextクラスから継承されたEF_DataContextを作成します 

using Microsoft.EntityFrameworkCore;
public class EF_DataContext: DbContext {
    public EF_DataContext(DbContextOptions < EF_DataContext > options): base(options) {}
    protected override void OnModelCreating(ModelBuilder modelBuilder) {
        modelBuilder.UseSerialColumns();
    }
    public DbSet <Product> Products {
        get;
        set;
    }
    public DbSet <Order> Orders {
        get;
        set;
    }
}

ステップ5

appsetting.jsonを開きます

"ConnectionStrings": {
    "Ef_Postgres_Db": "Server=localhost;Database=shopingpostgres;Port=5432;User Id=postgres;Password=qwerty1234;"
}

ステップ6

Program.csを開く

builder.Services.AddDbContext < EF_DataContext > (o => o.UseNpgsql(builder.Configuration.GetConnectionString("Ef_Postgres_Db")));

ステップ7

2つのコマンドを実行します 

Add-Migration InitialDatabase
Update-Database

ステップ8

API通信に使用されるAPI製品モデルと注文モデルを作成します

public class Product {
    public int id {
        get;
        set;
    }
    public string ? name {
        get;
        set;
    }
    public string ? brand {
        get;
        set;
    }
    public string ? size {
        get;
        set;
    }
    public decimal price {
        get;
        set;
    }
}
public class Order {
    public int id {
        get;
        set;
    }
    public int product_id {
        get;
        set;
    }
    public string ? name {
        get;
        set;
    }
    public string ? address {
        get;
        set;
    }
    public string ? phone {
        get;
        set;
    }
    public DateTime createdon {
        get;
        set;
    }
    public virtual Product product {
        get;
        set;
    }
}

ステップ9

データベースと通信するDBhelperクラスを追加します

using ShoppingWebApi.EfCore;
namespace ShoppingWebApi.Model {
    public class DbHelper {
        private EF_DataContext _context;
        public DbHelper(EF_DataContext context) {
            _context = context;
        }
        /// <summary>
        /// GET
        /// </summary>
        /// <returns></returns>
        public List < ProductModel > GetProducts() {
            List < ProductModel > response = new List < ProductModel > ();
            var dataList = _context.Products.ToList();
            dataList.ForEach(row => response.Add(new ProductModel() {
                brand = row.brand,
                    id = row.id,
                    name = row.name,
                    price = row.price,
                    size = row.size
            }));
            return response;
        }
        public ProductModel GetProductById(int id) {
            ProductModel response = new ProductModel();
            var row = _context.Products.Where(d => d.id.Equals(id)).FirstOrDefault();
            return new ProductModel() {
                brand = row.brand,
                    id = row.id,
                    name = row.name,
                    price = row.price,
                    size = row.size
            };
        }
        /// <summary>
        /// It serves the POST/PUT/PATCH
        /// </summary>
        public void SaveOrder(OrderModel orderModel) {
            Order dbTable = new Order();
            if (orderModel.id > 0) {
                //PUT
                dbTable = _context.Orders.Where(d => d.id.Equals(orderModel.id)).FirstOrDefault();
                if (dbTable != null) {
                    dbTable.phone = orderModel.phone;
                    dbTable.address = orderModel.address;
                }
            } else {
                //POST
                dbTable.phone = orderModel.phone;
                dbTable.address = orderModel.address;
                dbTable.name = orderModel.name;
                dbTable.Product = _context.Products.Where(f => f.id.Equals(orderModel.product_id)).FirstOrDefault();
                _context.Orders.Add(dbTable);
            }
            _context.SaveChanges();
        }
        /// <summary>
        /// DELETE
        /// </summary>
        /// <param name="id"></param>
        public void DeleteOrder(int id) {
            var order = _context.Orders.Where(d => d.id.Equals(id)).FirstOrDefault();
            if (order != null) {
                _context.Orders.Remove(order);
                _context.SaveChanges();
            }
        }
    }
}

ステップ10

Apiコントローラーを作成し、ShoppingRestApiという名前を付けます

using Microsoft.AspNetCore.Mvc;
using ShoppingWebApi.EfCore;
using ShoppingWebApi.Model;
// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
namespace ShoppingWebApi.Controllers {
    [ApiController]
    public class ShoppingApiController: ControllerBase {
        private readonly DbHelper _db;
        public ShoppingApiController(EF_DataContext eF_DataContext) {
            _db = new DbHelper(eF_DataContext);
        }
        // GET: api/<ShoppingApiController>
        [HttpGet]
        [Route("api/[controller]/GetProducts")]
        public IActionResult Get() {
            ResponseType type = ResponseType.Success;
            try {
                IEnumerable < ProductModel > data = _db.GetProducts();
                if (!data.Any()) {
                    type = ResponseType.NotFound;
                }
                return Ok(ResponseHandler.GetAppResponse(type, data));
            } catch (Exception ex) {
                return BadRequest(ResponseHandler.GetExceptionResponse(ex));
            }
        }
        // GET api/<ShoppingApiController>/5
        [HttpGet]
        [Route("api/[controller]/GetProductById/{id}")]
        public IActionResult Get(int id) {
            ResponseType type = ResponseType.Success;
            try {
                ProductModel data = _db.GetProductById(id);
                if (data == null) {
                    type = ResponseType.NotFound;
                }
                return Ok(ResponseHandler.GetAppResponse(type, data));
            } catch (Exception ex) {
                return BadRequest(ResponseHandler.GetExceptionResponse(ex));
            }
        }
        // POST api/<ShoppingApiController>
        [HttpPost]
        [Route("api/[controller]/SaveOrder")]
        public IActionResult Post([FromBody] OrderModel model) {
            try {
                ResponseType type = ResponseType.Success;
                _db.SaveOrder(model);
                return Ok(ResponseHandler.GetAppResponse(type, model));
            } catch (Exception ex) {
                return BadRequest(ResponseHandler.GetExceptionResponse(ex));
            }
        }
        // PUT api/<ShoppingApiController>/5
        [HttpPut]
        [Route("api/[controller]/UpdateOrder")]
        public IActionResult Put([FromBody] OrderModel model) {
            try {
                ResponseType type = ResponseType.Success;
                _db.SaveOrder(model);
                return Ok(ResponseHandler.GetAppResponse(type, model));
            } catch (Exception ex) {
                return BadRequest(ResponseHandler.GetExceptionResponse(ex));
            }
        }
        // DELETE api/<ShoppingApiController>/5
        [HttpDelete]
        [Route("api/[controller]/DeleteOrder/{id}")]
        public IActionResult Delete(int id) {
            try {
                ResponseType type = ResponseType.Success;
                _db.DeleteOrder(id);
                return Ok(ResponseHandler.GetAppResponse(type, "Delete Successfully"));
            } catch (Exception ex) {
                return BadRequest(ResponseHandler.GetExceptionResponse(ex));
            }
        }
    }
}

ステップ11

API応答を処理する応答モデルと応答ハンドラーを追加します

namespace ShoppingWebApi.Model {
    public class ApiResponse {
        public string Code {
            get;
            set;
        }
        public string Message {
            get;
            set;
        }
        public object ? ResponseData {
            get;
            set;
        }
    }
    public enum ResponseType {
        Success,
        NotFound,
        Failure
    }
}

次に、ビデオで示されているように、POSTMANを使用してAPIをテストします。

このストーリーは、もともとhttps://www.c-sharpcorner.com/article/restful-api-in-net-core-using-ef-core-and-postgres/で公開されました

#restful #api #aspdotnet #postgre #efcore 

EFコアとPostgresを使用した.NETコアのRESTfulAPI

API Restful en .NET Core usando EF Core y Postgres

REST API  es una interfaz de programación de aplicaciones que pueden utilizar varios clientes (o APLICACIONES) para comunicarse con un servidor.

Rest API es un tipo de servicio web que almacena y recupera los datos necesarios para su aplicación en un formato conveniente (por ejemplo, JSON o XML).

Proporciona una gran flexibilidad a los desarrolladores, ya que no necesita ninguna biblioteca de código dependiente para acceder a los servicios web, ya que no tiene estado.

Entre los muchos protocolos admitidos por REST, el más común es HTTP .

Cuando se envía una solicitud desde el cliente mediante HTTPRequest , se envía una respuesta correspondiente desde el servidor mediante HTTPResponse . Los formatos legibles por máquina más utilizados y admitidos para solicitudes y respuestas son JSON (Notificación de objetos Javascript) y XML (Lenguaje de marcado extensible).

REST fue creado por el informático ROY FIELDING .

Las API REST se pueden usar para realizar diferentes acciones. En función de las acciones, se debe utilizar el método pertinente. Los siguientes son los 5 métodos compatibles con REST.

  1. GET: este método se utiliza para recuperar datos de la base de datos/servidor.
  2. POST: este método se utiliza para crear un nuevo registro.
  3. PUT - Este método se utiliza para modificar/reemplazar el registro. Reemplaza todo el registro.
  4. PATCH - Este método se utiliza para modificar/actualizar el registro. Reemplaza partes del registro.
  5. ELIMINAR: este método se utiliza para eliminar el registro.

Restful Api en dotnet Core usando Ef Core y Postgres

Veamos esto con un ejemplo. Sabemos que las madres nunca descansan lo suficiente. Pero tomemos estas mamás sin descanso como ejemplo y veamos cómo usan Rest API. :)

Entre las demandas excesivas de un bebé recién nacido, el cambio de pañales ocupa el primer lugar en la tabla de clasificación.

Una madre quiere todo lo mejor para el bebé. Entonces, es obvio que la madre querría elegir el mejor pañal para su bebé. Entonces, ella va a un sitio web de compras (supongamos: flipkart) y busca pañales. Esto enviará una solicitud HTTP al servidor de flipkart para OBTENER la lista de todos los pañales. El servidor de Flipkart responde con una respuesta HTTP que será un objeto JSON (supongamos) que contiene una lista de pañales con algunos detalles básicos. El sitio web de Flipkart lee esta Respuesta, la convierte a un formato legible por humanos y la muestra en la página web para que la madre la vea.

Después, elige un pañal en particular para su bebé recién nacido y lo agrega a su lista. Esto crea una solicitud POST donde se crea un nuevo registro en la base de datos de flipkart que contiene la marca, el tamaño, la cantidad, el precio, etc. del pañal.

Su bebé sigue creciendo y pronto supera el tamaño del recién nacido. Supongamos que a la madre todavía le gusta la marca de pañales y solo quiere aumentar su tamaño, todo lo que tiene que hacer es elegir el nuevo tamaño de pañal. Cuando actualiza el tamaño del pañal del tamaño recién nacido al tamaño 1, se activa un método PATCH donde todo lo demás permanece igual y solo se cambia el tamaño del pañal.

Es muy común que la madre cambie la marca actual y decida cambiar a una alternativa. Aquí, la madre iniciará una solicitud PUT donde todos los datos que contienen la marca elegida previamente se modifican y reemplazan con los datos correspondientes a la marca recién elegida.

Finalmente, después de una serie de experimentos que involucran varios GET, POST, PUT y PATCH, es hora de que la madre enseñe al niño a ir al baño. Si logra entrenar al niño, ya no necesitará los pañales. Esto activa una solicitud DELETE .

PRERREQUISITOS

  1. estudio visual 2022
  2. .Net Core 6: ASP.NET Core es una nueva versión de ASP.NET, desarrollada por Microsoft. Es un marco de código abierto para desarrollar aplicaciones web y API y se puede ejecutar en Windows, Mac o Linux. Asp.Net Core es un marco de código abierto, multiplataforma y de alto rendimiento para crear aplicaciones modernas, basadas en la nube y conectadas a Internet.
  3. Núcleo de Entity Framework: Entity Framework (EF) Core es una versión liviana, extensible,  de código abierto  y multiplataforma de la popular tecnología de acceso a datos de Entity Framework.
  4. Base de datos de Postgres: PostgreSQL es un poderoso sistema de base de datos relacional de objetos de código abierto con más de 30 años de desarrollo activo que le ha valido una sólida reputación por su confiabilidad, robustez de funciones y rendimiento.

PASOS DE CREACIÓN DE API

Paso 1

Abra Visual Studio 2022 y cree el proyecto asp.net core webapi.

Paso 2

Instale Npgsql.EntityFrameworkCore.PostgreSQL y Microsoft.EntityFrameworkCore.Tools desde Nuget

Paso 3

Crear Product.cs y Order.cs

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
[Table("product")]
public class Product {
    [Key, Required]
    public int id {
        get;
        set;
    }
    [Required]
    public string ? name {
        get;
        set;
    }
    public string ? brand {
        get;
        set;
    }
    public string ? size {
        get;
        set;
    }
    public decimal price {
        get;
        set;
    }
    public virtual ICollection < Order > orders {
        get;
        set;
    }
}
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
[Table("order")]
public class Order {
    [Key, Required]
    public int id {
        get;
        set;
    }
    public int product_id {
        get;
        set;
    }
    [Required]
    public string ? name {
        get;
        set;
    }
    public string ? address {
        get;
        set;
    }
    public string ? phone {
        get;
        set;
    }
    public DateTime createdon {
        get;
        set;
    }
    public virtual Product product {
        get;
        set;
    }
}

Paso 4

Crear EF_DataContext heredado de la clase DbContext 

using Microsoft.EntityFrameworkCore;
public class EF_DataContext: DbContext {
    public EF_DataContext(DbContextOptions < EF_DataContext > options): base(options) {}
    protected override void OnModelCreating(ModelBuilder modelBuilder) {
        modelBuilder.UseSerialColumns();
    }
    public DbSet <Product> Products {
        get;
        set;
    }
    public DbSet <Order> Orders {
        get;
        set;
    }
}

Paso 5

Abrir appsetting.json

"ConnectionStrings": {
    "Ef_Postgres_Db": "Server=localhost;Database=shopingpostgres;Port=5432;User Id=postgres;Password=qwerty1234;"
}

Paso 6

Abrir programa.cs

builder.Services.AddDbContext < EF_DataContext > (o => o.UseNpgsql(builder.Configuration.GetConnectionString("Ef_Postgres_Db")));

Paso 7

Ejecuta los 2 comandos 

Add-Migration InitialDatabase
Update-Database

Paso 8

Cree los modelos de pedido y producto API que se utilizarán para la comunicación API

public class Product {
    public int id {
        get;
        set;
    }
    public string ? name {
        get;
        set;
    }
    public string ? brand {
        get;
        set;
    }
    public string ? size {
        get;
        set;
    }
    public decimal price {
        get;
        set;
    }
}
public class Order {
    public int id {
        get;
        set;
    }
    public int product_id {
        get;
        set;
    }
    public string ? name {
        get;
        set;
    }
    public string ? address {
        get;
        set;
    }
    public string ? phone {
        get;
        set;
    }
    public DateTime createdon {
        get;
        set;
    }
    public virtual Product product {
        get;
        set;
    }
}

Paso 9

Agregue la clase DBhelper que hablará con su base de datos

using ShoppingWebApi.EfCore;
namespace ShoppingWebApi.Model {
    public class DbHelper {
        private EF_DataContext _context;
        public DbHelper(EF_DataContext context) {
            _context = context;
        }
        /// <summary>
        /// GET
        /// </summary>
        /// <returns></returns>
        public List < ProductModel > GetProducts() {
            List < ProductModel > response = new List < ProductModel > ();
            var dataList = _context.Products.ToList();
            dataList.ForEach(row => response.Add(new ProductModel() {
                brand = row.brand,
                    id = row.id,
                    name = row.name,
                    price = row.price,
                    size = row.size
            }));
            return response;
        }
        public ProductModel GetProductById(int id) {
            ProductModel response = new ProductModel();
            var row = _context.Products.Where(d => d.id.Equals(id)).FirstOrDefault();
            return new ProductModel() {
                brand = row.brand,
                    id = row.id,
                    name = row.name,
                    price = row.price,
                    size = row.size
            };
        }
        /// <summary>
        /// It serves the POST/PUT/PATCH
        /// </summary>
        public void SaveOrder(OrderModel orderModel) {
            Order dbTable = new Order();
            if (orderModel.id > 0) {
                //PUT
                dbTable = _context.Orders.Where(d => d.id.Equals(orderModel.id)).FirstOrDefault();
                if (dbTable != null) {
                    dbTable.phone = orderModel.phone;
                    dbTable.address = orderModel.address;
                }
            } else {
                //POST
                dbTable.phone = orderModel.phone;
                dbTable.address = orderModel.address;
                dbTable.name = orderModel.name;
                dbTable.Product = _context.Products.Where(f => f.id.Equals(orderModel.product_id)).FirstOrDefault();
                _context.Orders.Add(dbTable);
            }
            _context.SaveChanges();
        }
        /// <summary>
        /// DELETE
        /// </summary>
        /// <param name="id"></param>
        public void DeleteOrder(int id) {
            var order = _context.Orders.Where(d => d.id.Equals(id)).FirstOrDefault();
            if (order != null) {
                _context.Orders.Remove(order);
                _context.SaveChanges();
            }
        }
    }
}

Paso 10

Cree su controlador Api, asígnele el nombre ShoppingRestApi

using Microsoft.AspNetCore.Mvc;
using ShoppingWebApi.EfCore;
using ShoppingWebApi.Model;
// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
namespace ShoppingWebApi.Controllers {
    [ApiController]
    public class ShoppingApiController: ControllerBase {
        private readonly DbHelper _db;
        public ShoppingApiController(EF_DataContext eF_DataContext) {
            _db = new DbHelper(eF_DataContext);
        }
        // GET: api/<ShoppingApiController>
        [HttpGet]
        [Route("api/[controller]/GetProducts")]
        public IActionResult Get() {
            ResponseType type = ResponseType.Success;
            try {
                IEnumerable < ProductModel > data = _db.GetProducts();
                if (!data.Any()) {
                    type = ResponseType.NotFound;
                }
                return Ok(ResponseHandler.GetAppResponse(type, data));
            } catch (Exception ex) {
                return BadRequest(ResponseHandler.GetExceptionResponse(ex));
            }
        }
        // GET api/<ShoppingApiController>/5
        [HttpGet]
        [Route("api/[controller]/GetProductById/{id}")]
        public IActionResult Get(int id) {
            ResponseType type = ResponseType.Success;
            try {
                ProductModel data = _db.GetProductById(id);
                if (data == null) {
                    type = ResponseType.NotFound;
                }
                return Ok(ResponseHandler.GetAppResponse(type, data));
            } catch (Exception ex) {
                return BadRequest(ResponseHandler.GetExceptionResponse(ex));
            }
        }
        // POST api/<ShoppingApiController>
        [HttpPost]
        [Route("api/[controller]/SaveOrder")]
        public IActionResult Post([FromBody] OrderModel model) {
            try {
                ResponseType type = ResponseType.Success;
                _db.SaveOrder(model);
                return Ok(ResponseHandler.GetAppResponse(type, model));
            } catch (Exception ex) {
                return BadRequest(ResponseHandler.GetExceptionResponse(ex));
            }
        }
        // PUT api/<ShoppingApiController>/5
        [HttpPut]
        [Route("api/[controller]/UpdateOrder")]
        public IActionResult Put([FromBody] OrderModel model) {
            try {
                ResponseType type = ResponseType.Success;
                _db.SaveOrder(model);
                return Ok(ResponseHandler.GetAppResponse(type, model));
            } catch (Exception ex) {
                return BadRequest(ResponseHandler.GetExceptionResponse(ex));
            }
        }
        // DELETE api/<ShoppingApiController>/5
        [HttpDelete]
        [Route("api/[controller]/DeleteOrder/{id}")]
        public IActionResult Delete(int id) {
            try {
                ResponseType type = ResponseType.Success;
                _db.DeleteOrder(id);
                return Ok(ResponseHandler.GetAppResponse(type, "Delete Successfully"));
            } catch (Exception ex) {
                return BadRequest(ResponseHandler.GetExceptionResponse(ex));
            }
        }
    }
}

Paso 11

Agregue el modelo de respuesta y el controlador de respuesta que manejará sus respuestas API

namespace ShoppingWebApi.Model {
    public class ApiResponse {
        public string Code {
            get;
            set;
        }
        public string Message {
            get;
            set;
        }
        public object ? ResponseData {
            get;
            set;
        }
    }
    public enum ResponseType {
        Success,
        NotFound,
        Failure
    }
}

Ahora pruebe las API usando POSTMAN como se muestra en el video.

Esta historia se publicó originalmente en https://www.c-sharpcorner.com/article/restful-api-in-net-core-using-ef-core-and-postgres/

#restful #api #aspdotnet #postgre #efcore 

API Restful en .NET Core usando EF Core y Postgres

ライブマテリアライズドビューでMySQLとPostgresに参加する方法

多くのマイクロサービスで構成されるプロジェクトで作業している場合、複数のデータベースも含まれる可能性があります。

たとえば、MySQLデータベースPostgreSQLデータベースがあり、どちらも別々のサーバーで実行されているとします。

通常、2つのデータベースのデータを結合するには、データを結合する新しいマイクロサービスを導入する必要があります。しかし、これはシステムの複雑さを増します。

このチュートリアルでは、マテリアライズを使用して、ライブのマテリアライズドビューでMySQLとPostgresを結合します。その後、それを直接クエリし、標準SQLを使用して両方のデータベースからリアルタイムで結果を取得できるようになります。

マテリアライズは、Rustで記述されたソース利用可能なストリーミングデータベースであり、データの変更時にSQLクエリ(マテリアライズドビュー)の結果をメモリに保持します。

チュートリアルには、の使用を開始できるデモプロジェクトが含まれていますdocker-compose

使用するデモプロジェクトでは、模擬Webサイトで注文を監視します。後でカートが長期間放棄されたときに通知を送信するために使用できるイベントを生成します。

デモプロジェクトのアーキテクチャは次のとおりです。

mz-abandoned-cart-demo

前提条件


デモで使用するすべてのサービスはDockerコンテナー内で実行されるため、DockerとDockerComposeではなくラップトップやサーバーに追加のサービスをインストールする必要はありません。

DockerとDockerComposeがまだインストールされていない場合は、ここでその方法に関する公式の指示に従うことができます。

概要

上の図に示すように、次のコンポーネントがあります。

  • 継続的に注文を生成するための模擬サービス。
  • 注文はMySQLデータベースに保存されます。
  • データベースへの書き込みが発生すると、DebeziumはMySQLからRedpandaトピックに変更をストリーミングします。
  • また、ユーザーを取得できるPostgresデータベースもあります。
  • 次に、このRedpandaトピックをPostgresデータベースのユーザーと一緒に直接Materializeに取り込みます。
  • マテリアライズでは、注文とユーザーを結合し、フィルタリングを実行して、放棄されたカート情報を表示するマテリアライズドビューを作成します。
  • 次に、放棄されたカートデータを新しいRedpandaトピックに送信するためのシンクを作成します。
  • 最後に、Metabaseを使用してデータを視覚化します。
  • 後で、その新しいトピックの情報を使用して、ユーザーに通知を送信し、カートが放棄されたことをユーザーに通知することができます。

ここでの補足として、レッサーパンダの代わりにカフカを使用してもまったく問題ありません。すべてのKafkaコンポーネントの代わりに単一のRedpandaインスタンスを実行できるため、Redpandaがテーブルにもたらすシンプルさが気に入っています。

デモを実行する方法

まず、リポジトリのクローンを作成することから始めます。

git clone https://github.com/bobbyiliev/materialize-tutorials.git

その後、ディレクトリにアクセスできます。

cd materialize-tutorials/mz-join-mysql-and-postgresql

まず、Redpandaコンテナを実行することから始めましょう。

docker-compose up -d redpanda

イメージを作成します。

docker-compose build

最後に、すべてのサービスを開始します。

docker-compose up -d

マテリアライズCLIを起動するには、次のコマンドを実行できます。

docker-compose run mzcli

postgres-clientこれは、プリインストールされたDockerコンテナへのショートカットにすぎません。すでにお持ちの場合は、代わりにpsql実行できますpsql -U materialize -h localhost -p 6875 materialize

マテリアライズカフカソースを作成する方法

ordersマテリアライズCLIを使用しているので、データベース内のテーブルmysql.shopをRedpandaソースとして定義しましょう。

CREATE SOURCE orders
FROM KAFKA BROKER 'redpanda:9092' TOPIC 'mysql.shop.orders'
FORMAT AVRO USING CONFLUENT SCHEMA REGISTRY 'http://redpanda:8081'
ENVELOPE DEBEZIUM;

orders次のステートメントを実行して、ソースから使用可能な列を確認する場合:

SHOW COLUMNS FROM orders;

MaterializeがRedpandaレジストリからメッセージスキーマデータを取得しているため、各属性に使用する列タイプを認識していることがわかります。

    name      | nullable |   type
--------------+----------+-----------
 id           | f        | bigint
 user_id      | t        | bigint
 order_status | t        | integer
 price        | t        | numeric
 created_at   | f        | text
 updated_at   | t        | timestamp

マテリアライズドビューを作成する方法

次に、最初のマテリアライズドビューを作成して、ordersRedpandaソースからすべてのデータを取得します。

CREATE MATERIALIZED VIEW orders_view AS
SELECT * FROM orders;
CREATE MATERIALIZED VIEW abandoned_orders AS
    SELECT
        user_id,
        order_status,
        SUM(price) as revenue,
        COUNT(id) AS total
    FROM orders_view
    WHERE order_status=0
    GROUP BY 1,2;

これで、を使用SELECT * FROM abandoned_orders;して結果を確認できます。

SELECT * FROM abandoned_orders;

マテリアライズド・ビューの作成の詳細については、マテリアライズドキュメンテーションの「マテリアライズド・ビュー」セクションを確認してください。

Postgresソースを作成する方法

MaterializeでPostgresソースを作成する方法は2つあります。

  • MySQLソースで行ったのと同じようにDebeziumを使用します。
  • Postgres Materialize Sourceを使用します。これにより、MaterializeをPostgresに直接接続できるため、Debeziumを使用する必要がありません。

このデモでは、Postgres Materialize Sourceを使用方法のデモンストレーションとして使用しますが、代わりにDebeziumを自由に使用してください。

Postgres Materialize Sourceを作成するには、次のステートメントを実行します。

CREATE MATERIALIZED SOURCE "mz_source" FROM POSTGRES
CONNECTION 'user=postgres port=5432 host=postgres dbname=postgres password=postgres'
PUBLICATION 'mz_source';

上記のステートメントの簡単な要約:

  • MATERIALIZED:PostgreSQLソースのデータを具体化します。すべてのデータはメモリに保持され、ソースを直接選択できるようになります。
  • mz_source:PostgreSQLソースの名前。
  • CONNECTION:PostgreSQL接続パラメータ。
  • PUBLICATION:マテリアライズにストリーミングされるテーブルを含むPostgreSQLパブリケーション。

PostgreSQLソースを作成したら、PostgreSQLテーブルをクエリできるようにするために、アップストリームパブリケーションの元のテーブルを表すビューを作成する必要があります。

この場合、呼び出されるテーブルは1つしかないusersため、実行する必要があるステートメントは次のとおりです。

CREATE VIEWS FROM SOURCE mz_source (users);

使用可能なビューを確認するには、次のステートメントを実行します。

SHOW FULL VIEWS;

それが完了したら、新しいビューを直接クエリできます。

SELECT * FROM users;

次に、先に進んで、さらにいくつかのビューを作成しましょう。

カフカシンクを作成する方法

シンクを使用すると、Materializeから外部ソースにデータを送信できます。

このデモでは、レッサーパンダを使用します。

RedpandaはKafkaAPIと互換性があり、MaterializeはKafkaソースからのデータを処理するのと同じようにRedpandaからのデータを処理できます。

大量の未払い注文をすべて保持するマテリアライズドビューを作成しましょう。

 CREATE MATERIALIZED VIEW high_value_orders AS
      SELECT
        users.id,
        users.email,
        abandoned_orders.revenue,
        abandoned_orders.total
      FROM users
      JOIN abandoned_orders ON abandoned_orders.user_id = users.id
      GROUP BY 1,2,3,4
      HAVING revenue > 2000;

ご覧のとおり、ここではusers、Postgresソースから直接abandond_ordersデータを取り込んでいるビューと、Redpandaトピックからデータを取り込んでいるビューを実際に結合しています。

上記のマテリアライズドビューのデータを送信するシンクを作成しましょう。

CREATE SINK high_value_orders_sink
    FROM high_value_orders
    INTO KAFKA BROKER 'redpanda:9092' TOPIC 'high-value-orders-sink'
    FORMAT AVRO USING
    CONFLUENT SCHEMA REGISTRY 'http://redpanda:8081';

これで、Redpandaコンテナに接続してrpk topic consumeコマンドを使用すると、トピックからレコードを読み取ることができるようになります。

rpkただし、現時点では、 AVRO形式であるため、結果をプレビューすることはできません。レッサーパンダは将来これを実装する可能性が最も高いですが、今のところ、実際にトピックをマテリアライズにストリーミングしてフォーマットを確認することができます。

まず、自動的に生成されたトピックの名前を取得します。

SELECT topic FROM mz_kafka_sinks;

出力:

                              topic
-----------------------------------------------------------------
 high-volume-orders-sink-u12-1637586945-13670686352905873426

トピック名の生成方法の詳細については、こちらのドキュメントをご覧ください。

次に、このレッサーパンダのトピックから新しいマテリアライズドソースを作成します。

CREATE MATERIALIZED SOURCE high_volume_orders_test
FROM KAFKA BROKER 'redpanda:9092' TOPIC ' high-volume-orders-sink-u12-1637586945-13670686352905873426'
FORMAT AVRO USING CONFLUENT SCHEMA REGISTRY 'http://redpanda:8081';

それに応じてトピック名を変更してください!

最後に、この新しいマテリアライズド・ビューを照会します。

SELECT * FROM high_volume_orders_test LIMIT 2;

トピックにデータが含まれるようになったので、他のサービスを接続してデータを使用し、たとえば電子メールやアラートをトリガーすることができます。

メタベースを接続する方法

デモをローカルで実行している場合、またはサーバーでデモを実行している場合は、メタベースインスタンスにアクセスするためにアクセスしてください。次に、手順に従ってメタベースのセットアップを完了します。http://localhost:3030http://your_server_ip:3030

データのソースとして必ずマテリアライズを選択してください。

準備ができたら、標準のPostgreSQLデータベースの場合と同じようにデータを視覚化できます。

デモを停止する方法

すべてのサービスを停止するには、次のコマンドを実行します。

docker-compose down

結論

ご覧のとおり、これはマテリアライズの使用方法の非常に簡単な例です。マテリアライズを使用して、さまざまなソースからデータを取り込み、それをさまざまな宛先にストリーミングできます。

ソース:https ://www.freecodecamp.org/news/how-to-join-mysql-and-postgres-in-a-live-materialized-view/

 #mysql #postgre

ライブマテリアライズドビューでMySQLとPostgresに参加する方法
Saul  Alaniz

Saul Alaniz

1651824660

Cómo Unir MySQL Y Postgres En Una Vista Materializada En Vivo

Cuando trabaja en un proyecto que consta de muchos microservicios, es probable que también incluya varias bases de datos.

Por ejemplo, puede tener una base de datos MySQL y una base de datos PostgreSQL , ambas ejecutándose en servidores separados.

Por lo general, para unir los datos de las dos bases de datos, tendría que introducir un nuevo microservicio que uniría los datos. Pero esto aumentaría la complejidad del sistema.

En este tutorial, usaremos Materialise para unir MySQL y Postgres en una vista materializada en vivo. Entonces podremos consultar eso directamente y obtener resultados de ambas bases de datos en tiempo real usando SQL estándar.

Materialise es una base de datos de transmisión disponible en la fuente escrita en Rust que mantiene los resultados de una consulta SQL (una vista materializada) en la memoria a medida que cambian los datos.

El tutorial incluye un proyecto de demostración que puede comenzar a usar docker-compose.

El proyecto de demostración que vamos a utilizar monitoreará los pedidos en nuestro sitio web simulado. Generará eventos que luego podrían usarse para enviar notificaciones cuando un carrito ha estado abandonado por mucho tiempo.

La arquitectura del proyecto de demostración es la siguiente:

mz-carrito-abandonado-demo

requisitos previos


Todos los servicios que usaremos en la demostración se ejecutarán dentro de los contenedores Docker, de esa manera no tendrá que instalar ningún servicio adicional en su computadora portátil o servidor en lugar de Docker y Docker Compose.

En caso de que no tenga Docker y Docker Compose ya instalados, puede seguir las instrucciones oficiales sobre cómo hacerlo aquí:

Visión de conjunto

Como se muestra en el diagrama anterior, tendremos los siguientes componentes:

  • Un servicio simulado para generar pedidos continuamente.
  • Los pedidos se almacenarán en una base de datos MySQL .
  • A medida que se producen las escrituras en la base de datos, Debezium transmite los cambios desde MySQL a un tema de Redpanda .
  • También tendremos una base de datos de Postgres donde podemos obtener nuestros usuarios.
  • Luego incorporaremos este tema de Redpanda en Materialise directamente junto con los usuarios de la base de datos de Postgres.
  • En Materialise uniremos nuestros pedidos y usuarios, filtraremos y crearemos una vista materializada que muestre la información del carrito abandonado.
  • Luego, crearemos un sumidero para enviar los datos del carrito abandonado a un nuevo tema de Redpanda.
  • Al final usaremos Metabase para visualizar los datos.
  • Más adelante, podría usar la información de ese nuevo tema para enviar notificaciones a sus usuarios y recordarles que tienen un carrito abandonado.

Como nota al margen aquí, estaría perfectamente bien usando Kafka en lugar de Redpanda. Simplemente me gusta la simplicidad que aporta Redpanda, ya que puede ejecutar una sola instancia de Redpanda en lugar de todos los componentes de Kafka.

Cómo ejecutar la demostración

Primero, comience clonando el repositorio:

git clone https://github.com/bobbyiliev/materialize-tutorials.git

Después de eso, puede acceder al directorio:

cd materialize-tutorials/mz-join-mysql-and-postgresql

Empecemos ejecutando primero el contenedor de Redpanda:

docker-compose up -d redpanda

Construye las imágenes:

docker-compose build

Finalmente, inicie todos los servicios:

docker-compose up -d

Para iniciar Materialise CLI, puede ejecutar el siguiente comando:

docker-compose run mzcli

Este es solo un acceso directo a un contenedor Docker con postgres-clientpreinstalado. Si ya lo has hecho, psqlpuedes ejecutarlo psql -U materialize -h localhost -p 6875 materializeen su lugar.

Cómo crear una fuente de Materialise Kafka

Ahora que está en Materialise CLI, definamos las orderstablas en la mysql.shopbase de datos como fuentes de Redpanda:

CREATE SOURCE orders
FROM KAFKA BROKER 'redpanda:9092' TOPIC 'mysql.shop.orders'
FORMAT AVRO USING CONFLUENT SCHEMA REGISTRY 'http://redpanda:8081'
ENVELOPE DEBEZIUM;

Si tuviera que verificar las columnas disponibles de la ordersfuente ejecutando la siguiente declaración:

SHOW COLUMNS FROM orders;

Podrá ver que, dado que Materialise extrae los datos del esquema del mensaje del registro de Redpanda, conoce los tipos de columna que se deben usar para cada atributo:

    name      | nullable |   type
--------------+----------+-----------
 id           | f        | bigint
 user_id      | t        | bigint
 order_status | t        | integer
 price        | t        | numeric
 created_at   | f        | text
 updated_at   | t        | timestamp

Cómo crear vistas materializadas

A continuación, crearemos nuestra primera vista materializada para obtener todos los datos de la ordersfuente de Redpanda:

CREATE MATERIALIZED VIEW orders_view AS
SELECT * FROM orders;
CREATE MATERIALIZED VIEW abandoned_orders AS
    SELECT
        user_id,
        order_status,
        SUM(price) as revenue,
        COUNT(id) AS total
    FROM orders_view
    WHERE order_status=0
    GROUP BY 1,2;

Ahora puede usar SELECT * FROM abandoned_orders;para ver los resultados:

SELECT * FROM abandoned_orders;

Para obtener más información sobre la creación de vistas materializadas, consulte la sección Vistas materializadas de la documentación de Materialise.

Cómo crear una fuente de Postgres

Hay dos formas de crear una fuente de Postgres en Materialise:

  • Usando Debezium tal como lo hicimos con la fuente MySQL.
  • Usando Postgres Materialise Source, que le permite conectar Materialise directamente a Postgres para que no tenga que usar Debezium.

Para esta demostración, usaremos Postgres Materialise Source solo como una demostración de cómo usarlo, pero siéntase libre de usar Debezium en su lugar.

Para crear una fuente Materialise de Postgres, ejecute la siguiente instrucción:

CREATE MATERIALIZED SOURCE "mz_source" FROM POSTGRES
CONNECTION 'user=postgres port=5432 host=postgres dbname=postgres password=postgres'
PUBLICATION 'mz_source';

Un resumen rápido de la declaración anterior:

  • MATERIALIZED: Materializa los datos de la fuente de PostgreSQL. Todos los datos se retienen en la memoria y hace que las fuentes se puedan seleccionar directamente.
  • mz_source: El nombre de la fuente de PostgreSQL.
  • CONNECTION: Los parámetros de conexión de PostgreSQL.
  • PUBLICATION: La publicación de PostgreSQL, que contiene las tablas que se transmitirán a Materialise.

Una vez que hayamos creado la fuente de PostgreSQL, para poder consultar las tablas de PostgreSQL, necesitaríamos crear vistas que representen las tablas originales de la publicación ascendente.

En nuestro caso, solo tenemos una tabla llamada, userspor lo que la declaración que necesitaríamos ejecutar es:

CREATE VIEWS FROM SOURCE mz_source (users);

Para ver las vistas disponibles ejecute la siguiente instrucción:

SHOW FULL VIEWS;

Una vez hecho esto, puede consultar las nuevas vistas directamente:

SELECT * FROM users;

A continuación, avancemos y creemos algunas vistas más.

Cómo crear un lavabo Kafka

Los sumideros le permiten enviar datos desde Materialise a una fuente externa.

Para esta demostración, usaremos Redpanda .

Redpanda es compatible con la API de Kafka y Materialise puede procesar datos de la misma manera que procesaría datos de una fuente de Kafka.

Vamos a crear una vista materializada, que contendrá todos los pedidos pendientes de pago de gran volumen:

 CREATE MATERIALIZED VIEW high_value_orders AS
      SELECT
        users.id,
        users.email,
        abandoned_orders.revenue,
        abandoned_orders.total
      FROM users
      JOIN abandoned_orders ON abandoned_orders.user_id = users.id
      GROUP BY 1,2,3,4
      HAVING revenue > 2000;

Como puede ver, aquí estamos uniendo la usersvista que ingiere los datos directamente desde nuestra fuente de Postgres y la abandond_ordersvista que ingiere los datos del tema de Redpanda, juntos.

Vamos a crear un Sumidero donde enviaremos los datos de la vista materializada anterior:

CREATE SINK high_value_orders_sink
    FROM high_value_orders
    INTO KAFKA BROKER 'redpanda:9092' TOPIC 'high-value-orders-sink'
    FORMAT AVRO USING
    CONFLUENT SCHEMA REGISTRY 'http://redpanda:8081';

Ahora, si se conectara al contenedor de Redpanda y usara el rpk topic consumecomando, podrá leer los registros del tema.

Sin embargo, por el momento, no podremos obtener una vista previa de los resultados rpkporque tiene formato AVRO. Lo más probable es que Redpanda implemente esto en el futuro, pero por el momento, podemos transmitir el tema nuevamente a Materialise para confirmar el formato.

Primero, obtenga el nombre del tema que se ha generado automáticamente:

SELECT topic FROM mz_kafka_sinks;

Producción:

                              topic
-----------------------------------------------------------------
 high-volume-orders-sink-u12-1637586945-13670686352905873426

Para obtener más información sobre cómo se generan los nombres de los temas, consulte la documentación aquí .

Luego crea una nueva fuente materializada a partir de este tema de Redpanda:

CREATE MATERIALIZED SOURCE high_volume_orders_test
FROM KAFKA BROKER 'redpanda:9092' TOPIC ' high-volume-orders-sink-u12-1637586945-13670686352905873426'
FORMAT AVRO USING CONFLUENT SCHEMA REGISTRY 'http://redpanda:8081';

¡Asegúrate de cambiar el nombre del tema en consecuencia!

Finalmente, consulta esta nueva vista materializada:

SELECT * FROM high_volume_orders_test LIMIT 2;

Ahora que tiene los datos en el tema, puede hacer que otros servicios se conecten a ellos y los consuman y luego activen correos electrónicos o alertas, por ejemplo.

Cómo conectar la metabase

Para acceder a la instancia de Metabasehttp://localhost:3030 , visite si está ejecutando la demostración localmente o http://your_server_ip:3030si está ejecutando la demostración en un servidor. Luego siga los pasos para completar la configuración de Metabase.

Asegúrese de seleccionar Materialise como la fuente de los datos.

Una vez que esté listo, podrá visualizar sus datos tal como lo haría con una base de datos PostgreSQL estándar.

Cómo detener la demostración

Para detener todos los servicios, ejecute el siguiente comando:

docker-compose down

Conclusión

Como puede ver, este es un ejemplo muy simple de cómo usar Materialise. Puede usar Materialise para ingerir datos de una variedad de fuentes y luego transmitirlos a una variedad de destinos.

Fuente: https://www.freecodecamp.org/news/how-to-join-mysql-and-postgres-in-a-live-materialized-view/

#mysql #postgre

Cómo Unir MySQL Y Postgres En Una Vista Materializada En Vivo
Dylan  Iqbal

Dylan Iqbal

1649657512

PostgreSQL Replication (PDF Book for FREE Download)

Download This PDF Book: PostgreSQL Replication – Second Edition 2nd Revised by Hans-Jürgen Schönig, for free.

Leverage the power of PostgreSQL replication to make your databases more robust, secure, scalable, and fast

About This Book

  • Efficiently replicate PostgreSQL using high-end techniques to protect your data and run your server without interruptions
  • Improve reliability, fault-tolerance, and accessibility by maintaining consistency between redundant resources
  • Get grips with the latest version of PostgreSQL with this example-based guide

Who This Book Is For

This book is ideal for PostgreSQL administrators who want to set up and understand replication. By the end of the book, you will be able to make your databases more robust and secure by getting to grips with PostgreSQL replication.

What You Will Learn

  • Use Point-in-time Recovery to perform data recovery as well as replication
  • Set up synchronous as well as asynchronous streaming replication
  • Get familiarized with the transaction log, the core component of most replication setups and its purpose
  • Improve speed and reliability with an understanding of pgpool and PgBouncer
  • Increase your data security and geographically distribute data
  • Make your systems more available and secure with Linux High Availability
  • Scale out with PL/Proxy and Postgres-XC
  • Detect, investigate, and solve replication-related problems

In Detail

PostgreSQL offers a comprehensive set of replication related features. Unleashing the power of PostgreSQL provides you with countless opportunities and a competitive advantage over other database systems.

This book will guide you through the most important concepts of PostgreSQL replication. It contains all the information you need to design and operate replicated setups.

Beginning by giving you an understanding of replication concepts, the PostgreSQL transaction log, and Point-in-time Recovery, we gradually move on to setting up asynchronous and synchronous replication. Next up, you will learn to monitor a PostgreSQL cluster setup, deal with monitoring tools, and then move on to understanding Linux High Availability. Further, we explore widely-used tools such as Slony, SkyTools, Postgres-XC, and walbouncer, and set up PL/Proxy.

Finally, you'll get acquainted with the new technology of BDR, which allows bidirectional replication in PostgreSQL.

Download

#postgresql #postgre #sql #database #developer 

PostgreSQL Replication (PDF Book for FREE Download)
Franz  Becker

Franz Becker

1648990800

Pg_insights: Convenient SQL for Monitoring Postgres Database Health.

pg_insights

Convenient SQL for monitoring Postgres database health. This repository is inspired by commands from Heroku's pg_extras repository.

How to Use

psql

You can run a script using psql's -f option. For example:

$ psql postgres -f sql/cache_hit_rate.sql

It also works with aliases you have setup with psql:

$ alias psql_my_db="psql -h my_db.com -d my_db -U admin"
$ psql_my_db -f sql/cache_hit_rate.sql

Other

You can also copy/paste any of the SQL in the sql/ directory and run with the Postgres client of your choice.

Scripts

active_autovacuums.sql (admin permission)

  • Returns all running autovacuums operations.

analyze_stats.sql (read permission)

  • Returns autovacuum analyze stats for each table.

bloat.sql (read permission)

  • Returns the approximate bloat from dead tuples for each table.
  • This bloat can also be index bloat.

buffer_cache_usage.sql (admin permission)

  • Returns the distribution of shared buffers used for each table.
  • Requires the pg_buffercache extension.
  • Includes the total bytes of a table in shared buffers, the percentage of shared buffers a table is using, and the percentage of a table the exists in shared buffers.

cache_hit_rate.sql (read permission)

  • Returns the cache hit rate for indices and tables.
  • This is the rate of queries that only hit in-memory shared buffers rather than having to fetch from disk.
  • Note that a queries that are cache misses in Postgres's shared buffers may still hit the in-memory OS page cache, so a miss not technically go all the way to the disk.
  • Both of these rates should be 99+% ideally.

index_hit_rate.sql (read permission)

  • Returns the index hit rate for each table.
  • This rate represents the percentage of queries that utilize 1 or more indices when querying a table.
  • These rates should be 99+% ideally.

index_size.sql (read permission)

  • Returns the size of each index in bytes.

reset_stats.sql (admin permission)

  • Resets pg_stats statistics tables.

table_settings.sql (read permission)

  • Returns the table-specific settings of each table.

table_size.sql (read permission)

  • Returns the size of each table in bytes.
  • Does not include size of the tables' indices.

table_size_with_indices.sql (read permission)

  • Returns size of each table in bytes including all indices.

toast_size.sql (read permission)

  • Returns total size of all TOAST data in each table in bytes.

unused_indices.sql (read permission)

  • Returns indices that are rarely used.
  • Note that sometimes the query optimizer will elect to avoid using indices for tables with a very small number of rows because it can be more efficient.

vacuum_stats.sql (read permission)

  • Returns autovacuum stats for each table.

Contributing

Pull requests for bug fixes, improvements, or new SQL are always welcome!


Author: lob
Source Code: https://github.com/lob/pg_insights
License: MIT License

#postgre 

Franz  Becker

Franz Becker

1648954800

Command-line Admin tool for Observing and Troubleshooting Postgres.

pgCenter is a command-line admin tool for observing and troubleshooting Postgres.


Main goal

Postgres provides various activity statistics about its runtime, such as connections, statements, database operations, replication, resources usage and more. The general purpose of the statistics is to help DBAs to monitor and troubleshoot Postgres. However, these statistics provided in textual form retrieved from SQL functions and views, and Postgres doesn't provide native tools for working with statistics views.

pgCenter's main goal is to help Postgres DBA working with statistics and provide a convenient way to observe Postgres in runtime.

Key features

  • Top-like interface that allows you to monitor stats changes as you go. See details here.
  • Configuration management function allows viewing and editing of current configuration files and reloading the service, if needed.
  • Logfiles functions allow you to quickly check Postgres logs without stopping statistics monitoring.
  • "Poor man’s monitoring" allows you to collect Postgres statistics into files and build reports later on. See details here.
  • Wait events profiler allows seeing what wait events occur during queries execution. See details here.

Quick start

Pull Docker image from DockerHub; run pgcenter and connect to the database.

docker pull lesovsky/pgcenter:latest
docker run -it --rm lesovsky/pgcenter:latest pgcenter top -h 1.2.3.4 -U user -d dbname

Supported statistics

PostgreSQL statistics

System statistics

pgcenter top also provides system usage information based on statistics from procfs filesystem:

  • load average and CPU usage time (user, system, nice, idle, iowait, software, and hardware interrupts, steal);
  • memory and swap usage, amount of cached and dirty memory, writeback activity;
  • storage devices statistics: IOPS, throughput, latencies, average queue and requests size, devices utilization;
  • network interfaces statistics: throughput in bytes and packets, different kind of errors, saturation and utilization.
  • mounted filesystems' usage statistics: total size, amount of free/used/reserved space and inodes.

In the case of connecting to remote Postgres, there is possibility to use additional SQL functions used for retrieving /proc statistics from a remote host. For more information, see details here.

Install notes

Packages for DEB, RPM, APK are available on releases page.

Usage notes

pgCenter has been developed to work on Linux and hasn't been tested on other OS (operating systems); therefore, it is not recommended using it on alternative systems because it will not operate properly.

pgCenter supports a wide range of PostgreSQL versions, despite the difference in statistics between each version. If pgCenter is unable to read a particular stat, it will show a descriptive error message.

Ideally, pgCenter requires SUPERUSER database privileges, or at least privileges to view statistics, read settings, logfiles and send signals to other backends. Roles with such privileges (except reading logs) have been introduced in Postgres 10; see details here.

It is recommended to run pgCenter on the same host where Postgres is running. This is because for Postgres, pgCenter is just a simple client application, and it may have the same problems as other applications that work with Postgres, such as network-related problems, slow responses, etc.

It is possible to run pgCenter on one host and connect to Postgres, which runs on another host, but some functions may not work - this fully applies to pgcenter top command.

pgCenter also supports Amazon RDS for PostgreSQL, but as mentioned above, some functions will not work, and also system stats will not be available, because of PostgreSQL RDS instances don't support untrusted procedural languages due to security reasons.

Development, testing and contribution

To help development you are encouraged to:

  • provide suggestion/feedback or issue (follow the provided issue template carefully).
  • pull requests for bug fixes of improvements; this docs might be helpful.
  • star the project

Thanks

  • Thank you for using pgCenter!
  • Sebastien Godard for sysstat.
  • Brendan Gregg and Tim Cook for nicstat.
  • Pavel Stěhule for his articles.
  • Pavel Alexeev, package maintainer on EPEL testing repo (Fedora/Centos).
  • Manuel Rüger, ebuild maintainer on mrueg overlay (Gentoo Linux).
  • Anton Novojilov, package maintainer on RHEL/CentOS Linux (Essential Kaos repo).
  • Nikolay A. Fetisov, package maintainer at Sisyphus ALT Linux.
  • Devrim Gündüz, package maintainer on official PostgreSQL yum repo.

Author: lesovsky
Source Code: https://github.com/lesovsky/pgcenter
License: View license

#postgre 

Command-line Admin tool for Observing and Troubleshooting Postgres.