2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[RTA] web アプリ素人が AI の力を借りて簡素な入力フォームを作ってみた

Last updated at Posted at 2024-03-23

目的

 昨今の AI 技術の発展は目覚ましく、ソフトウェアエンジニアの開発環境や開発者体験も AI の力を借りて発展している。そこで、以下のことに挑戦してみる

  • web アプリ素人が AI に頼り切った状態で "とりあえず動く" web アプリを作れるか? 作れるならどれくらいの時間がかかるか?

 RTA と題しているが、途中で普通に家事などするので RT ではない。あくまで最速を目指すため AI に頼り切ることがコンセプトである。

なお、筆者の技能としては以下の通り

最終目標

 目指す web アプリは以下の通り

  • すべて docker でホストする
  • 商品名とその値段を登録/削除するフォームを提供する
  • 登録済み商品と値段を表示するページを表示する
  • フロントエンドは React、流行っていそうだから
  • バックエンドは Rust、流行っていそうだから
  • データベースは AI におまかせ

リポジトリは https://github.com/HalDoggy/web-app

結果

 所要時間は約19時間だった。一日8時間労働でこれだけにフルコミットすると考えると2日半ほどの所要時間である。

経過

開発環境準備 (約1時間)

以下のことをした

  • Rust の準備 https://www.rust-lang.org/ja/learn/get-started
  • docker desktop インストール
  • VS code extension
    • React
      • Copilot 曰く
        1. ES7 React/Redux/GraphQL/React-Native snippets
        2. Auto Rename Tag
        3. VSCode React Refactor
    • Rust
      • Copilot 曰く
        1. rust-analyzer
    • docker
      • Copilot 曰く
        1. Docker
        2. Dev Containers
        3. Docker Explorer
        4. Docker Compose
  • GitHub Copilot セットアップ
    • 初月無料、以降月10ドル

Coplilot に how to というより、アドバイスをもらったに過ぎなかった。

フロントエンド開発準備 (約1.5時間)

 Copilot への質問は「react 開発環境を docker で作りたい」。返事は

Docker を使って React の開発環境を構築する方法を説明します。以下の手順に従って進めてみてください。

  1. Docker のインストール:
    - もしインストールしていない場合は、Docker 公式サイト から Docker をインストールしてください。
  2. Dockerfile の作成:
    - 作業用フォルダ内に Dockerfile という名前のファイルを作成します。
    - 以下は、Dockerfile の中身の例です:
    # Node.js の最新版をベースにする
    FROM node:14.17.0
    
    # コンテナ内の作業ディレクトリを指定
    WORKDIR /usr/src/app
    
  3. docker-compose.yml の作成:
    - 作業用フォルダ内に docker-compose.yml という名前のファイルを作成します。
    - 以下は、docker-compose.yml の中身の例です:
    version: '3.8'
    services:
      node:
        build:
          context: .
          dockerfile: Dockerfile
        volumes:
          - ./:/usr/src/app
        command: sh -c "cd {プロジェクトフォルダ名} && yarn start"
        ports:
          - "3000:3000"
    
  4. React のインストールとプロジェクトの作成:
    - 作業用フォルダ内で以下のコマンドを実行して React のプロジェクトを作成します:
    docker-compose run --rm node sh -c "npm install -g create-react-app && create-react-app {プロジェクト名}"
    
  5. Docker を起動:
    - 以下のコマンドで Docker を起動します:
    docker-compose up
    
  6. React を実行:
    - ブラウザで http://localhost:3000 にアクセスして React プロジェクト画面が立ち上がるか確認してください。

これで Docker を使った React の開発環境が構築されました!

プロジェクトフォルダ名 としては frontend-react とでもする。Copilot が前提とするディレクトリ構成と自分のそれが異なるので、その分手を加えた。
コミットの内容は https://github.com/HalDoggy/web-app/commit/c699a022d097f8ae6e56a54bba3ee5edd362afa3 の通り。tools/start-dev-frontend.ps1 を実行した。

これで React のデフォページは表示できた。

しかしホットリロードができない。Copilot に尋ねたが解決せず、結局 https://qiita.com/chora_8192/items/c5838a343a17ccb44d24 の解決策を使った。

