One place for hosting & domains

      Archives hassan latif

      Использование функции map в Python


      Введение

      Встроенная в Python функция map() используется для применения функции к каждому элементу итерируемого объекта (например, списка или словаря) и возврата нового итератора для получения результатов. Функция map() возвращает объект map (итератор), который мы можем использовать в других частях нашей программы. Также мы можем передать объект map в функцию list() или другой тип последовательности для создания итерируемого объекта.

      Функция map() имеет следующий синтаксис:

      map(function, iterable, [iterable 2, iterable 3, ...])
      

      Вместо использования цикла for функция map() дает возможность применить функцию к каждому элементу итерируемого объекта. Это повышает производительность, поскольку функция применяется только к одному элементу за раз без создания копий элементов в другом итерируемом объекте. Это особенно полезно при обработке больших наборов данных. Также map() может принимать несколько итерируемых объектов в качестве аргументов функции, отправляя в функцию по одному элементу каждого итерируемого объекта за раз.

      В этом обучающем модуле мы рассмотрим три способа работы с map(): с функцией lambda, с определяемой пользователем функцией и со встроенной функцией, использующей несколько аргументов итерируемого объекта.

      Использование функции Lambda

      Первый аргумент map() — это функция, которую мы используем для применения к каждому элементу. Python вызывает функцию один раз для каждого элемента итериируемого объекта, который мы передаем в map(), и возвращает измененный элемент в объект map. В качестве первого аргумента функции мы можем передать определенную пользователем функцию или использовать функции lambda, особенно если выражение будет менее сложным.

      Синтаксис map() с функцией lambda выглядит следующим образом:

      map(lambda item: item[] expression, iterable)
      

      С таким списком мы можем реализовать функцию lambda с выражением, которое хотим применить к каждому элементу в нашем списке:

      numbers = [10, 15, 21, 33, 42, 55]
      

      Чтобы применить выражение к каждому из наших чисел, мы можем использовать map() и lambda:

      mapped_numbers = list(map(lambda x: x * 2 + 3, numbers))
      

      Здесь мы декларируем элемент в нашем списке как x. Затем мы добавим наше выражение. Мы передадим список чисел как итерируемый объект для map().

      Для немедленного получения результатов мы распечатаем список объекта map:

      print(mapped_numbers)
      

      Output

      [23, 33, 45, 69, 87, 113]

      Мы использовали list(), чтобы объект map был выведен как список, а не в трудной для интерпретации объектной форме, например: <map object at 0x7fc250003a58>. Объект map является итератором наших результатов, чтобы мы могли использовать его в цикле for или использовать list() для превращения в список. Мы делаем это здесь, потому что это хороший способ просмотра результатов.

      В итоге map() наиболее полезна для работы с большими наборами данных, чтобы мы могли работать с объектом map, и не использовали для них конструктор, например, list().

      Для небольших наборов данных список может быть более понятным, однако для этого обучающего модуля мы используем небольшой набор данных для демонстрации возможностей map().

      Реализация определяемой пользователем функции

      Аналогично lambda мы можем использовать определенную функцию для применения к итерируемому объекту. Функции lambda более полезны при использовании выражения с одной строкой, определяемые пользователем функции лучше подходят для более сложных выражений. Если же нам нужно передать в функцию другой элемент данных, применяемый к итерируемому объекту, определяемые пользователем функции будут удобнее для чтения.

      Например, в следующем итерируемом объекте каждый элемент является словарем, содержащим различные детали о каждом из существ в нашем аквариуме:

      aquarium_creatures = [
          {"name": "sammy", "species": "shark", "tank number": 11, "type": "fish"},
          {"name": "ashley", "species": "crab", "tank number": 25, "type": "shellfish"},
          {"name": "jo", "species": "guppy", "tank number": 18, "type": "fish"},
          {"name": "jackie", "species": "lobster", "tank number": 21, "type": "shellfish"},
          {"name": "charlie", "species": "clownfish", "tank number": 12, "type": "fish"},
          {"name": "olly", "species": "green turtle", "tank number": 34, "type": "turtle"}
      ]
      

      Мы решили, что все существа в аквариуме будут перемещены в один и тот же резервуар. Нам нужно обновить наши записи, чтобы показать, что все наши существа перемещаются в резервуар 42. Чтобы дать map() доступ к каждому словарю и каждой паре ключ:значение в словарях, мы построим вложенную функцию:

      def assign_to_tank(aquarium_creatures, new_tank_number):
          def apply(x):
              x["tank number"] = new_tank_number
              return x
          return map(apply, aquarium_creatures)
      

      Мы определяем функцию assign_to_tank(), которая принимает aquarium_creatures и new_tank_number в качестве параметров. В assign_to_tank() мы передаем apply() как функцию в map() в последней строке. Функция assign_to_tank возвратит iterator, полученный от map().

      apply() принимает в качестве аргумента x, что означает элемент в нашем списке — одиночный словарь.

      Далее мы указываем, что x — это ключ "tank number" из aquarium_creatures, и что он должен хранить переданное значение new_tank_number. Мы возвратим каждый элемент после применения нового номера резервуара.

      Мы вызовем assign_to_tank() с нашим списком словарей и новый номер резервуара, который нам нужно заменить для каждого существа:

      assigned_tanks = assign_to_tank(aquarium_creatures, 42)
      

      После выполнения функции мы сохраним объект фильтра в переменной assigned_tanks, которую мы превратим в список и распечатаем:

      print(list(assigned_tanks))
      

      Вывод программы будет выглядеть следующим образом:

      Output

      [{'name': 'sammy', 'species': 'shark', 'tank number': 42, 'type': 'fish'}, {'name': 'ashley', 'species': 'crab', 'tank number': 42, 'type': 'shellfish'}, {'name': 'jo', 'species': 'guppy', 'tank number': 42, 'type': 'fish'}, {'name': 'jackie', 'species': 'lobster', 'tank number': 42, 'type': 'shellfish'}, {'name': 'charlie', 'species': 'clownfish', 'tank number': 42, 'type': 'fish'}, {'name': 'olly', 'species': 'green turtle', 'tank number': 42, 'type': 'turtle'}]

      Мы присвоили новый номер резервуара нашему списку словарей. Используя функцию, которую мы определяем, мы включаем map() для эффективного применения функции к каждому элементу списка.

      Использование встроенной функции с несколькими итерируемыми объектами

      Помимо функций lambda и определяемых нами функций, мы можем использовать вместе с map() встроенные функции Python. Для применения функции с несколькими итерируемыми объектами мы передаем имя другого итерируемого объекта после первого. Рассмотрим в качестве примера функцию pow(), которая принимает два числа и возводит базовое число в указанную степень.

      Здесь у нас списки целых чисел, которые мы хотим использовать с pow():

      base_numbers = [2, 4, 6, 8, 10]
      powers = [1, 2, 3, 4, 5]
      

      Затем мы передадим pow() в качестве функции в map() и укажем два списка в качестве итерируемых объектов:

      numbers_powers = list(map(pow, base_numbers, powers))
      
      print(numbers_powers)
      

      map() применит функцию pow() к тому же элементу в каждом списке для возведения в степень. Поэтому в результатах мы увидим 2**1, 4**2, 6**3 и т. д.:

      Output

      [2, 16, 216, 4096, 100000]

      Если мы передадим map() итерируемый объект, который будет длиннее другого итерируемого объекта, map() остановит расчеты после достижения конца наиболее короткого объекта. В следующей программе мы дополним base_numbers тремя дополнительными числами:

      base_numbers = [2, 4, 6, 8, 10, 12, 14, 16]
      powers = [1, 2, 3, 4, 5]
      
      numbers_powers = list(map(pow, base_numbers, powers))
      
      print(numbers_powers)
      

      В расчетах программы ничего не изменится, и результат будет точно таким же:

      Output

      [2, 16, 216, 4096, 100000]

      Мы использовали функцию map() со встроенной функцией Python и посмотрели на одновременную обработку нескольких итерируемых объектов. Мы увидели, что map() продолжит обрабатывать несколько итерируемых объектов, пока не достигнет конца объекта, содержащего меньше всего элементов.

      Заключение

      В этом обучающем модуле мы узнали о различных способах использования функции map() в Python. Теперь вы можете использовать map() с собственной функцией, с функцией lambda и с любыми другими встроенными функциями. Также вы можете реализовать map() с функциями, для которых требуется несколько итерируемых объектов.

      В этом обучающем модуле мы распечатали результаты map() в формате списка для демонстрационных целей. В наших программах мы обычно будем использовать возвращаемый объект map для дальнейших манипуляций с данными.

      Для получения дополнительной информации о Python​​ ознакомьтесь с нашей серией Программирование на Python 3 и посетите нашу тематическую страницу, посвященную Python. Чтобы узнать больше о работе с наборами данных в функциональном программировании, читайте нашу статью о функции filter().



      Source link

      Erstellen einer REST-API mit Prisma und PostgreSQL


      Der Autor hat den Diversity in Tech Fund dazu ausgewählt, eine Spende im Rahmen des Programms Write for DOnations zu erhalten.

      Einführung

      Prisma ist ein Open-Source-basiertes Datenbank-Toolkit. Es besteht aus drei Haupttools:

      • Prisma Client: Ein automatisch generierter und typensicherer Query Builder für Node.js und TypeScript.
      • Prisma Migrate: Ein deklaratives Modellierungs- und Migrationssystem für Daten.
      • Prisma Studio: Eine GUI zum Anzeigen und Bearbeiten von Daten in Ihrer Datenbank.

      Diese Tools dienen dazu, die Produktivität von Anwendungsentwicklern in ihren Datenbank-Workflows zu steigern. Einer der größten Vorteile von Prisma ist die Ebene der Abstraktion, die möglich ist: Anstatt komplexe SQL-Abfragen oder Schemamigrationen zu erstellen, können Anwendungsentwickler bei der Arbeit mit ihrer Datenbank unter Verwendung von Prisma Daten intuitiver verwalten.

      In diesem Tutorial erstellen Sie eine REST-API für eine kleine Blogging-Anwendung in TypeScript mithilfe von Prisma und eine PostgreSQL-Datenbank. Sie werden Ihre PostgreSQL-Datenbank mit Docker lokal einrichten und die REST-API mit Express implementieren. Am Ende des Tutorials verfügen Sie über einen Webserver, der auf Ihrem Rechner lokal ausgeführt wird und auf verschiedene HTTP-Anfragen reagieren sowie Daten in der Datenbank lesen und schreiben kann.

      Voraussetzungen

      Dieses Tutorial setzt Folgendes voraus:

      Grundlegende Vertrautheit mit TypeScript und REST-APIs ist hilfreich, für dieses Tutorial jedoch nicht erforderlich.

      Schritt 1 — Erstellen Ihres TypeScript-Projekts

      In diesem Schritt werden Sie mit npm ein einfaches TypeScript-Projekt einrichten. Dieses Projekt wird als Grundlage für die REST-API dienen, die Sie im Laufe dieses Tutorials erstellen werden.

      Erstellen Sie zunächst ein neues Verzeichnis für Ihr Projekt:

      Navigieren Sie als Nächstes in das Verzeichnis und initialisieren Sie ein leeres npm-Projekt. Beachten Sie, dass die Option -y hier bedeutet, dass Sie die interaktiven Eingabeaufforderungen des Befehls überspringen. Um die Eingabeaufforderungen zu durchlaufen, entfernen Sie -y aus dem Befehl:

      Weitere Details zu diesen Eingabeaufforderungen finden Sie in Schritt 1 unter Verwenden von Node.js-Modulen mit npm und package.json.

      Sie erhalten eine Ausgabe, die der folgenden ähnelt und die Standardantworten umfasst:

      Output

      Wrote to /.../my-blog/package.json: { "name": "my-blog", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo "Error: no test specified" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }

      Dieser Befehl erstellt eine minimale package.json-Datei, die Sie als Konfigurationsdatei für Ihr npm-Projekt verwenden können. Sie sind nun bereit, TypeScript in Ihrem Projekt zu konfigurieren.

      Führen Sie den folgenden Befehl für eine einfache TypeScript-Einrichtung aus:

      • npm install typescript ts-node @types/node --save-dev

      Dadurch werden drei Pakete als Entwicklungsabhängigkeiten in Ihrem Projekt installiert:

      • typescript: Die TypeScript-Toolchain.
      • ts-node: Ein Paket zum Ausführen von TypeScript-Anwendungen ohne vorherige Kompilierung in JavaScript.
      • @types/node: Die TypeScript-Typdefinitionen für Node.js.

      Die letzte Aufgabe besteht darin, eine tsconfig.json-Datei hinzuzufügen, um sicherzustellen, dass TypeScript für die von Ihnen erstellte Anwendung richtig konfiguriert ist.

      Führen Sie den folgenden Befehl aus, um die Datei zu erstellen:

      Fügen Sie in der Datei den folgenden JSON-Code hinzu:

      my-blog/tsconfig.json

      {
        "compilerOptions": {
          "sourceMap": true,
          "outDir": "dist",
          "strict": true,
          "lib": ["esnext"],
          "esModuleInterop": true
        }
      }
      

      Speichern und schließen Sie die Datei.

      Dies ist eine Standard- und Minimalkonfiguration für ein TypeScript-Projekt. Wenn Sie mehr über die einzelnen Eigenschaften der Konfigurationsdatei erfahren möchten, können Sie die TypeScript-Dokumentation konsultieren.

      Sie haben Ihr einfaches TypeScript-Projekt mit npm eingerichtet. Als Nächstes werden Sie Ihre PostgreSQL-Datenbank mit Docker einrichten und Prisma damit verbinden.

      Schritt 2 — Einrichten von Prisma mit PostgreSQL

      In diesem Schritt installieren Sie die Prisma-CLI , erstellen Ihre erste Prisma-Schemadatei, richten PostgreSQL mit Docker ein und verbinden Prisma damit. Das Prisma-Schema ist die wichtigste Konfigurationsdatei für Ihr Prisma-Setup und enthält das Datenbankschema.

      Installieren Sie zunächst die Prisma-CLI mit dem folgenden Befehl:

      • npm install @prisma/cli --save-dev

      Als bewährte Praxis wird empfohlen, die Prisma-CLI in Ihrem Projekt lokal zu installieren (und nicht im Rahmen einer globalen Installation). Dadurch lassen sich Versionskonflikte vermeiden, falls Sie mehr als ein Prisma-Projekt auf Ihrem Rechner verwenden.

      Als Nächstes richten Sie mit Docker Ihre PostgreSQL-Datenbank ein. Erstellen Sie mit dem folgenden Befehl eine neue Docker Compose-Datei:

      Fügen Sie der neu erstellten Datei den folgenden Code hinzu:

      my-blog/docker-compose.yml

      version: '3.8'
      services:
        postgres:
          image: postgres:10.3
          restart: always
          environment:
            - POSTGRES_USER=sammy
            - POSTGRES_PASSWORD=your_password
          volumes:
            - postgres:/var/lib/postgresql/data
          ports:
            - '5432:5432'
      volumes:
        postgres:
      

      Diese Docker Compose-Datei konfiguriert eine PostgreSQL-Datenbank, auf die über Port 5432 des Docker-Containers zugegriffen werden kann. Beachten Sie außerdem, dass die Anmeldedaten für die Datenbank aktuell sammy (Benutzer) und your_password (Passwort) lauten. Sie können diese Anmeldedaten in Ihren bevorzugten Benutzer und Ihr bevorzugtes Passwort ändern. Speichern und schließen Sie die Datei.

      Fahren Sie nun fort und starten Sie den PostgreSQL-Datenbankserver mit dem folgenden Befehl:

      Die Ausgabe dieses Befehls wird in etwa wie folgt aussehen:

      Output

      Pulling postgres (postgres:10.3)... 10.3: Pulling from library/postgres f2aa67a397c4: Pull complete 6de83ca23e55: Pull complete . . . Status: Downloaded newer image for postgres:10.3 Creating my-blog_postgres_1 ... done

      Sie können mit folgendem Befehl überprüfen, ob der Datenbankserver ausgeführt wird:

      Dadurch erhalten Sie eine Aufgabe, die in etwa wie folgt aussieht:

      Output

      CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8547f8e007ba postgres:10.3 "docker-entrypoint.s…" 3 seconds ago Up 2 seconds 0.0.0.0:5432->5432/tcp my-blog_postgres_1

      Nachdem der Datenbankserver ausgeführt wird, können Sie nun Ihr Prisma-Setup erstellen. Führen Sie den folgenden Befehl über die Prisma-CLI aus:

      Dadurch erhalten Sie folgende Ausgabe:

      Output

      ✔ Your Prisma schema was created at prisma/schema.prisma. You can now open it in your favorite editor.

      Beachten Sie, dass Sie als bewährte Praxis allen Aufrufen der Prisma-CLI npx voranstellen sollten. Dadurch wird sichergestellt, dass Sie Ihre lokale Installation verwenden.

      Nachdem Sie den Befehl ausgeführt haben, erstellt die Prisma-CLI in Ihrem Projekt einen neuen Ordner namens prisma. Er enthält die folgenden zwei Dateien:

      • schema.prisma: Die Hauptkonfigurationsdatei für Ihr Prisma-Projekt (schließt Ihr Datenmodell mit ein).
      • .env: Eine dotenv-Datei zum Definieren Ihrer Datenbankverbindungs-URL.

      Um sicherzustellen, dass Prisma den Speicherort Ihrer Datenbank kennt, öffnen Sie die Datei .env und passen Sie die Umgebungsvariable DATABASE_URL an.

      Öffnen Sie zunächst die .env-Datei:

      Jetzt können Sie die Umgebungsvariable wie folgt setzen:

      my-blog/prisma/.env

      DATABASE_URL="postgresql://sammy:your_password@localhost:5432/my-blog?schema=public"
      

      Ändern Sie die Anmeldedaten für die Datenbank unbedingt auf jene, die Sie in der Docker Compose-Datei angegeben haben. Um mehr über das Format der Verbindungs-URL zu erfahren, besuchen Sie die Prisma-Dokumentation.

      Wenn Sie damit fertig sind, speichern und schließen Sie die Datei.

      In diesem Schritt haben Sie Ihre PostgreSQL-Datenbank mit Docker eingerichtet, die Prisma-CLI installiert und Prisma über eine Umgebungsvariable mit der Datenbank verbunden. Im nächsten Abschnitt definieren Sie Ihr Datenmodell und erstellen Ihre Datenbanktabellen.

      Schritt 3 — Definieren des Datenmodells und Erstellen von Datenbanktabellen

      In diesem Schritt definieren Sie Ihr Datenmodell in der Prisma-Schemadatei. Dieses Datenmodell wird dann mit Prisma Migrate der Datenbank zugeordnet; dadurch werden die SQL-Anweisungen generiert und gesendet, um die Tabellen zu erstellen, die Ihrem Datenmodell entsprechen. Da Sie eine Blogging-Anwendung erstellen, werden die wichtigsten Entitäten der Anwendung Benutzer und Beiträge sein.

      Prisma verwendet seine eigene Datenmodellierungssprache zum Definieren der Form Ihrer Anwendungsdaten.

      Öffnen Sie zunächst Ihre schema.prisma-Datei mit dem folgenden Befehl:

      • nano prisma/schema.prisma

      Fügen Sie nun folgende Modelldefinitionen hinzu. Sie können die Modelle am Ende der Datei platzieren, unmittelbar nach dem generator client-Block:

      my-blog/prisma/schema.prisma

      . . .
      model User {
        id    Int     @default(autoincrement()) @id
        email String  @unique
        name  String?
        posts Post[]
      }
      
      model Post {
        id        Int     @default(autoincrement()) @id
        title     String
        content   String?
        published Boolean @default(false)
        author    User?   @relation(fields: [authorId], references: tag:www.digitalocean.com,2005:/community/tutorials/how-to-build-a-rest-api-with-prisma-and-postgresql-de)
        authorId  Int?
      }
      

      Speichern und schließen Sie die Datei.

      Sie definieren zwei Modelle namens User und Post. Jedes von ihnen verfügt über eine Reihe vonFeldern, die die Eigenschaften des Modells darstellen. Die Modelle werden Datenbanktabellen zugeordnet; die Felder stellen die einzelnen Spalten dar.

      Beachten Sie außerdem, dass es eine one-to-many-Beziehung zwischen den beiden Modellen gibt, die von den Beziehungsfeldern posts und author in User und Post angegeben werden. Das bedeutet, dass ein Benutzer mit verschiedenen Beiträgen verknüpft sein kann.

      Nach Implementierung dieser Modelle können Sie nun unter Verwendung von Prisma Migrate die entsprechenden Tabellen in der Datenbank erstellen. Führen Sie in Ihrem Terminal folgenden Befehl aus:

      • npx prisma migrate save --experimental --create-db --name "init"

      Dieser Befehl erstellt in Ihrem Dateisystem eine neue Migration. Hier finden Sie einen kurzen Überblick über die drei Optionen, die dem Befehl bereitgestellt werden:

      • --experimental: Erforderlich, da Prisma Migrate derzeit in einem experimentellen Zustand ist.
      • --create-db: Ermöglicht Prisma Migrate die Erstellung der Datenbank mit dem Namen my-blog, die in der Verbindungs-URL angegeben ist.
      • --name "init": Gibt den Namen der Migration an (wird zum Benennen des Migrationsordners verwendet, der in Ihrem Dateisystem erstellt wird).

      Die Ausgabe dieses Befehls wird in etwa wie folgt aussehen:

      Output

      New datamodel: // This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema datasource db { provider = "postgresql" url = env("DATABASE_URL") } generator client { provider = "prisma-client-js" } model User { id Int @default(autoincrement()) @id email String @unique name String? posts Post[] } model Post { id Int @default(autoincrement()) @id title String content String? published Boolean @default(false) author User? @relation(fields: [authorId], references: tag:www.digitalocean.com,2005:/community/tutorials/how-to-build-a-rest-api-with-prisma-and-postgresql-de) authorId Int? } Prisma Migrate just created your migration 20200811140708-init in migrations/ └─ 20200811140708-init/ └─ steps.json └─ schema.prisma └─ README.md

      Sie können die Migrationsdateien erkunden, die im Verzeichnis prisma/migrations erstellt wurden.

      Um die Migration für Ihre Datenbank auszuführen und die Tabellen für Ihre Prisma-Modelle zu erstellen, führen Sie in Ihrem Terminal folgenden Befehl aus:

      • npx prisma migrate up --experimental

      Sie erhalten die folgende Ausgabe:

      Output

      . . . Checking the datasource for potential data loss... Database Changes: Migration Database actions Status 20200811140708-init 2 CreateTable statements. Done 🚀 You can get the detailed db changes with prisma migrate up --experimental --verbose Or read about them here: ./migrations/20200811140708-init/README.md 🚀 Done with 1 migration in 206ms.

      Prisma Migrate generiert nun die SQL-Anweisungen, die für die Migration erforderlich sind, und sendet sie an die Datenbank. Im Folgenden sehen Sie die SQL-Anweisungen, die die Tabellen erstellt haben:

      CREATE TABLE "public"."User" (
        "id" SERIAL,
        "email" text  NOT NULL ,
        "name" text   ,
        PRIMARY KEY ("id")
      )
      
      CREATE TABLE "public"."Post" (
        "id" SERIAL,
        "title" text  NOT NULL ,
        "content" text   ,
        "published" boolean  NOT NULL DEFAULT false,
        "authorId" integer   ,
        PRIMARY KEY ("id")
      )
      
      CREATE UNIQUE INDEX "User.email" ON "public"."User"("email")
      
      ALTER TABLE "public"."Post" ADD FOREIGN KEY ("authorId")REFERENCES "public"."User"("id") ON DELETE SET NULL ON UPDATE CASCADE
      

      In diesem Schritt haben Sie mit Prisma Migrate Ihr Datenmodell im Prisma-Schema definiert und die jeweiligen Datenbanktabellen erstellt. Im nächsten Schritt installieren Sie Prisma Client in Ihrem Projekt, damit Sie die Datenbank abfragen können.

      Schritt 4 — Erkunden von Prisma Client-Abfragen in einem einfachen Skript

      Prisma Client ist ein automatisch generierter und typensicherer Query Builder, mit dem Sie aus einer Node.js- oder TypeScript-Anwendung Daten in einer Datenbank programmatisch lesen und schreiben können. Sie werden ihn für Datenbankzugriff in Ihren REST-API-Routen verwenden, indem Sie traditionelle ORMs, einfache SQL-Abfragen, benutzerdefinierte Datenzugriffsebenen oder andere Methoden zur Kommunikation mit einer Datenbank ersetzen.

      In diesem Schritt installieren Sie Prisma Client und lernen die Abfragen kennen, die Sie damit senden können. Bevor Sie in den nächsten Schritten die Routen für Ihre REST-API implementieren, werden Sie zunächst einige der Prisma Client-Abfragen in einem einfachen ausführbaren Skript erkunden.

      Zuerst installieren Sie Prisma Client in Ihrem Projekt, indem Sie Ihr Terminal öffnen und das Prisma Client npm-Paket installieren:

      • npm install @prisma/client

      Erstellen Sie als Nächstes ein neues Verzeichnis namens src, das Ihre Quelldateien enthalten wird:

      Erstellen Sie nun in dem neuen Verzeichnis eine TypeScript-Datei:

      Alle Prisma Client-Abfragen geben promises (Zusagen) zurück, auf die Sie in Ihrem Code warten können. Dazu müssen Sie die Abfragen in einer async-Funktion senden.

      Fügen Sie den folgenden Codebaustein mit einer async-Funktion hinzu, die in Ihrem Skript ausgeführt wird:

      my-blog/src/index.ts

      import { PrismaClient } from '@prisma/client'
      
      const prisma = new PrismaClient()
      
      async function main() {
        // ... your Prisma Client queries will go here
      }
      
      main()
        .catch((e) => console.error(e))
        .finally(async () => await prisma.disconnect())
      

      Hier finden Sie einen kurzen Überblick über den Codebaustein:

      1. Sie importieren den PrismaClient-Konstruktor aus dem zuvor installierten @prisma/client npm-Paket.
      2. Sie instanziieren PrismaClient, indem Sie den Konstrukteur aufrufen, und erhalten eine Instanz namens prisma.
      3. Sie definieren eine async-Funktion namens main, wo Sie als Nächstes Ihre Prisma Client-Abfragen hinzufügen werden.
      4. Sie rufen die main-Funktion auf, fangen dabei alle möglichen Ausnahmen ab und stellen sicher, dass Prisma Client alle offenen Datenbankverbindungen schließt, indem Sie prisma.disconnect() aufrufen.

      Nach Implementierung der main-Funktion können Sie mit dem Hinzufügen von Prisma Client-Abfragen in das Skript beginnen. Passen Sie index.ts wie folgt an:

      my-blog/src/index.ts

      import { PrismaClient } from '@prisma/client'
      
      const prisma = new PrismaClient()
      
      async function main() {
        const newUser = await prisma.user.create({
          data: {
            name: 'Alice',
            email: '[email protected]',
            posts: {
              create: {
                title: 'Hello World',
              },
            },
          },
        })
        console.log('Created new user: ', newUser)
      
        const allUsers = await prisma.user.findMany({
          include: { posts: true },
        })
        console.log('All users: ')
        console.dir(allUsers, { depth: null })
      }
      
      main()
        .catch((e) => console.error(e))
        .finally(async () => await prisma.disconnect())
      

      In diesem Code verwenden Sie zwei Prisma Client-Abfragen:

      • create: Erstellt einen neuen User-Eintrag. Beachten Sie, dass Sie in Wahrheit ein nested write verwenden, was bedeutet, dass Sie in derselben Abfrage sowohl einen User– als auch einen Post-Eintrag erstellen.
      • findMany: Liest alle vorhandenen User-Einträge aus der Datenbank. Sie geben die include-Option an, wodurch zusätzlich auch die entsprechenden Post-Einträge für die einzelnen User-Einträge geladen werden.

      Führen Sie das Skript nun mit dem folgenden Befehl aus:

      Sie erhalten in Ihrem Terminal folgende Ausgabe:

      Output

      Created new user: { id: 1, email: '[email protected]', name: 'Alice' } [ { id: 1, email: '[email protected]', name: 'Alice', posts: [ { id: 1, title: 'Hello World', content: null, published: false, authorId: 1 } ] }

      Anmerkung: Wenn Sie eine Datenbank-GUI verwenden, können Sie überprüfen, ob die Daten erstellt wurden, indem Sie sich die Tabellen User und Post ansehen. Alternativ können Sie die Daten in Prisma Studio erkunden, indem Sie npx prisma studio --experimental ausführen.

      Sie haben Prisma Client nun verwendet, um Daten in Ihrer Datenbank zu lesen und zu schreiben. In den verbleibenden Schritten wenden Sie dieses neue Wissen an, um die Routen für eine beispielhafte REST-API zu implementieren.

      Schritt 5 — Implementieren Ihrer ersten REST-API-Route

      In diesem Schritt installieren Sie Express in Ihrer Anwendung. Express ist ein beliebtes Webframework für Node.js, das Sie zur Implementierung der REST-API-Routen in diesem Projekt verwenden werden. Die erste Route, die Sie implementieren werden, erlaubt es, mithilfe einer GET-Anfrage alle Benutzer von der API abzurufen. Die Benutzerdaten werden mit Prisma Client aus der Datenbank abgerufen.

      Installieren Sie dann Express mit dem folgenden Befehl:

      Da Sie TypeScript verwenden, sollten Sie die jeweiligen Typen auch als Entwicklungsabhängigkeiten installieren. Führen Sie dazu folgenden Befehl aus:

      • npm install @types/express --save-dev

      Nach Implementierung der Abhängigkeiten können Sie Ihre Express-Anwendung einrichten.

      Öffnen Sie dazu erneut Ihre zentrale Quelldatei:

      Löschen Sie nun den ganzen Code in index.ts und ersetzen Sie ihn durch Folgendes, um Ihre REST-API zu starten:

      my-blog/src/index.ts

      import { PrismaClient } from '@prisma/client'
      import express from 'express'
      
      const prisma = new PrismaClient()
      const app = express()
      
      app.use(express.json())
      
      // ... your REST API routes will go here
      
      app.listen(3000, () =>
        console.log('REST API server ready at: http://localhost:3000'),
      )
      

      Hier finden Sie eine kurze Aufschlüsselung des Codes:

      1. Sie importieren PrismaClient und express aus den jeweiligen npm-Paketen.
      2. Sie instanziieren PrismaClient, indem Sie den Konstruktor aufrufen, und erhalten eine Instanz namens prisma.
      3. Sie erstellen Ihre Express-Anwendung, indem Sie express() aufrufen.
      4. Sie fügen die Middleware express.json() hinzu, um sicherzustellen, dass Express JSON-Daten ordnungsgemäß verarbeiten kann.
      5. Sie starten den Server unter Port 3000.

      Jetzt können Sie Ihre erste Route implementieren. Fügen Sie zwischen den Aufrufen für app.use und app.listen folgenden Code hinzu:

      my-blog/src/index.ts

      . . .
      app.use(express.json())
      
      app.get('/users', async (req, res) => {
        const users = await prisma.user.findMany()
        res.json(users)
      })
      
      app.listen(3000, () =>
      console.log('REST API server ready at: http://localhost:3000'),
      )
      

      Speichern und schließen Sie anschließend Ihre Datei. Starten Sie dann mit dem folgenden Befehl Ihren lokalen Webserver:

      Sie erhalten die folgende Ausgabe:

      Output

      REST API server ready at: http://localhost:3000

      Um auf die Route /users zuzugreifen, können Sie Ihren Browser auf http://localhost:3000/users oder einen anderen HTTP-Client verweisen.

      In diesem Tutorial testen Sie alle REST-API-Routen mit curl (einem Terminal-basierten HTTP-Client).

      Anmerkung: Wenn Sie lieber einen GUI-basierten HTTP-Client verwenden, können Sie Alternativen wie Postwoman oder den Advanced REST Client verwenden.

      Öffnen Sie zum Testen Ihrer Route ein neues Terminalfenster oder eine Registerkarte (damit Ihr lokaler Webserver weiter ausgeführt werden kann) und führen Sie folgenden Befehl aus:

      • curl http://localhost:3000/users

      Sie erhalten die im vorherigen Schritt erstellten User-Daten:

      Output

      [{"id":1,"email":"[email protected]","name":"Alice"}]

      Beachten Sie, dass das posts-Array diesmal nicht enthalten ist. Das liegt daran, dass Sie die Option include nicht an den findMany-Aufruf in der Implementierung der Route /users übergeben.

      Sie haben unter /users Ihre erste REST-API-Route implementiert. Im nächsten Schritt implementieren Sie die verbleibenden REST-API-Routen, um Ihrer API weitere Funktionen hinzuzufügen.

      Schritt 6 — Implementieren der verbleibenden REST-API-Routen

      In diesem Schritt implementieren Sie die verbleibenden REST-API-Routen für Ihre Blogging-Anwendung. Am Ende wird Ihr Webserver verschiedene GET-, POST-, PUT– und DELETE-Anfragen bereitstellen.

      Hier finden Sie einen Überblick über die verschiedenen Routen, die Sie implementieren werden:

      HTTP-MethodeRouteBeschreibung
      GET/feedRuft alle veröffentlichten Beiträge ab.
      GET/post/:idRuft einen einzelnen Beitrag anhand seiner ID ab.
      POST/userErstellt einen neuen Benutzer.
      POST/postErstellt einen neuen Beitrag (als Entwurf).
      PUT/post/publish/:idSetzt das published-Feld eines Beitrags auf true.
      DELETEpost/:idLöscht einen Beitrag anhand seiner ID.

      Fahren Sie fort und implementieren Sie zunächst die verbleibenden GET-Routen.

      Öffnen Sie die Datei index.ts mit dem folgenden Befehl:

      Fügen Sie dann im Anschluss an die Implementierung der Route /users folgenden Code hinzu:

      my-blog/src/index.ts

      . . .
      
      app.get('/feed', async (req, res) => {
        const posts = await prisma.post.findMany({
          where: { published: true },
          include: { author: true }
        })
        res.json(posts)
      })
      
      app.get(`/post/:id`, async (req, res) => {
        const { id } = req.params
        const post = await prisma.post.findOne({
          where: { id: Number(id) },
        })
        res.json(post)
      })
      
      app.listen(3000, () =>
        console.log('REST API server ready at: http://localhost:3000'),
      )
      

      Speichern und schließen Sie Ihre Datei.

      Dieser Code implementiert die API-Routen für zwei GET-Anfragen:

      • /feed: Gibt eine Liste mit veröffentlichten Beiträgen zurück.
      • /post/:id: Gibt einen einzelnen Beitrag anhand seiner ID zurück.

      Prisma Client wird in beiden Implementierungen verwendet. In der Implementierung der Route /feed filtert die Abfrage, die Sie mit Prisma Client senden, nach allen Post-Einträgen, bei denen die Spalte published den Wert true enthält. Außerdem nutzt die Prisma Client-Abfrage include, um die entsprechenden author-Informationen für die einzelnen Beiträge abzurufen. In der Implementierung der Route /post/:id übergeben Sie die ID, die aus dem URL-Pfad abgerufen wird, um einen bestimmten Post-Eintrag aus der Datenbank zu lesen.

      Sie können den Server anhalten, indem Sie auf Ihrer Tastatur Strg+C drücken. Starten Sie dann den Server neu:

      Um die Route /feed zu testen, können Sie folgenden curl-Befehl verwenden:

      • curl http://localhost:3000/feed

      Da noch keine Beiträge veröffentlicht wurden, ist die Antwort ein leeres Array:

      Output

      []

      Um die Route /post/:id zu testen, können Sie folgenden curl-Befehl verwenden:

      • curl http://localhost:3000/post/1

      Dadurch wird der von Ihnen ursprünglich erstellte Beitrag zurückgegeben:

      Output

      {"id":1,"title":"Hello World","content":null,"published":false,"authorId":1}

      Implementieren Sie als Nächstes die beiden POST-Routen. Fügen Sie nach den Implementierungen der drei GET-Routen folgenden Code zu index.ts hinzu:

      my-blog/src/index.ts

      . . .
      
      app.post(`/user`, async (req, res) => {
        const result = await prisma.user.create({
          data: { ...req.body },
        })
        res.json(result)
      })
      
      app.post(`/post`, async (req, res) => {
        const { title, content, authorEmail } = req.body
        const result = await prisma.post.create({
          data: {
            title,
            content,
            published: false,
            author: { connect: { email: authorEmail } },
          },
        })
        res.json(result)
      })
      
      app.listen(3000, () =>
        console.log('REST API server ready at: http://localhost:3000'),
      )
      

      Wenn Sie damit fertig sind, speichern und schließen Sie die Datei.

      Dieser Code implementiert die API-Routen für zwei POST-Anfragen:

      • /user: Erstellt in der Datenbank einen neuen Benutzer.
      • /post: Erstellt in der Datenbank einen neuen Beitrag.

      Wie zuvor wird in beiden Implementierungen Prisma Client verwendet. In der Implementierung der Route /user übergeben Sie die Werte aus dem Haupttext der HTTP-Anfrage an die Prisma Client-Abfrage create.

      Die Route /post ist etwas aufwendiger: Hier können Sie die Werte aus dem Haupttext der HTTP-Anfrage nicht direkt übergeben; stattdessen müssen Sie sie zunächst manuell extrahieren, um sie dann an die Prisma Client-Abfrage zu übergeben. Der Grund dafür besteht darin, dass die Struktur von JSON im Haupttext der Anfrage nicht mit der Struktur übereinstimmt, die Prisma Client erwartet. Daher müssen Sie die erwartete Struktur manuell einrichten.

      Sie können die neuen Routen testen, indem Sie den Server mit Strg+C anhalten. Starten Sie dann den Server neu:

      Um über die Route /user einen neuen Benutzer zu erstellen, können Sie mit curl folgende POST-Anfrage senden:

      • curl -X POST -H "Content-Type: application/json" -d '{"name":"Bob", "email":"[email protected]"}' http://localhost:3000/user

      Dadurch wird in der Datenbank ein neuer Benutzer erstellt und folgende Ausgabe ausgedruckt:

      Output

      {"id":2,"email":"[email protected]","name":"Bob"}

      Um über die Route /post einen neuen Beitrag zu erstellen, können Sie mit curl folgende POST-Anfrage senden:

      • curl -X POST -H "Content-Type: application/json" -d '{"title":"I am Bob", "authorEmail":"[email protected]"}' http://localhost:3000/post

      Dadurch wird in der Datenbank ein neuer Beitrag erstellt und unter Verwendung der E-Mail-Adresse [email protected] mit dem Benutzer verbunden. Sie erhalten die folgende Ausgabe:

      Output

      {"id":2,"title":"I am Bob","content":null,"published":false,"authorId":2}

      Schließlich können Sie die Routen PUT und DELETE implementieren.

      Öffnen Sie die Datei index.ts mit dem folgenden Befehl:

      Fügen Sie dann nach der Implementierung der beiden POST-Routen den hervorgehobenen Code hinzu:

      my-blog/src/index.ts

      . . .
      
      app.put('/post/publish/:id', async (req, res) => {
        const { id } = req.params
        const post = await prisma.post.update({
          where: { id: Number(id) },
          data: { published: true },
        })
        res.json(post)
      })
      
      app.delete(`/post/:id`, async (req, res) => {
        const { id } = req.params
        const post = await prisma.post.delete({
          where: { id: Number(id) },
        })
        res.json(post)
      })
      
      app.listen(3000, () =>
        console.log('REST API server ready at: http://localhost:3000'),
      )
      

      Speichern und schließen Sie Ihre Datei.

      Dieser Code implementiert die API-Routen für eine PUT– sowie eine DELETE-Anfrage.

      • /post/publish/:id (PUT): Veröffentlicht einen Beitrag anhand seiner ID.
      • /post/:id (DELETE) Löscht einen Beitrag anhand seiner ID.

      Erneut wird Prisma Client in beiden Implementierungen verwendet. In der Implementierung der Route /post/publish/:id wird die ID des zu veröffentlichenden Beitrags von der URL abgerufen und an die update-Abfrage von Prisma Client übergeben. Die Implementierung der Route /post/:id zum Löschen eines Beitrags in der Datenbank ruft außerdem die Beitrags-ID aus der URL ab und übergibt sie an die delete-Abfrage von Prisma Client.

      Halten Sie den Server mit Strg+C auf Ihrer Tastatur erneut an. Starten Sie dann den Server neu:

      Sie können die Route PUT mit dem folgenden curl-Befehl testen:

      • curl -X PUT http://localhost:3000/post/publish/2

      Dadurch wird der Beitrag mit einem ID-Wert von 2 veröffentlicht. Wenn Sie die Anfrage /feed neu senden, wird dieser Beitrag nun in die Antwort aufgenommen.

      Abschließend können Sie die Route DELETE mit dem folgenden curl-Befehl testen:

      • curl -X DELETE http://localhost:3000/post/1

      Dadurch wird der Beitrag mit dem ID-Wert von 1 gelöscht. Um zu überprüfen, ob der Beitrag mit dieser ID gelöscht wurde, können Sie erneut eine GET-Anfrage an die Route /post/1 senden.

      In diesem Schritt haben Sie die verbleibenden REST-API-Routen für Ihre Blogging-Anwendung implementiert. Die API reagiert nun auf verschiedene GET-, POST-, PUT– und DELETE-Anfragen und implementiert Funktionen zum Lesen und Schreiben von Daten in der Datenbank.

      Zusammenfassung

      In diesem Artikel haben Sie eine REST-API mit einer Reihe von verschiedenen Routen erstellt, um Benutzer- und Beitragsdaten für eine beispielhafte Blogging-Anwendung zu erstellen, zu lesen, zu aktualisieren und zu löschen. Innerhalb der API-Routen haben Sie den Prisma Client zum Senden der entsprechenden Abfragen an Ihre Datenbank verwendet.

      Als Nächstes können Sie weitere API-Routen implementieren oder Ihr Datenbankschema mithilfe von Prisma Migrate erweitern. Konsultieren Sie die Prisma-Dokumentation, um mehr über verschiedene Aspekte von Prisma zu erfahren und einige sofort einsatzbereite Beispielprojekte zu erkunden (im Repository prisma-examples). Verwenden Sie dazu Tools wie GraphQL oder grPC APIs.



      Source link

      How To Improve Flexibility Using Terraform Variables, Dependencies, and Conditionals


      Introduction

      Hashicorp Configuration Language (HCL), which Terraform uses, provides many useful structures and capabilities that are present in other programming languages. Using loops in your infrastructure code can greatly reduce code duplication and increase readability, allowing for easier future refactoring and greater flexibility. HCL also provides a few common data structures, such as lists and maps (also called arrays and dictionaries respectively in other languages), as well as conditionals for execution path branching.

      Unique to Terraform is the ability to manually specify the resources one depends on. While the execution graph it builds when running your code already contains the detected links (which are correct in most scenarios), you may find yourself in need of forcing a dependency relationship that Terraform was unable to detect.

      In this article, we’ll review the data structures HCL provides, its looping features for resources (the count key, for_each, and for), and writing conditionals to handle known and unknown values, as well as explicitly specifying dependency relationships between resources.

      Prerequisites

      • A DigitalOcean account. If you do not have one, sign up for a new account.

      • A DigitalOcean Personal Access Token, which you can create via the DigitalOcean control panel. Instructions to do that can be found in this link: How to Generate a Personal Access Token.

      • Terraform installed on your local machine and a project set up with the DigitalOcean provider. Complete Step 1 and Step 2 of the How To Use Terraform with DigitalOcean tutorial, and be sure to name the project folder terraform-flexibility, instead of loadbalance. During Step 2, you do not need to include the pvt_key variable and the SSH key resource.

      • A fully registered domain name added to your DigitalOcean account. For instructions on how to do that, visit the official docs.

      Note: This tutorial has specifically been tested with Terraform 0.13.

      Data Types in HCL

      In this section, before you learn more about loops and other features of HCL that make your code more flexible, we’ll first go over the available data types and their uses.

      The Hashicorp Configuration Language supports primitive and complex data types. Primitive data types are strings, numbers, and boolean values, which are the basic types that can not be derived from others. Complex types, on the other hand, group multiple values into a single one. The two types of complex values are structural and collection types.

      Structural types allow values of different types to be grouped together. The main example is the resource definitions you use to specify what your infrastructure will look like. Compared to the structural types, collection types also group values, but only ones of the same type. The three collection types available in HCL that we are interested in are lists, maps, and sets.

      Lists

      Lists are similar to arrays in other programming languages. They contain a known number of elements of the same type, which can be accessed using the array notation ([]) by their whole-number index, starting from 0. Here is an example of a list variable declaration holding names of Droplets you’ll deploy in the next steps:

      variable "droplet_names" {
        type    = list(string)
        default = ["first", "second", "third"]
      }
      

      For the type, you explicitly specify that it’s a list whose element type is string, and then provide its default value. Values enumerated in brackets signify a list in HCL.

      Maps

      Maps are collections of key-value pairs, where each value is accessed using its key of type string. There are two ways of specifying maps inside curly brackets: by using colons (:) or equal signs (=) for specifying values. In both situations, the value must be enclosed with quotes. When using colons, the key must too be enclosed.

      The following map definition containing Droplet names for different environments is written using the equal sign:

      variable "droplet_env_names" {
        type = map(string)
      
        default = {
          development = "dev-droplet"
          staging = "staging-droplet"
          production = "prod-droplet"
        }
      }
      

      If the key starts with a number, you must use the colon syntax:

      variable "droplet_env_names" {
        type = map(string)
      
        default = {
          "1-development": "dev-droplet"
          "2-staging": "staging-droplet"
          "3-production": "prod-droplet"
        }
      }
      

      Sets

      Sets do not support element ordering, meaning that traversing sets is not guaranteed to yield the same order each time and that their elements can not be accessed in a targeted way. They contain unique elements repeated exactly once, and specifying the same element multiple times will result in them being coalesced with only one instance being present in the set.

      Declaring a set is similar to declaring a list, the only difference being the type of the variable:

      variable "droplet_names" {
        type    = set(string)
        default = ["first", "second", "third", "fourth"]
      }
      

      Now that you’ve learned about the types of data structures HCL offers and reviewed the syntax of lists, maps, and sets, which we’ll use throughout this tutorial, you’ll move on to trying some flexible ways of deploying multiple instances of the same resource in Terraform.

      Setting the Number of Resources Using the count Key

      In this section, you’ll create multiple instances of the same resource using the count key. The count key is a parameter available on all resources that specifies how many instances of it to create.

      You’ll see how it works by writing a Droplet resource, which you’ll store in a file named droplets.tf, in the project directory you created as part of the prerequisites. Create and open it for editing by running:

      Add the following lines:

      terraform-flexibility/droplets.tf

      resource "digitalocean_droplet" "test_droplet" {
        count  = 3
        image  = "ubuntu-18-04-x64"
        name   = "web"
        region = "fra1"
        size   = "s-1vcpu-1gb"
      }
      

      This code defines a Droplet resource called test_droplet, running Ubuntu 18.04 with 1GB RAM.

      Note that the value of count is set to 3, which means that Terraform will attempt to create three instances of the same resource. When you are done, save and close the file.

      You can plan the project to see what actions Terraform would take by running:

      • terraform plan -var "do_token=${DO_PAT}"

      The output will be similar to this:

      Output

      ... An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # digitalocean_droplet.test_droplet[0] will be created + resource "digitalocean_droplet" "test_droplet" { ... name = "web" ... } # digitalocean_droplet.test_droplet[1] will be created + resource "digitalocean_droplet" "test_droplet" { ... name = "web" ... } # digitalocean_droplet.test_droplet[2] will be created + resource "digitalocean_droplet" "test_droplet" { ... name = "web" ... } Plan: 3 to add, 0 to change, 0 to destroy. ...

      The output details that Terraform would create three instances of test_droplet, all with the same name web. While possible, it is not preferred, so let’s modify the Droplet definition to make the name of each instance different. Open droplets.tf for editing:

      Modify the highlighted line:

      terraform-flexibility/droplets.tf

      resource "digitalocean_droplet" "test_droplet" {
        count  = 3
        image  = "ubuntu-18-04-x64"
        name   = "web.${count.index}"
        region = "fra1"
        size   = "s-1vcpu-1gb"
      }
      

      Save and close the file.

      The count object provides the index parameter, which contains the index of the current iteration, starting from 0. The current index is substituted into the name of the Droplet using string interpolation, which allows you to dynamically build a string by substituting variables. You can plan the project again to see the changes:

      • terraform plan -var "do_token=${DO_PAT}"

      The output will be similar to this:

      Output

      ... An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # digitalocean_droplet.test_droplet[0] will be created + resource "digitalocean_droplet" "test_droplet" { ... name = "web.0" ... } # digitalocean_droplet.test_droplet[1] will be created + resource "digitalocean_droplet" "test_droplet" { ... name = "web.1" ... } # digitalocean_droplet.test_droplet[2] will be created + resource "digitalocean_droplet" "test_droplet" { ... name = "web.2" ... } Plan: 3 to add, 0 to change, 0 to destroy. ...

      This time, the three instances of test_droplet will have their index in their names, making them easier to track.

      You now know how to create multiple instances of a resource using the count key, as well as fetch and use the index of an instance during provisioning. Next, you’ll learn how to fetch the Droplet’s name from a list.

      Getting Droplet Names From a List

      In situations when multiple instances of the same resource need to have custom names, you can dynamically retrieve them from a list variable you define. During the rest of the tutorial, you’ll see several ways of automating Droplet deployment from a list of names, promoting flexibility and ease of use.

      You’ll first need to define a list containing the Droplet names. Create a file called variables.tf and open it for editing:

      Add the following lines:

      terraform-flexibility/variables.tf

      variable "droplet_names" {
        type    = list(string)
        default = ["first", "second", "third", "fourth"]
      }
      

      Save and close the file. This code defines a list called droplet_names, containing the strings first, second, third, and fourth.

      Open droplets.tf for editing:

      Modify the highlighted lines:

      terraform-flexibility/droplets.tf

      resource "digitalocean_droplet" "test_droplet" {
        count  = length(var.droplet_names)
        image  = "ubuntu-18-04-x64"
        name   =  var.droplet_names[count.index]
        region = "fra1"
        size   = "s-1vcpu-1gb"
      }
      

      To improve flexibility, instead of manually specifying a constant number of elements, you pass in the length of the droplet_names list to the count parameter, which will always return the number of elements in the list. For the name, you fetch the element of the list positioned at count.index, using the array bracket notation. Save and close the file when you’re done.

      Try planning the project again. You’ll receive output similar to this:

      Output

      ... An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # digitalocean_droplet.test_droplet[0] will be created + resource "digitalocean_droplet" "test_droplet" { ... + name = "first" ... } # digitalocean_droplet.test_droplet[1] will be created + resource "digitalocean_droplet" "test_droplet" { ... + name = "second" ... } # digitalocean_droplet.test_droplet[2] will be created + resource "digitalocean_droplet" "test_droplet" { ... + name = "third" ... } # digitalocean_droplet.test_droplet[3] will be created + resource "digitalocean_droplet" "test_droplet" { ... + name = "fourth" ... Plan: 4 to add, 0 to change, 0 to destroy. ...

      As a result of modifications, four Droplets would be deployed, successively named after the elements of the droplet_names list.

      You’ve learned about count, its features and syntax, and using it together with a list to modify the resource instances. You’ll now see its disadvantages, and how to overcome them.

      Understanding the Disadvantages of count

      Now that you know how count is used, you’ll see its disadvantages when modifying the list it’s used with.

      Let’s try deploying the Droplets to the cloud:

      • terraform apply -var "do_token=${DO_PAT}"

      Enter yes when prompted. The end of your output will be similar to this:

      Output

      Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

      Now let’s create one more Droplet instance by enlarging the droplet_names list. Open variables.tf for editing:

      Add a new element to the beginning of the list:

      terraform-flexibility/variables.tf

      variable "droplet_names" {
        type    = list(string)
        default = ["zero", "first", "second", "third", "fourth"]
      }
      

      When you’re done, save and close the file.

      Plan the project:

      • terraform plan -var "do_token=${DO_PAT}"

      You’ll receive output like this:

      Output

      ... An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: + create ~ update in-place Terraform will perform the following actions: # digitalocean_droplet.test_droplet[0] will be updated in-place ~ resource "digitalocean_droplet" "test_droplet" { ... ~ name = "first" -> "zero" ... } # digitalocean_droplet.test_droplet[1] will be updated in-place ~ resource "digitalocean_droplet" "test_droplet" { ... ~ name = "second" -> "first" ... } # digitalocean_droplet.test_droplet[2] will be updated in-place ~ resource "digitalocean_droplet" "test_droplet" { ... ~ name = "third" -> "second" ... } # digitalocean_droplet.test_droplet[3] will be updated in-place ~ resource "digitalocean_droplet" "test_droplet" { ... ~ name = "fourth" -> "third" ... } # digitalocean_droplet.test_droplet[4] will be created + resource "digitalocean_droplet" "test_droplet" { ... + name = "fourth" ... } Plan: 1 to add, 4 to change, 0 to destroy. ...

      The output shows that Terraform would rename the first four Droplets and create a fifth one called fourth, because it considers the instances as an ordered list and identifies the elements (Droplets) by their index number in the list. This is how Terraform initially considers the four Droplets:

      Index Number0123
      Droplet Namefirstsecondthirdfourth

      When the a new Droplet zero is added to the beginning, its internal list representation looks like this:

      Index Number01234
      Droplet Namezerofirstsecondthirdfourth

      The four initial Droplets are now shifted one place to the right. Terraform then compares the two states represented in tables: at position 0, the Droplet was called first, and because it’s different in the second table, it plans an update action. This continues until position 4, which does not have a comparable element in the first table, and instead a Droplet provisioning action is planned.

      This means that adding a new element to the list anywhere but to the very end would result in resources being modified when they do not need to be. Similar update actions would be planned if an element of the droplet_names list was removed.

      Incomplete resource tracking is the main downfall of using count for deploying a dynamic number of differing instances of the same resource. For a constant number of constant instances, count is a simple solution that works well. In situations like this, though, when some attributes are being pulled in from a variable, the for_each loop, which you’ll learn about later in this tutorial, is a much better choice.

      Referencing the Current Resource (self)

      Another downside of count is that referencing an arbitrary instance of a resource by its index is not possible in some cases.

      The main example is destroy-time provisioners, which run when the resource is planned to be destroyed. The reason is that the requested instance may not exist (it’s already destroyed) or would create a mutual dependency cycle. In such situations, instead of referring to the object through the list of instances, you can access only the current resource through the self keyword.

      To demonstrate its usage, you’ll now add a destroy-time local provisioner to the test_droplet definition, which will show a message when run. Open droplets.tf for editing:

      Add the following highlighted lines:

      terraform-flexibility/droplets.tf

      resource "digitalocean_droplet" "test_droplet" {
        count  = length(var.droplet_names)
        image  = "ubuntu-18-04-x64"
        name   =  var.droplet_names[count.index]
        region = "fra1"
        size   = "s-1vcpu-1gb"
      
        provisioner "local-exec" {
          when    = destroy
          command = "echo 'Droplet ${self.name} is being destroyed!'"
        }
      }
      

      Save and close the file.

      The local-exec provisioner runs a command on the local machine Terraform is running on. Because the when parameter is set to destroy, it will run only when the resource is going to be destroyed. The command it runs echoes a string to stdout, which substitutes the name of the current resource using self.name.

      Because you’ll be creating the Droplets in a different way in the next section, destroy the currently deployed ones by running the following command:

      • terraform destroy -var "do_token=${DO_PAT}"

      Enter yes when prompted. You’ll receive the local-exec provisioner being run four times:

      Output

      ... digitalocean_droplet.test_droplet["first"] (local-exec): Executing: ["/bin/sh" "-c" "echo 'Droplet first is being destroyed!'"] digitalocean_droplet.test_droplet["second"] (local-exec): Executing: ["/bin/sh" "-c" "echo 'Droplet second is being destroyed!'"] digitalocean_droplet.test_droplet["second"] (local-exec): Droplet second is being destroyed! digitalocean_droplet.test_droplet["third"] (local-exec): Executing: ["/bin/sh" "-c" "echo 'Droplet third is being destroyed!'"] digitalocean_droplet.test_droplet["third"] (local-exec): Droplet third is being destroyed! digitalocean_droplet.test_droplet["fourth"] (local-exec): Executing: ["/bin/sh" "-c" "echo 'Droplet fourth is being destroyed!'"] digitalocean_droplet.test_droplet["fourth"] (local-exec): Droplet fourth is being destroyed! digitalocean_droplet.test_droplet["first"] (local-exec): Droplet first is being destroyed! ...

      In this step, you learned the disadvantages of count. You’ll now learn about the for_each loop construct, which overcomes them and works on a wider array of variable types.

      Looping Using for_each

      In this section, you’ll consider the for_each loop, its syntax, and how it helps flexibility when defining resources with multiple instances.

      for_each is a parameter available on each resource, but unlike count, which requires a number of instances to create, for_each accepts a map or a set. Each element of the provided collection is traversed once and an instance is created for it. for_each makes the key and value available under the each keyword as attributes (the pair’s key and value as each.key and each.value, respectively). When a set is provided, the key and value will be the same.

      Because it provides the current element in the each object, you won’t have to manually access the desired element as you did with lists. In case of sets, that’s not even possible, as it has no observable ordering internally. Lists can also be passed in, but they must first be converted into a set using the toset function.

      The main advantage of using for_each, aside from being able to enumerate all three collection data types, is that only the actually affected elements will be modified, created, or deleted. If you change the order of the elements in the input, no actions will be planned, and if you add, remove, or modify an element from the input, appropriate actions will be planned only for that element.

      Let’s convert the Droplet resource from count to for_each and see how it works in practice. Open droplets.tf for editing by running:

      Modify the highlighted lines:

      terraform-flexibility/droplets.tf

      resource "digitalocean_droplet" "test_droplet" {
        for_each = toset(var.droplet_names)
        image    = "ubuntu-18-04-x64"
        name     = each.value
        region   = "fra1"
        size     = "s-1vcpu-1gb"
      }
      

      You can remove the local-exec provisioner. When you’re done, save and close the file.

      The first line replaces count and invokes for_each, passing in the droplet_names list in the form of a set using the toset function, which automatically converts the given input. For the Droplet name, you specify each.value, which holds the value of the current element from the set of Droplet names.

      Plan the project by running:

      • terraform plan -var "do_token=${DO_PAT}"

      The output will detail steps Terraform would take:

      Output

      ... An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # digitalocean_droplet.test_droplet["first"] will be created + resource "digitalocean_droplet" "test_droplet" { ... + name = "first" ... } # digitalocean_droplet.test_droplet["fourth"] will be created + resource "digitalocean_droplet" "test_droplet" { ... + name = "fourth" ... } # digitalocean_droplet.test_droplet["second"] will be created + resource "digitalocean_droplet" "test_droplet" { ... + name = "second" ... } # digitalocean_droplet.test_droplet["third"] will be created + resource "digitalocean_droplet" "test_droplet" { ... + name = "third" ... } # digitalocean_droplet.test_droplet["zero"] will be created + resource "digitalocean_droplet" "test_droplet" { ... + name = "zero" ... } Plan: 5 to add, 0 to change, 0 to destroy. ...

      Unlike when using count, Terraform now considers each instance individually, and not as elements of an ordered list. Every instance is linked to an element of the given set, as signified by the shown string element in the brackets next to each resource that will be created.

      Apply the plan to the cloud by running:

      • terraform apply -var "do_token=${DO_PAT}"

      Enter yes when prompted. When it finishes, you’ll remove one element from the droplet_names list to demonstrate that other instances won’t be affected. Open variables.tf for editing:

      Modify the list to look like this:

      terraform-flexibility/variables.tf

      variable "droplet_names" {
        type    = list(string)
        default = ["first", "second", "third", "fourth"]
      }
      

      Save and close the file.

      Plan the project again, and you’ll receive the following output:

      Output

      ... An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: - destroy Terraform will perform the following actions: # digitalocean_droplet.test_droplet["zero"] will be destroyed - resource "digitalocean_droplet" "test_droplet" { ... - name = "zero" -> null ... } Plan: 0 to add, 0 to change, 1 to destroy. ...

      This time, Terraform would destroy only the removed instance (zero), and would not touch any of the other instances, which is the correct behavior.

      In this step, you’ve learned about for_each, how to use it, and its advantages over count. Next, you’ll learn about the for loop, its syntax and usage, and when it can be used to automate certain tasks.

      Looping Using for

      The for loop works on collections, and creates a new collection by applying a transformation to each element of the input. The exact type of the output will depend on whether the loop is surrounded by brackets ([]) or braces ({}), which give a list or a map, respectively. As such, it is suitable for querying resources and forming structured outputs for later processing.

      The general syntax of the for loop is:

      for element in collection:
      transform(element)
      if condition
      

      Similarly to other programming languages, you first name the traversal variable (element) and specify the collection to enumerate. The body of the loop is the transformational step, and the optional if clause can be used for filtering the input collection.

      You’ll now work through a few examples using outputs. You’ll store them in a file named outputs.tf. Create it for editing by running the following command:

      Add the following lines to output pairs of deployed Droplet names and their IP addresses:

      terraform-flexibility/outputs.tf

      output "ip_addresses" {
        value = {
          for instance in digitalocean_droplet.test_droplet:
          instance.name => instance.ipv4_address
        }
      }
      

      This code specifies an output called ip_addresses, and specifies a for loop that iterates over the instances of the test_droplet resource you’ve been customizing in the previous steps. Because the loop is surrounded by curly brackets, its output will be a map. The transformational step for maps is similar to lambda functions in other programming languages, and here it creates a key-value pair by combining the instance name as the key with its private IP as its value.

      Save and close the file, then refresh Terraform state to account for the new output by running:

      • terraform refresh -var "do_token=${DO_PAT}"

      The Terraform refresh command updates the local state with the actual infrastructure state in the cloud.

      Then, check the contents of the outputs:

      Output

      ip_addresses = { "first" = "ip_address" "fourth" = "ip_address" "second" = "ip_address" "third" = "ip_address" }

      Terraform has shown the contents of the ip_addresses output, which is a map constructed by the for loop. (The order of the entries may be different for you.) The loop will work seamlessly for every number of entries—meaning that you can add a new element to the droplet_names list and the new Droplet, which would be created without any further manual input, would also show up in this output automatically.

      By surrounding the for loop in square brackets, you can make the output a list. For example, you could output only Droplet IP addresses, which is useful for external software that may be parsing the data. The code would look like this:

      terraform-flexibility/outputs.tf

      output "ip_addresses" {
        value = [
          for instance in digitalocean_droplet.test_droplet:
          instance.ipv4_address
        ]
      }
      

      Here, the transformational step simply selects the IP address attribute. It would give the following output:

      Output

      ip_addresses = [ "ip_address", "ip_address", "ip_address", "ip_address", ]

      As was noted before, you can also filter the input collection using the if clause. Here is how you would write the loop if you’d filter it by the fra1 region:

      terraform-flexibility/outputs.tf

      output "ip_addresses" {
        value = [
          for instance in digitalocean_droplet.test_droplet:
          instance.ipv4_address
          if instance.region == "fra1"
        ]
      }
      

      In HCL, the == operator checks the equality of the values of the two sides—here it checks if instance.region is equal to fra1. If it is, the check passes and the instance is transformed and added to the output, otherwise it is skipped. The output of this code would be the same as the prior example, because all Droplet instances are in the fra1 region, according to the test_droplet resource definition. The if conditional is also useful when you want to filter the input collection for other values in your project, like the Droplet size or distribution.

      Because you’ll be creating resources differently in the next section, destroy the currently deployed ones by running the following command:

      • terraform destroy -var "do_token=${DO_PAT}"

      Enter yes when prompted to finish the process.

      We’ve gone over the for loop, its syntax, and examples of usage in outputs. You’ll now learn about conditionals and how they can be used together with count.

      Directives and Conditionals

      In one of the previous sections, you’ve seen the count key and how it works. You’ll now learn about ternary conditional operators, which you can use elsewhere in your Terraform code, and how they can be used with count.

      The syntax of the ternary operator is:

      condition ? value_if_true : value_if_false
      

      condition is an expression that computes to a boolean (true or false). If the condition is true, then the expression evaluates to value_if_true. On the other hand, if the condition is false, the result will be value_if_false.

      The main use of ternary operators is to enable or disable single resource creation according to the contents of a variable. This can be achieved by passing in the result of the comparison (either 1 or 0) to the count key on the desired resource.

      Let’s add a variable called create_droplet, which will control if a Droplet will be created. First, open variables.tf for editing:

      Add the highlighted lines:

      terraform-flexibility/variables.tf

      variable "droplet_names" {
        type    = list(string)
        default = ["first", "second", "third", "fourth"]
      }
      
      variable "create_droplet" {
        type = bool
        default = true
      }
      

      This code defines the create_droplet variable of type bool. Save and close the file.

      Then, to modify the Droplet declaration, open droplets.tf for editing by running:

      Modify your file like the following:

      terraform-flexibility/droplets.tf

      resource "digitalocean_droplet" "test_droplet" {
        count  = var.create_droplet ? 1 : 0
        image  = "ubuntu-18-04-x64"
        name   =  "test_droplet"
        region = "fra1"
        size   = "s-1vcpu-1gb"
      }
      

      For count, you use a ternary operator to return either 1 if the create_droplet variable is true, and 0 if false, which will result in no Droplets being provisioned. Save and close the file when you’re done.

      Plan the project execution plan with the variable set to false by running:

      • terraform plan -var "do_token=${DO_PAT}" -var "create_droplet=false"

      You’ll receive the following output:

      Output

      Refreshing Terraform state in-memory prior to plan... The refreshed state will be used to calculate this plan, but will not be persisted to local or remote state storage. ------------------------------------------------------------------------ No changes. Infrastructure is up-to-date. This means that Terraform did not detect any differences between your configuration and real physical resources that exist. As a result, no actions need to be performed.

      Because create_droplet was passed in the value of false, the count of instances is 0, and no Droplets will be created.

      You’ve reviewed how to use the ternary conditional operator together with the count key to enable a higher level of flexibility in choosing whether to deploy desired resources. Next you’ll learn about explicitly setting resource dependencies for your resources.

      Explicitly Setting Resource Dependencies

      While creating the execution plan for your project, Terraform detects dependency chains between resources and implicitly orders them so that they will be built in the appropriate order. In the majority of cases, it is able to detect relationships by scanning all expressions in resources and building a graph.

      However, when one resource requires access control settings to already be deployed at the cloud provider, in order to be provisioned, there is no clear sign to Terraform that they are related. In turn, Terraform will not know they are dependent on each other behaviorally. In such cases, the dependency must be manually specified using the depends_on argument.

      The depends_on key is available on each resource and used to specify to which resources one has hidden dependency links. Hidden dependency relationships form when a resource depends on another one’s behavior, without using any of its data in its declaration, which would prompt Terraform to connect them one way.

      Here is an example of how depends_on is specified in code:

      resource "digitalocean_droplet" "droplet" {
        image  = "ubuntu-18-04-x64"
        name   = "web"
        region = "fra1"
        size   = "s-1vcpu-1gb"
      
        depends_on = [
          # Resources...
        ]
      }
      

      It accepts a list of references to other resources, and it does not accept arbitrary expressions.

      depends_on should be used sparingly, and only when all other options are exhausted. Its use signifies that what you are trying to declare is stepping outside the boundaries of Terraform’s automated dependency detection system; it may signify that the resource is explicitly depending on more resources than it needs to.

      You’ve now learned about explicitly setting additional dependencies for a resource using the depends_on key, and when it should be used.

      Conclusion

      In this article, we’ve gone over the features of HCL that improve flexibility and scalability of your code, such as count for specifying the number of resource instances to deploy and for_each as an advanced way of looping over collection data types and customizing instances. When used correctly, they greatly reduce code duplication and operational overhead of managing the deployed infrastructure.

      You’ve also learned about conditionals and ternary operators, and how they can be utilized to control if a resource will get deployed. While Terraform’s automated dependency analysis system is quite capable, there may be cases where you need to manually specify resource dependencies using the depends_on key.

      To learn more about Terraform, check out our How To Manage Infrastructure with Terraform series.



      Source link