JHipster開発における実践的なノウハウとトラブルシューティング

「天赋吉星」というJHipsterを使用して開発したウェブサイトがついにリリースされました。本記事では、開発プロセスで遭遇した様々な小さな知識点とテクニックを紹介します。これらは非常に役立つ情報ですので、参考にしてください。直接「天赋吉星」をクリックして、サイトの完成形をご覧いただけます。

まず、プロジェクトの技術選定について紹介します。JHipster バージョン: 8.1.0, プロジェクトタイプ: monolith. フロントエンドはReact+TypeScript+Redux, バックエンドはWebFlux, データベースはPostgreSQLです。 本プロジェクトでは、Circle CI + Helmを使用してデプロイフローを実装しています。算命機能はChatGPTの大規模言語モデルを利用しています。

以下に記録されている知識点は、開発と同時に記録したものですので、少し整理されていない点があるかもしれません。皆様にはご容赦ください。收藏しておいて、開発中に同じ問題に遭遇した際に参照していただければ幸いです。また、本記事は継続的に更新され、開発中に遭遇する一般的な問題を記録していきます。

JHipsterでのエンドツーエンド(e2e)テストの実行方法

npm test:e2e
code .
  • JDLを使用してコードを生成 jhipster jdl blog.jdl

  • モックデータを生成したくない場合は、application-dev.ymlからfaker設定を削除します。

  • ホットデプロイ:Javaクラスを変更した後、IDEのビルド機能を使用して該当クラスをコンパイルし、アプリ全体を再起動する必要を回避できます。

  • 現在ログインしているユーザー名 #{authentication.name}

  • フロントエンドでHTML効果を表示する許可

[innerHTML]="post.content"> - JHipsterのコンポーネント - Herokuへのデプロイ

jhipster heroku
heroku open
heroku logs --tail
  • 2つのファイルを比較するためにIDEのコマンドを使用する
idea diff a.file b.file

グローバルなJHipsterバージョンのアップグレード

npm uninstall -g generator-jhipster
npm install -g generator-jhipster@8.1.0

既存のjhi_userテーブルを利用する方法。例えば、システムのユーザーテーブルにいくつかのフィールドを追加したい場合?JHipster公式はユーザーエンティティを直接変更することを推奨していません。代わりに、組み合わせるか統合する2つの方法があります。 例: relationship OneToMany { CustomUser to User with builtInEntity } 参考:https://www.jhipster.tech/user-entity/ @Transientアノテーションが付けられたinternalUserプロパティは、データベースに永続化されないことを意味します。このプロパティはUserエンティティとの関連を保持するために使用されますが、実際にはデータベースにこのフィールドが作成されません。

JDLを定義する際、ServiceやOrderのような名前をエンティティ名として使用しないように注意してください。

実用的なテクニック https://www.jhipster.tech/tips/

  • ログイン成功後に指定のページにリダイレクトする方法? login.tsxで認証成功後のリダイレクトパスを変更します。下図を参照してください。

    JDL構文

  • 双方向の1対1

entity Product
entity Category

relationship OneToOne {
 Product{category} to Category{product}
}

これは、AからBへの関連関係を示し、ナビゲーションは双方向です:AからBへ、そしてBからAへナビゲートできます。Bはaフィールドを通じてAにアクセスでき、Aはbフィールドを通じてBにアクセスできます。

  • 双方向の1対多
entity Owner
entity Car

relationship OneToMany {
 Owner{cars} to Car{owner}
}

carオブジェクトからそのownerを見つけることができ、ownerから所有しているcarの数を見つけることができます。

  • 単方向の多対1
entity Owner
entity Car
relationship ManyToOne {
 Car{owner} to Owner
}

JHipsterのドキュメントによると、この場合、ownerからcarを追加または削除することはできません。

  • 単方向の1対多 この状況は、現在JHipsterではサポートされていません。代わりに2つのソリューションが提供されています。

  • 双方向の1対多で置き換える(推奨)

  • 生成されたコードで、@OneToManyアノテーションの"mappedBy"属性を削除し、必要な結合テーブルを生成します:mvn liquibase:diffを実行してテーブルを生成できます。

  • 2つの1対多 personは複数のcarを持つことができ、同時に複数のcarを運転することもできます。