フロントエンド開発 (約0.5時間)

 入力フォームを作る。イメージはこんな感じ
image.png

  • Copilot への質問

    react で入力フォームを作りたい。入力欄は以下の通り;
    入力欄1: 名前は "商品名"、値は文字列
    入力欄2: 名前は "値段"、値は正の整数
    ボタン: 名前は"追加"

  • Copilot の回答

    Reactで入力フォームを作成する際、以下の手順に従って実装できます。

    1. プロジェクトのセットアップ:
      - まず、Reactプロジェクトを作成します。以下のコマンドを実行して、新しいReactアプリケーションを作成します。

      npx create-react-app my-form-app
      cd my-form-app
      
    2. フォームコンポーネントの作成:

      • src フォルダ内に Form.js ファイルを作成します。
      • Form.js 内で以下のようにフォームコンポーネントを作成します。
      // src/Form.js
      import React, { useState } from 'react';
      
      const Form = () => {
        const [productName, setProductName] = useState('');
        const [price, setPrice] = useState('');
      
        const handleAdd = () => {
          // ここでフォームの値を使って追加の処理を実行する
          console.log('商品名:', productName);
          console.log('値段:', price);
        };
      
        return (
          <div>
            <label>商品名</label>
            <input
              type="text"
              value={productName}
              onChange={(e) => setProductName(e.target.value)}
            />
            <br />
            <label>値段</label>
            <input
              type="number"
              value={price}
              onChange={(e) => setPrice(e.target.value)}
            />
            <br />
            <button onClick={handleAdd}>追加</button>
          </div>
        );
      };
      
      export default Form;
      
    3. Appコンポーネントでフォームを表示:

      • src/App.js ファイルで Form コンポーネントを表示します。
      // src/App.js
      import React from 'react';
      import './App.css';
      import Form from './Form';
      
      function App() {
        return (
          <div className="App">
            <Form />
          </div>
        );
      }
      export default App;
      
    4. スタイルの追加:

      • 必要に応じてスタイルを追加します。例えば、src/Form.css ファイルを作成してスタイルを適用できます。
      /* src/Form.css */
      .App {
        text-align: center;
      }
      input {
        margin: 5px;
      }
      button {
        margin-top: 10px;
      }
      
    5. フォームの値を利用する:

      • handleAdd 関数内でフォームの値を使って追加の処理を実行してください。

    これで、Reactアプリケーション内で入力フォームを作成できます。フォームの値を適切に処理するために、handleAdd 関数をカスタマイズしてください。¹²³

やってみよう。

image.png
お見事。

ただ、Form.js を編集中、GitHub Copilot の提案したコードは以下のもので、

import React, {useState} from 'react'; 

const Form = () => {
    const [productName, setProductName] = useState('');
    const [productPrice, setProductPrice] = useState('');

    const handleAdd = () => {
        console.log(productName, productPrice);
    };

    return (
        <div>
            <input type="text" placeholder="Product Name" value={productName} onChange={(e) => setProductName(e.target.value)} />
            <input type="text" placeholder="Product Price" value={productPrice} onChange={(e) => setProductPrice(e.target.value)} />
            <button onClick={handleAdd}>Add</button>
        </div>
    );
};

export default Form;

このようになった。
image.png

せっかくなのでこっちにしよう。

 しかし、このままだと値段欄が数値以外も受け付けてしまう。

  • Copilot への質問

    値段入力欄は正の整数だけ受け付けるようにしたい。どうすれば?

  • Copilot の回答

    値段入力欄が正の整数だけを受け付けるようにするには、以下の手順を実行します。

    1. フォームの入力制限を設定:

      • input 要素に type="number" を指定して、数値のみを受け付けるようにします。
      • input 要素に min="0" を指定して、正の整数のみを受け付けるようにします。
    2. フォームコンポーネントを修正:

      • Form.js ファイル内で、price の入力欄に対して上記の設定を追加します。
      // src/Form.js
      import React, { useState } from 'react';
      
      const Form = () => {
        const [productName, setProductName] = useState('');
        const [price, setPrice] = useState('');
      
        const handleAdd = () => {
          // ここでフォームの値を使って追加の処理を実行する
          console.log('商品名:', productName);
          console.log('値段:', price);
        };
      
        return (
          <div>
            <label>商品名</label>
            <input
              type="text"
              value={productName}
              onChange={(e) => setProductName(e.target.value)}
            />
            <br />
            <label>値段</label>
            <input
              type="number"
              value={price}
              onChange={(e) => setPrice(e.target.value)}
              min="0" // 正の整数のみを受け付ける
            />
           <br />
          <button onClick={handleAdd}>追加</button>
       </div>
        );
      };
      
      export default Form;
      

    これで、price 入力欄は正の整数のみを受け付けるようになります。

これでそんなに悪くはないが、負の値をキーボード入力できる。結局こうした。

<input type="number" placeholder="Product Price" value={productPrice} 
            onChange={(e) => 
                {
                    if(e.target.value < 0) {
                        e.target.value = 0;
                    }
                    setProductPrice(e.target.value)
                }} 
            min="0"/>

削除処理のことを忘れていたので改めて

  • Copilot への質問

    react で商品情報追加/編集/削除フォームを作りたい。UIの様子は以下の通り;
    編集フォーム1行では:
    入力欄1: 名前は "商品名"、値は文字列
    入力欄2: 名前は "値段"、値は正の整数
    ボタン1: 名前は"追加"
    ボタン2: 名前は"編集"
    ボタン3: 名前は"削除"
    編集フォーム全体は、この1行がバックエンドから与えられた商品数分縦に連なっている。

  • Copilot の回答

    商品情報の追加、編集、削除フォームを作成するために、以下の手順を実行します。

    1. フォームコンポーネントの作成:

      • src フォルダ内に ProductForm.js ファイルを作成します。
      • ProductForm.js 内で、商品情報の入力欄とボタンを作成します。
      // src/ProductForm.js
      import React, { useState } from 'react';
      
      const ProductForm = ({ products, onAdd, onEdit, onDelete }) => {
        const [productName, setProductName] = useState('');
        const [price, setPrice] = useState('');
      
        const handleAdd = () => {
          // 商品情報を追加する処理
          onAdd({ name: productName, price: price });
          setProductName('');
          setPrice('');
        };
      
        return (
          <div>
            <input
              type="text"
              placeholder="商品名"
              value={productName}
              onChange={(e) => setProductName(e.target.value)}
            />
            <input
              type="number"
              placeholder="値段"
              value={price}
              onChange={(e) => setPrice(e.target.value)}
            />
            <button onClick={handleAdd}>追加</button>
          </div>
        );
      };
      
      export default ProductForm;
      
    2. トップレベルのコンポーネントで商品情報を表示:

      • トップレベルのコンポーネント(例えば App.js)で、商品情報を表示します。
      // src/App.js
      import React, { useState } from 'react';
      import ProductForm from './ProductForm';
      
      const App = () => {
        const [products, setProducts] = useState([]);
      
        const handleAddProduct = (product) => {
          setProducts([...products, product]);
        };
      
        const handleEditProduct = (index, updatedProduct) => {
          const newProducts = [...products];
          newProducts[index] = updatedProduct;
          setProducts(newProducts);
        };
      
        const handleDeleteProduct = (index) => {
          const newProducts = [...products];
          newProducts.splice(index, 1);
          setProducts(newProducts);
        };
      
        return (
          <div>
            {products.map((product, index) => (
              <div key={index}>
                <span>{product.name} - {product.price}</span>
                <button onClick={() => handleEditProduct(index, { ...product, name: '新しい商品名' })}>編集</button>
                <button onClick={() => handleDeleteProduct(index)}>削除</button>
              </div>
            ))}
            <ProductForm
              products={products}
              onAdd={handleAddProduct}
              onEdit={handleEditProduct}
              onDelete={handleDeleteProduct}
            />
          </div>
        );
      };
      
      export default App;
      
    3. 商品情報の表示と操作:

      • App.js 内で商品情報を表示し、編集と削除の操作を実装します。

    これで、商品情報の追加、編集、削除フォームが作成され、バックエンドから与えられた商品数分だけ縦に連なって表示されます。¹²³

結局編集処理は面倒だったので無視した。先のコードと整合するように提案を取り入れてこうなった

image.png