entity Person
entity Car

relationship OneToMany {
 Person{ownedCars} to Car{owner}
}

relationship OneToMany {
 Person{drivenCars} to Car{driver}
}
entity Citizen
entity Passport
relationship OneToOne {
 Citizen{passport} to @Id Passport
}

@Idは、Passportの主キーがCitizenの外部キーであることを示し、それらは主キーを共有します。

@Entity
public class Citizen {
   @Id
   private Long id;

   @OneToOne
   @MapsId
   private Passport passport;
}

@Entity
public class Passport {
   @Id
   private Long id;

   @OneToOne(mappedBy = "passport")
   private Citizen citizen;
}

データのフェッチ戦略: OneToMany: LAZY ManyToOne: EAGER ManyToMany: LAZY OneToOne: EAGER WebFluxアーキテクチャでは、R2DBCを使用してデータベースを操作します。R2DBCには、自動的な連鎖保存、更新、または削除機能がありません。

  • 複数のJDLがある場合、jhipster jdl my_file1.jdl my_file2.jdl
  • 既に作成されたエンティティに対してJDLで追加、変更、削除などの操作を行った場合、Liquibaseが古いchangelogファイルを変更するのではなく、新しいchangelogを追加するように以下のコマンドを使用する必要があります:jhipster jdl ./jdl/business.jdl --incremental-changelog

上記の知識点を事前に知らなかった場合、JDLでフィールドを変更した後もjhipster jdl my_file1のようなコマンドを実行した場合。 その場合、Liquibaseは元のchangelogファイルに基づいて変更を行い、システムを起動するとchecksumエラーが発生します。以下のようなエラーです。

liquibase.exception.CommandExecutionException: liquibase.exception.ValidationFailedException: Validation Failed:
    3 changesets check sum
         config/liquibase/changelog/20240103135710_added_entity_Profile.xml::20240103135710-1::jhipster was: 9:91b5c4f73fbdf83cf6fb005cd45b356a but is now: 9:ae96db2ae1ff65c4d43b795f541e7399
         config/liquibase/changelog/20240103135710_added_entity_Profile.xml::20240103135710-1-data::jhipster was: 9:943b13b81ce107f44d593e4eb8117b9d but is now: 9:2ad33bba71a8dd0116f82b702e2e22a9
         config/liquibase/changelog/20240103135710_added_entity_constraints_Profile.xml::20240103135710-2::jhipster was: 9:60ce48c5bc8eb1829c4ddf65292ee11e but is now: 9:d9472654721100224162c2ee24ac4c6d

この場合、changelogファイルを慎重に修正し、jhipster jdl my_file1を実行する前の状態に戻す必要があります。これにより、データベースに記録されているファイルとchangelogファイルが一致し、checksumエラーが発生しなくなります。もしあなたが確かに前のファイルに戻ったと感じているのに、まだchecksumエラーが発生する場合は、checksumのエラーメッセージに従って、データベース内のchecksumの値を強制的に修正し、一致させることで解決できます。

その後、jhipster jdl ./jdl/business.jdl --incremental-changelogを実行して、正しい軌道に戻してください。この時、master.xmlにLiquibaseの新しい変更を反映させる必要があるかもしれません。手動で追加のchangelogをmaster.xmlに追加してください。追加する際は、下図に示すような順序に注意してください。Good luck!

  • その他のオプション:コマンドライン形式でエンティティを変更する。 jhipster import-jdl myapp.jdl --json-only --json-onlyオプションを使用すると、開発者はJSON設定ファイルを生成する前にレビューして確認できます。 jhipster entity MyChangedEntity1 --single-entity jhipster entity MyChangedEntity2 --single-entity

開発プロセス中に、新しいフィールドを追加した後、アプリを起動するとエラーが発生することがあります。

liquibase.exception.DatabaseException: org.postgresql.util.PSQLException: ERROR: column "product_start_date" of relation "profile" does not exist_  Position: 58 