バックエンド開発 (約6時間)

 追加/削除処理を受け付けたい。まだ DB がないので、要求事項と引数を受け取り、それに応じた action string を返す API にしてみる。

  • Copilot への質問

    バックエンドを Rust で作ります。Add という名前の API で、商品名と値段を受け取り、文字列を返すものを作ってください。

  • Copilot の回答

    もちろん、Rustでバックエンドを作成し、商品情報を受け取って文字列を返すAPIを作成します。以下は、簡単な例です。

    1. Rustプロジェクトのセットアップ:

      • まず、Rustプロジェクトを作成します。以下のコマンドを実行して、新しいRustプロジェクトを作成します。
      cargo new my-backend
      cd my-backend
      
    2. APIの実装:

      • src/main.rs ファイル内で、商品情報を受け取って文字列を返すAPIを実装します。
      // src/main.rs
      use actix_web::{web, App, HttpResponse, HttpServer, Responder};
      
      async fn add_product(info: web::Json<ProductInfo>) -> impl Responder {
          let response = format!("商品名: {}, 値段: {}", info.name, info.price);
          HttpResponse::Ok().body(response)
      }
      
      #[derive(serde::Deserialize)]
      struct ProductInfo {
          name: String,
          price: u32,
      }
      
      #[actix_rt::main]
      async fn main() -> std::io::Result<()> {
          HttpServer::new(|| {
              App::new()
                  .route("/add", web::post().to(add_product))
          })
          .bind("127.0.0.1:8080")?
          .run()
          .await
      }
      
    3. サーバーの起動:

      • ターミナルで以下のコマンドを実行して、サーバーを起動します。
      cargo run
      
    4. APIのテスト:

      • ブラウザやcurlなどのツールを使用して、http://127.0.0.1:8080/add にPOSTリクエストを送信して、商品情報を追加できることを確認してください。

    これで、RustでバックエンドAPIを作成し、商品情報を受け取って文字列を返すAPIが完成しました。¹²³⁴⁵

このサービスをホストする docker コンテナの作り方を訊く

Copilot への質問
このサービスをホストするdockerコンテナを作ってください
Copilot の回答
もちろん、Dockerコンテナを作成してサービスをホストする方法を説明します。以下は、Rustで作成したバックエンドサービスをDockerコンテナでホストする手順です。

1. **Dockerfileの作成**:
   - プロジェクトのルートディレクトリに `Dockerfile` という名前のファイルを作成します。

     ```Dockerfile
     # Dockerfile
     FROM rust:latest

     WORKDIR /usr/src/my-backend
     COPY . .

     RUN cargo build --release

     CMD ["cargo", "run"]
     ```

2. **Dockerイメージのビルド**:
   - ターミナルで以下のコマンドを実行して、Dockerイメージをビルドします。

     ```
     docker build -t my-backend .
     ```

3. **Dockerコンテナの実行**:
   - 以下のコマンドを実行して、Dockerコンテナを起動します。

     ```
     docker run -p 8080:8080 my-backend
     ```

4. **サービスのアクセス**:
   - ブラウザやcurlなどのツールを使用して、`http://localhost:8080/add` にアクセスして、商品情報を追加できることを確認してください。

これで、Rustで作成したバックエンドサービスがDockerコンテナでホストされています。¹²³⁴

実はこの HttpServer::new のところの設定は、今回のような docker コンテナでホストするときには使えない。docker コンテナは 0.0.0.0:8080 -> <コンテナIPアドレス>:8080 のようにポートフォワーディング設定になっている(何も特別に設定しなければ)。

また、今回はフロントエンドもバックエンドも localhost にあるので、CORSポリシーによってフロントエンドからバックエンドへのアクセスがブロックされる。この設定も必要となる。よって、最終的には

修正版
     #[actix_rt::main]
     async fn main() -> std::io::Result<()> {
         HttpServer::new(|| {
             App::new().wrap(
                    Cors::permissive()
                    .allowed_methods(vec!["POST", "GET"])
                    .supports_credentials()
                    .max_age(3600)
                )
                 .route("/add", web::post().to(add_product))
         })
         .bind("0.0.0.0:8080")?
         .run()
         .await
     }

となる。これは Copilot では解決できなかった。
また、docker-compose.yaml にポート設定を書いたり、Cargo.toml を修正したりする。

commit は https://github.com/HalDoggy/web-app/compare/c3f419cbc392fcf6d455859b7d93f4275e30ed82...8c12f1e9e6804992faea9fb9502060b9591556ef。

DB 準備 (約0.5時間)

 DB は何を使うとよいだろう?

  • Copilot への質問

    簡単にwebアプリケーションを作りたいです。データベースは何を使うとよいですか? ただし、docker コンテナでホストします。

  • Copilot からの回答

    WebアプリケーションをDockerコンテナでホストする際に、データベースとしてどのものを使用するかは重要です。以下の選択肢があります。

    1. PostgreSQL:

      • PostgreSQLは広く使われているオープンソースのリレーショナルデータベースです。
      • Dockerコンテナ内でPostgreSQLを実行でき、どのプラットフォームでも利用できます。
    2. MySQL:

      • MySQLは人気のあるオープンソースのリレーショナルデータベースです。
      • Dockerコンテナ内でMySQLを実行できます。
    3. MongoDB:

      • MongoDBは最も広く使われているNoSQLデータベースです。
      • Dockerコンテナ内でMongoDBを実行でき、依存関係やライブラリと一緒にどのプラットフォームでも利用できます。
    4. SQLite:

      • 軽量で埋め込み型のデータベースで、単一のファイルにデータを格納します。
      • Dockerコンテナ内でSQLiteを実行できます。
    5. Amazon RDS:

      • Amazon RDSはフルマネージドのリレーショナルデータベースサービスで、AWSのRDSを使用することでDockerコンテナ内でデータベースを実行できます。

    注意点:

    • データベースをDockerコンテナ内で実行する場合、データの永続性とバックアップ戦略を考慮してください。
    • データベースのセキュリティ設定やパスワードは適切に管理してください。

    どのデータベースを選択するかは、アプリケーションの要件やプロジェクトのニーズによります。

決め手がわからなかったので追加質問

  • Copilot への質問

    Rust からアクセスしようと思いますが、その中だとどれが簡単ですか?

  • Copilot からの回答

    Rustでデータベースにアクセスする際に、簡単な選択肢をいくつか紹介します。これらは、Dockerコンテナ内で実行できるものです。

    1. SQLite:

      • 軽量で埋め込み型のデータベースです。単一のファイルにデータを格納します。
      • Rustのライブラリであるrusqliteを使用して、Dockerコンテナ内でSQLiteを実行できます。
    2. PostgreSQL:

      • PostgreSQLは広く使われているオープンソースのリレーショナルデータベースです。
      • Dockerコンテナ内でPostgreSQLを実行できます。Rustのライブラリであるsqlxを使用することで、簡単にアクセスできます。
    3. MySQL:

      • MySQLは人気のあるオープンソースのリレーショナルデータベースです。
      • Dockerコンテナ内でMySQLを実行できます。Rustのライブラリであるmysql_asyncを使用することで、簡単にアクセスできます。
    4. SeaORM:

      • SeaORMはRustと各種RDBを接続するためのクレート(ライブラリ)です。SQLx上に構築されたORMであり、多くのDBエンジンと遅延読み込みをサポートしています。
      • Dockerコンテナ内でSeaORMを使用してデータベースにアクセスできます。

    選択肢はプロジェクトの要件や好みによりますが、SQLiteやPostgreSQLが初心者にとって簡単な選択肢となることが多いです。

 PostgreSQL にしてみる。聞いたことがあるから。知見が多くインターネット上にあれば、Copilot も働きやすいだろう。

 では作ってゆく。

  • Copilot への質問

    PostgreSQL を使うことにします。これをホストする docker コンテナを作り、商品名と値段を格納するテーブルを作ってください。

  • Copilot からの回答

    もちろんです!以下は、Dockerコンテナ内でPostgreSQLをホストし、商品名と値段を格納するテーブルを作成する手順です。

    1. Docker Composeファイルの作成:

      • プロジェクトのルートディレクトリに docker-compose.yml ファイルを作成します。
      version: "3"
      services:
        db:
          image: postgres:latest
          container_name: "my_postgres"
          environment:
            POSTGRES_USER: myuser
            POSTGRES_PASSWORD: mypassword
            POSTGRES_DB: mydb
          ports:
            - "5432:5432"
          volumes:
            - ./pgdata:/var/lib/postgresql/data
      
      • 上記の設定で、PostgreSQLコンテナがポート5432でホストされ、データベース名はmydb、ユーザー名はmyuser、パスワードはmypasswordです。
    2. Dockerコンテナの起動:

      • ターミナルでプロジェクトのルートディレクトリに移動し、以下のコマンドを実行してDockerコンテナを起動します。
      docker-compose up -d
      
    3. PostgreSQLに接続:

      • ターミナルで以下のコマンドを実行して、PostgreSQLコンテナに接続します。
      docker exec -it my_postgres psql -U myuser -d mydb
      
    4. テーブルの作成:

      • PostgreSQLコンテナ内で以下のSQLコマンドを実行して、商品情報を格納するテーブルを作成します。
      CREATE TABLE products (
          product_id SERIAL PRIMARY KEY,
          product_name VARCHAR(255) NOT NULL,
          price DECIMAL(10, 2) NOT NULL
      );
      
      • このテーブルは、商品名と値段を格納するためのものです。

    これで、Dockerコンテナ内でPostgreSQLが実行され、商品情報を格納するテーブルが作成されました。

    せっかくなので SQL 文は GitHub Copilot Chant にも訊いてみる。英語は雑め。

  • GitHub Copilot Chant への質問

    how to make a table of postgresql? It has three columns, product id, product name and price.

  • GitHub Copilot Chant の回答

    CREATE TABLE table_name (
    product_id SERIAL PRIMARY KEY,
    product_name VARCHAR(255),
    price DECIMAL(10, 2)
    );
    