これらのフィールドは別のchangelogで管理されているにもかかわらず、Liquibaseはこれらの新しいフィールドが見つからないというエラーを報告することがあります。これは、fakedataがこれらすべてのフィールドの値を持っているため、データベースで新しいフィールドをチェックしようとしてエラーが発生する可能性が高いです。一時的な解決策は、application-dev.yamlでfakeを無効にすることです。キーワードを検索=># Remove ‘faker’ if you do not want the sample data to be loaded automatically

権限に基づいてメニューの表示を制御する。以下のmenu.tsxの修正を参照してください。admin権限を持つユーザーのみがすべてのエンティティを見ることができます。

  • SSE(Server Sent Event)がheaderに認証情報を含めることができない問題の解決方法 問題の説明: フロントエンドのEventSourceはheaderにAuthorization情報を含めることができないため、バックエンドはユーザーがシステムにログインしている場合でも、どのユーザーから送られたSSEリクエストかを識別できず、権限制御を実現できません。 簡単な解決策:(cookieで解決することもできますが、少し面倒です)
    1. フロントエンドがSSEリクエストを送信する際、ローカルからログイン済みユーザーのJWTトークンを取得し、それをURLパラメータでバックエンドに渡します。
const token = Storage.local.get('jhi-authenticationToken') || Storage.session.get('jhi-authenticationToken');
       if (!token) {
           toast.error('Please login first');
           return;
       }
     let sseApiUrl = `/api/chat?question=${question}&threadId=${threadIdInLocal}&token=${token}`;
     const eventSource = new EventSource(sseApiUrl);
2. バックエンドにフィルターを追加し、URLから取得したトークンをheaderに追加します。これにより、システムの元の認証メカニズムをそのまま使用でき、最小限の変更で済みます。

@Component
public class JwtTokenFilter implements WebFilter {

   @Override
   public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
       String token = exchange.getRequest().getQueryParams().getFirst("token");
       if(token!=null){
           exchange.getRequest().mutate().header(HttpHeaders.AUTHORIZATION, "Bearer " + token).build();
       }
       return chain.filter(exchange);
   }
}
  1. SecurityConfigurationでこのフィルターを設定します。
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
       http
           .securityMatcher(
               new NegatedServerWebExchangeMatcher(
                   new OrServerWebExchangeMatcher(pathMatchers("/app/**", "/i18n/**", "/content/**", "/swagger-ui/**"))
               )
           )
           .cors(withDefaults())
           .csrf(csrf -> csrf.disable())
           .addFilterAfter(new SpaWebFilter(), SecurityWebFiltersOrder.HTTPS_REDIRECT)
           .addFilterBefore(jwtTokenFilter, SecurityWebFiltersOrder.AUTHENTICATION)
  • WebFluxのデバッグ:以下のコードでは、ストリーム処理中にエラーが発生しても、通常コンソールにエラーが表示されず、デバッグが困難になります。
permissionCheck.flatMapMany(canUse -> {
           if (!canUse) {
               return Flux.just(ServerSentEvent.<String>builder().event("noPermission").data("Please purchase usage time!").build());
           } else {
               return aiService.generateResponse(question, threadId);
           }
       })

デバッグのために、doOnErrorを追加してコンソールにエラーを表示させます。

permissionCheck.flatMapMany(canUse -> {
           if (!canUse) {
               return Flux.just(ServerSentEvent.<String>builder().event("noPermission").data("Please purchase usage time!").build());
           } else {
               return aiService.generateResponse(question, threadId);
           }
       })
       .doOnError(error -> log.error("Error processing permission check flux: ", error));

Stripeと連携する際にページがエラーを表示する場合

この時のデバッグプロセスは以下の通りです: a. コンソールを開き、直接401エラーのリンクをクリックします。

b. 表示されたページで、public keyのエラーであることがわかります。

  • 要件:order内のuserがuser detailページにナビゲートできるようにする 実際の状況:JHipsterがデフォルトで生成する注文リストページには、user detailページにナビゲートする機能がありません。下図の通り:

    理由:orderエンティティはuser idを通じてuserに関連付けられていますが、現在userへのアクセスはloginを通じて行われており、/admin/user-management/{login}というパスです。これが自動的にナビゲーションを生成しない理由かもしれません。 実装:Flux findAllBy(Pageable pageable);というコードは実際にデータベースからすべての関連データを取得しているため、マッピングクラスを修正するだけで済みます。下図の通り:

    フロントエンドのコードは以下の通りです:

<td>{orderRecord.user ?<Link to={`/admin/user-management/${orderRecord.user.username}`}>{orderRecord.user.username} </Link>: ''}</td>
  • エンティティuserProfilejhi_userは同じ主キーを使用しています。テーブル作成ステートメントは以下の通りです。要件:userを作成する際に、同時に1つのprofileをinsertする必要があります。
create table public.user_profile (
 id bigint primary key not null,
....
 foreign key (id) references public.jhi_user (id)
....
);

間違った方法:userProfileにidを設定すると、システムはこれをupdate操作と判断し、idが見つからないため保存に失敗するエラーが発生します。

flatMap(createdUser ->{
               UserProfile profile =  new UserProfile();
               profile.setId(createdUser.getId());
               profile.setInternalUser(createdUser);
               return userProfileRepository.save(profile).thenReturn(createdUser);
           })

正しい方法:userProfileにはsetIdは不要です。

flatMap(createdUser ->{
               UserProfile profile =  new UserProfile();
               profile.setInternalUser(createdUser);
               return userProfileRepository.save(profile).thenReturn(createdUser);
           })

最後に注意:userのupdateもsave userコードを呼び出すため、profileを作成する前にprofileが存在するかどうかを確認する必要があります。以下の通り:

flatMap(createdUser ->
               userProfileRepository
                   .findById(createdUser.getId())
                   .flatMap(existedProfile -> Mono.just(createdUser))
                   .switchIfEmpty(
                       Mono.defer(() -> {
                           UserProfile profile = new UserProfile();
                           profile.setInternalUser(createdUser);
                           return userProfileRepository.save(profile).thenReturn(createdUser);
                       })
                   )
           );

SEO最適化:title, description, keywords

JHipsterは多言語をサポートしていますが、多言語はSEOのサポートにはほとんど役立ちません。以下では、検索エンジンが検索しやすいように、ホームページのhome.tsxで多言語を使用する方法を紹介します。現在、「天赋吉星」は5つの言語をサポートしています。

  1. index.htmlに対応する動的メタデータタグを追加します。
<title id="page-title">天赋吉星</title>
   <meta id="dynamic-description" name="description" content="天赋吉星:在线算命,解梦,看风水,帮您求神拜佛。" />
   <meta
     id="dynamic-keywords"
     name="keywords"
     content="fortune telling, dream interpretation, feng shui, praying, 算命, 解梦, 看风水, 求神拜佛"
   />
  1. HomePage.tsx。ここでの鍵は、useEffect内でactiveLanguageを監視することです。なぜなら、言語は非同期でロードされるため、ページがマウントされた直後は言語がロードされていない可能性があり、nullエラーが発生するからです。
const activeLanguage = useAppSelector(state => state.locale.currentLocale);
useEffect(() => {
   const title = translate('home.seo.title');
   const description = translate('home.seo.description');
   const keywords = translate('home.seo.keywords');

   document.title = title;
   const descriptionMetaTag = document.getElementById('dynamic-description');
   const keywordsMetaTag = document.getElementById('dynamic-keywords');
   if (descriptionMetaTag) {
     descriptionMetaTag.setAttribute('content', description);
   }
   if (keywordsMetaTag) {
     keywordsMetaTag.setAttribute('content', keywords);
   }
 }, [activeLanguage]);

アイコンライブラリの追加方法

例えば、https://fontawesome.com/ でアイコン "fa-user" (https://fontawesome.com/icons/user?f=classic&s=solid) を見つけた場合、どのようにプロジェクトで使用しますか?

  1. icon-loader.tsを見つけ、loadIcons関数がfasライブラリをロードしているかどうかを確認し、ロードしていない場合は追加します。下図を参照してください。
  2. プログラムで使用する
<IconComponent icon="user"  border/>

タグ: JHipster Spring WebFlux React TypeScript PostgreSQL

6月25日 00:31 投稿