Copilot の方が言外の、普通みんなやるであろう設定も盛り込んでくれる様子。 DECIMAL(10, 2)INT に変えて Copilot の方を採用。

試しに適当なデータを入れてみる。

  • GitHub Copilot Chant への質問

    how to add data to existing table?

  • GitHub Copilot Chant の回答

    INSERT INTO products (product_id, product_name, price)
    VALUES (1, 'Product A', 10),
           (2, 'Product B', 19),
           (3, 'Product C', 15);
    

SELECT * FROM products LIMIT 10; でデータ挿入を確認した。

DB - バックエンド通信 (約7時間)

 バックエンドからDBに対して以下のことをする

  1. テーブルにあるすべての商品情報を読みだしてjsonに加工する
  2. 商品名と値段が渡され、これをテーブルに追加する

順に取り組もう。

テーブルにあるすべての商品情報を読みだしてjsonに加工する

  • Copilot への質問

    rust で、テーブルにある全レコードを取得するには?postgresqlです。

  • Copilot の回答

    RustでPostgreSQLのテーブルから全レコードを取得するには、以下のステップを実行します。

    1. データベース接続の設定:

      • RustでPostgreSQLに接続するために、tokio-postgresクレートを使用します。
      • .envファイルなどで接続情報を設定します。
    2. クエリを実行して全レコードを取得:

      • SELECT * FROM table_name;のクエリを実行して、テーブルから全レコードを取得します。

    以下は、RustでPostgreSQLのテーブルから全レコードを取得する例です。

    use dotenv::dotenv;
    use std::env;
    use tokio_postgres::{NoTls, Error};
    
    #[derive(Debug)]
    struct MyData {
       id: i32,
       name: String,
    }
    
    #[tokio::main]
    async fn main() -> Result<(), Error> {
       // .envファイルを読み込む
       dotenv().ok();
    
       // 環境変数から接続情報を取得
       let db_host = env::var("DB_HOST").expect("DB_HOST not set");
       let db_port = env::var("DB_PORT").expect("DB_PORT not set");
       let db_user = env::var("DB_USER").expect("DB_USER not set");
       let db_password = env::var("DB_PASSWORD").expect("DB_PASSWORD not set");
       let db_name = env::var("DB_NAME").expect("DB_NAME not set");
    
       // PostgreSQLの接続情報を設定
       let connection_string = format!(
           "host={} port={} user={} password={} dbname={}",
           db_host, db_port, db_user, db_password, db_name
       );
    
       let (client, connection) = tokio_postgres::connect(&connection_string, NoTls).await?;
    
       // 接続タスクをスポーンして実行
       tokio::spawn(async move {
           if let Err(e) = connection.await {
               eprintln!("connection error: {}", e);
           }
       });
    
       // テーブルから全レコードを取得
       let rows = client.query("SELECT * FROM my_table", &[]).await?;
    
       for row in rows {
           let id: i32 = row.get("id");
           let name: String = row.get("name");
           let data = MyData { id, name };
           println!("{:?}", data);
       }
    
       Ok(())
    }
    

    この例では、.envファイルから接続情報を読み込み、my_tableというテーブルから全レコードを取得しています。実際のプロジェクトでは、エラーハンドリングやテーブル名などを適切に設定してください。

とりあえず.envは後回しにした。
Get API を公開するのは Post の時をまねて実装した。

データをテーブルに追加する

  • Copilot への質問

    DBはpostgreSQLです。rust からDBに以下のようなデータを追加します。どう実装しますか?

    1. id, serial型
    2. name, string
      3.price, int
  • Copilot の回答

    PostgreSQLへのデータ追加をRustで実装するために、以下のステップを踏みます。

    1. PostgreSQLに接続: RustプログラムからPostgreSQLデータベースに接続します。
    2. テーブル作成: データを格納するためのテーブルを作成します。
    3. データ挿入: 作成したテーブルにデータを挿入します。

    まず、Rustプロジェクトを作成し、postgres クレートを依存関係に追加します。以下は、PostgreSQLに接続してデータを挿入する例です。

    // main.rs
    
    extern crate postgres;
    
    use postgres::{Client, Error, NoTls};
    
    fn main() -> Result<(), Error> {
       // PostgreSQLへの接続情報
       let db_url = "postgresql://postgres:password@127.0.0.1:5432/testdb";
    
       // PostgreSQLに接続
       let mut client = Client::connect(db_url, NoTls)?;
    
       // テーブル作成
       client.batch_execute("
           CREATE TABLE IF NOT EXISTS my_table (
               id SERIAL PRIMARY KEY,
               name TEXT NOT NULL,
               price INT
           )
       ")?;
    
       // データ挿入
       client.execute(
           "INSERT INTO my_table (name, price) VALUES ($1, $2)",
           &["example_name", &100],
       )?;
    
       println!("Data inserted successfully!");
    
       Ok(())
    }
    

    この例では、my_table というテーブルを作成し、データを挿入しています。id は自動生成されるシリアル型、name は文字列、price は整数型です。接続情報やテーブル名、データの具体的な値は、環境に合わせて適切に設定してください。

    また、PostgreSQLサーバーが実行中であることを確認してください。Dockerを使用している場合は、docker-compose up でPostgreSQLを起動できます。

    このコードは、PostgreSQLデータベースへの接続とデータの操作の基本的な例です。必要に応じてカスタマイズしてください。

テーブルはもうあるので手を加える。また、非同期メソッドに必要な実装も加える。これも Copilot に尋ねた。

データ削除する

Delete 文を叩くだけ。
https://github.com/HalDoggy/web-app/compare/34ad22dab7cb642c4cf147e1a8507256edfbf20f...6e9754e5381d6e6961b964d2856383ef784d91a7

フロントエンド - バックエンド通信 (約1時間)

 実装したAPIを正しく叩くためにフロントエンドを改修する。delete は特に言及することはない。抜粋するとこの通り。

const handleDeleteProduct = async (id, index) => {
    const newProducts = [...products];
    newProducts.splice(index, 1);
    setProducts(newProducts);
    
    try {
      const url = 'http://localhost:8765/delete/' + id;
      const response = await fetch(url, { method: 'DELETE' });
    } catch (error) {
      throw new Error('Failed to delete product, id: ' + id + ', index: ' + index + ' from the backend API');
    }
  };

return (
    <div>
      {products.map((product, index) => (
        <div key={index}>
          <span>{product.name} - {product.price}</span>
          <button onClick={() => handleDeleteProduct(product.id, index)}>削除</button>
        </div>
      ))}
      <Form
        products={products}
        onAdd={handleAddProduct}
        onDelete={handleDeleteProduct}
      />
    </div>
  );

 商品の追加はもうできている。
 ページを開いたときに、DBにある商品を表示しておきたい。

  • GitHub Copilot Chat への質問(概要)

    ページを開いたときにバックエンドから情報を取って products に格納したい

  • GitHub Copilot Chat の提案 + 手直し

      const fetchProducts = async () => {
        try {
          const url = 'http://localhost:8765/products';
          const response = await fetch(url, { method: 'GET' });
          const data = await response.json();
          return data;
        } catch (error) {
          throw new Error('Failed to fetch products');
        }
      };
    
      useEffect(() => {
        // Fetch products from backend API
        fetchProducts()
          .then((data) => setProducts(data))
          .catch((error) => console.log(error));
      }, []);
    

これでできた。
image.png

 削除もできるが、挙動が気に食わない。現状は

  1. 当該の商品に対応する UI を削除する
  2. 削除リクエストをバックエンドに送る

である。削除リクエストを送ったあとに表示を更新する、の方が正しい。これは商品追加も同様。Copilot に尋ねるまでもなくできた。削除の例を以下に示す。

const handleDeleteProduct = async (id, index) => {    
    try {
      const url = 'http://localhost:8765/delete/' + id;
      const response = await fetch(url, { method: 'DELETE' });
    } catch (error) {
      throw new Error('Failed to delete product, id: ' + id + ', index: ' + index + ' from the backend API');
    }

    fetchProducts()
      .then((data) => setProducts(data))
      .catch((error) => console.log(error));
  };

削除リクエストを送ってから、全商品情報を更新しているだけ。追加も同様。

セキュリティや構成ファイル、デプロイのワークフローは詰め切れていないが、以上で当初目指したものは"できた"ので終了とする。

考察

 AI を活用したソフトウェア開発はもう2年ほどなされているだろうから、既知の内容ばかりかもしれないが書く。

  • 良かった点
    • やはり、知識や経験の浅い技術もある程度使える
      • 今回でいえば React。React のご利益にあずかれていたかは不明。
    • 目的達成までが早い
      • AIを使わなければ、目的に近い事例を検索して情報を集めることになるだろう。AI を使えばこの検索とまとめ作業をしてくれるので、それを使うなり、各要素を検索することもできる。AI の返答文自体に自分が知らなかった言葉もあるので、検索のしやすさははるかに良い。
    • 学習のめどが立つ
      • 邪道かもしれないが、やりたいことに近いことの情報が出力されるので、その周辺から学習をすることが可能。最終的にはすべて学ぶことになるのだろうが、目的の早期達成を優先するなら良い。
  • 個人的な課題
    • 結局、各技術要素の勉強は必要となる
      • 当然である。特に、CORS の件は Copilot に頼っても解決できず、ブログやドキュメントを読み漁ることになった。
      • 提案されたコードの正しさを評価するのが困難。特にエラーハンドリング。Rust はとりあえず Err に例外情報を入れてログに出す程度しかできなかった。
  • 今回感じたAIの良い使い方
    • 仕様決定
      • 人間が頑張って話し合えばよい。Copilot からは一般に良いとされる挙動のアドバイスは得られるかもしれない。
    • 設計
      • 実現したいことを正しく言語化できれば、それを Copilot に投げて実現方法を提案してくれる。また、ふさわしいメソッド名やクラス名を決める手立てになる。
    • プログラミング
      • 意味がはっきりしている命名があれば、GitHub Copilot のサポートも受けやすい。また、GitHub Copilot Chat への質問のしやすさも増すだろう。
  • Copilot の感想
    • 検索&要約の代行として素晴らしい性能。あとは使用者の技術力次第。
  • GitHub Copilot, GitHub Copilot Chat の感想
    • 純粋にコーディングのサポートとしてかなり強い。メソッド名を書いた時点でよくある実装が提案される。その提案を多少いじればよいケースが多かった。提案はだいたい芯を食っている印象なので、関心のある実装に注力できる。
    • 今回は Rust を使ったので特にご利益を強く感じた。仕様を理解しておくべきというのは百も承知だが。
    • ソースコードレベルで Chat で相談できる。ファイル内で Ctrl+I を押せばチャットが開くので、質問事項が簡素で済む。Copilot に訊こうと思ったらもっと背景の説明が必要になるだろう。
    • もっと規模の大きいコードならよりご利益がある?
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?