1. 序章

この記事では、SpringとJava 17を使用して、Faunaデータベースサービスを利用したブログサービスへのバックエンドを構築します。

2. プロジェクトの設定

サービスの構築を開始する前に実行する必要のある初期設定手順がいくつかあります。具体的には、Faunaデータベースと空のSpringアプリケーションを作成する必要があります。

2.1. 動物相データベースの作成

開始する前に、使用する動物相データベースが必要です。まだ持っていない場合は、動物相で新しいアカウントを作成する必要があります。

これが完了すると、新しいデータベースを作成できます。 これに名前とリージョンを付け、独自のスキーマを構築するため、デモデータを含めないことを選択します。

次に、アプリケーションからこれにアクセスするためのセキュリティキーを作成する必要があります。データベース内の[セキュリティ]タブからこれを行うことができます。

ここでは、「サーバー」の「役割」を選択し、オプションでキーに名前を付ける必要があります。 これは、キーがこのデータベースにアクセスできるが、このデータベースにのみアクセスできることを意味します。 または、「管理者」のオプションがあります。これを使用して、アカウント内の任意のデータベースにアクセスできます。

これが完了したら、シークレットを書き留める必要があります。 これはサービスにアクセスするために必要ですが、セキュリティ上の理由からこのページを離れると再度取得することはできません

2.2. Springアプリケーションの作成

データベースができたら、アプリケーションを作成できます。これはSpring Webアプリになるため、 SpringInitializrからブートストラップすることをお勧めします。

Springの最新リリースとJavaの最新LTSリリースを使用してMavenプロジェクトを作成するためのオプションを選択したいと思います。これを書いている時点では、これらはSpring2.6.2とJava17でした。 また、サービスの依存関係としてSpringWebとSpringSecurityを選択します。

ここで完了したら、[生成]ボタンを押してスタータープロジェクトをダウンロードできます。

次に、Faunaドライバーをプロジェクトに追加する必要があります。 これは、生成されたpom.xmlファイルにそれらへの依存関係を追加することによって行われます。

<dependency>
    <groupId>com.faunadb</groupId>
    <artifactId>faunadb-java</artifactId>
    <version>4.2.0</version>
    <scope>compile</scope>
</dependency>

この時点で、 mvn install を実行し、ビルドで必要なものをすべて正常にダウンロードできるようになります。

2.3. 動物相クライアントの構成

使用するSpringWebアプリができたら、データベースを使用するためにFaunaクライアントが必要です。

まず、いくつかの構成を行う必要があります。 このために、 application.properties ファイルに2つのプロパティを追加して、dastabaseに正しい値を提供します。

fauna.region=us
fauna.secret=<Secret>

次に、Faunaクライアントを構築するための新しいSpring構成クラスが必要になります。

@Configuration
class FaunaConfiguration {
    @Value("https://db.${fauna.region}.fauna.com/")
    private String faunaUrl;

    @Value("${fauna.secret}")
    private String faunaSecret;

    @Bean
    FaunaClient getFaunaClient() throws MalformedURLException {
        return FaunaClient.builder()
          .withEndpoint(faunaUrl)
          .withSecret(faunaSecret)
          .build();
    }
}

これにより、 FaunaClient のインスタンスが、他のBeanが使用できるようにSpringコンテキストで使用できるようになります。

3. ユーザーのサポートの追加

投稿のサポートをAPIに追加する前に、投稿を作成するユーザーのサポートが必要です。このために、Springセキュリティを利用して、ユーザーを表す動物相コレクションに接続します記録。

3.1. ユーザーコレクションの作成

最初に行うことは、コレクションを作成することです。これは、データベースの[コレクション]画面に移動し、[新しいコレクション]ボタンを使用して、フォームに入力することで実行されます。 この場合、デフォルト設定で「users」コレクションを作成します。

次に、ユーザーレコードを追加します。 このために、コレクションの[新しいドキュメント]ボタンを押して、次のJSONを提供します。

{
  "username": "baeldung",
  "password": "Pa55word",
  "name": "Baeldung"
}

ここでは、パスワードをプレーンテキストで保存していることに注意してください。 これはひどい習慣であり、このチュートリアルの便宜のためにのみ行われることに注意してください。

最後に、インデックスが必要です。 参照以外のフィールドでレコードにアクセスするときはいつでも、それを可能にするインデックスを作成する必要があります。 ここでは、ユーザー名でレコードにアクセスします。 これは、「新しいインデックス」ボタンを押してフォームに入力することで実行されます。

これで、「users_by_username」インデックスを使用してFQLクエリを記述し、ユーザーを検索できるようになります。 例えば:

Map(
  Paginate(Match(Index("users_by_username"), "baeldung")),
  Lambda("user", Get(Var("user")))
)

上記は、以前に作成したレコードを返します。

3.2. 動物相に対する認証

動物相にユーザーのコレクションができたので、これに対して認証するようにSpringSecurityを構成できます。

これを実現するには、まず、ユーザーを動物相に対して検索するUserDetailsServiceが必要です。

public class FaunaUserDetailsService implements UserDetailsService {
    private final FaunaClient faunaClient;

    // standard constructors

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        try {
            Value user = faunaClient.query(Map(
              Paginate(Match(Index("users_by_username"), Value(username))),
              Lambda(Value("user"), Get(Var("user")))))
              .get();

            Value userData = user.at("data").at(0).orNull();
            if (userData == null) {
                throw new UsernameNotFoundException("User not found");
            }

            return User.withDefaultPasswordEncoder()
              .username(userData.at("data", "username").to(String.class).orNull())
              .password(userData.at("data", "password").to(String.class).orNull())
              .roles("USER")
              .build();
        } catch (ExecutionException | InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

次に、それを設定するためにいくつかのSpring構成が必要です。 これは、上記のUserDetailsServiceを接続するための標準のSpringSecurity構成です。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private FaunaClient faunaClient;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.authorizeRequests()
          .antMatchers("/**").permitAll()
          .and().httpBasic();
    }

    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        return new FaunaUserDetailsService(faunaClient);
    }
}

この時点で、標準の @PreAuthorize アノテーションをコードに追加し、Faunaの「users」コレクションに認証の詳細が存在するかどうかに基づいてリクエストを承認または拒否できます。

4. 投稿の一覧表示のサポートの追加

私たちのブログサービスは、投稿の概念をサポートしていなければ目立たないでしょう。これらは、他の人が書いたり読んだりできる実際のブログ投稿です。

4.1. 投稿コレクションの作成

以前と同様に、最初に投稿を保存するコレクションが必要です。これは同じように作成され、「ユーザー」ではなく「投稿」とのみ呼ばれます。 4つのフィールドがあります。

  • title –投稿のタイトル。
  • content –投稿のコンテンツ。
  • 作成済み–投稿が作成されたタイムスタンプ。
  • authorRef –投稿の作成者の「users」レコードへの参照。

また、2つのインデックスが必要になります。 1つ目は「posts_by_author」です。これにより、特定の著者がいる「投稿」レコードを検索できます。

2番目のインデックスは「posts_sort_by_created_desc」になります。 これにより、結果を作成日で並べ替えることができ、最近作成された投稿が最初に返されます。 これは、Web UIで使用できない機能に依存しているため、別の方法で作成する必要があります。これは、インデックスが値を逆の順序で格納することを示しています。

このために、動物相シェルでFQLの一部を実行する必要があります。

CreateIndex({
  name: "posts_sort_by_created_desc",
  source: Collection("posts"),
  terms: [ { field: ["ref"] } ],
  values: [
    { field: ["data", "created"], reverse: true },
    { field: ["ref"] }
  ]
})

Web UIが行うことはすべて、この方法で等しく実行できるため、実行する内容をより正確に制御できます。

次に、Fauna Shellに投稿を作成して、いくつかの開始データを取得できます。

Create(
  Collection("posts"),
  {
    data: {
      title: "My First Post",
      contents: "This is my first post",
      created: Now(),
      authorRef: Select("ref", Get(Match(Index("users_by_username"), "baeldung")))
    }
  }
)

ここでは、「authorRef」の値が、前に作成した「users」レコードからの正しい値であることを確認する必要があります。 これを行うには、「users_by_username」インデックスをクエリして、ユーザー名を検索して参照を取得します。

4.2. 投稿サービス

Fauna内の投稿がサポートされたので、アプリケーションでサービスレイヤーを構築して操作できます。

まず、フェッチするデータを表すためにいくつかのJavaレコードが必要です。 これは、AuthorPostレコードクラスで構成されます。

public record Author(String username, String name) {}

public record Post(String id, String title, String content, Author author, Instant created, Long version) {}

これで、投稿サービスを開始できます。 これは、 FaunaClient をラップし、それを使用してデータストアにアクセスするSpringコンポーネントになります。

@Component
public class PostsService {
    @Autowired
    private FaunaClient faunaClient;
}

4.3. すべての投稿を取得する

PostsService内で、すべての投稿をフェッチするメソッドを実装できるようになりました。この時点では、適切なページ付けについて心配する必要はなく、代わりにデフォルトのみを使用します。つまり、結果セット。

これを実現するために、PostsServiceクラスに次のメソッドを追加します。

List<Post> getAllPosts() throws Exception {
    var postsResult = faunaClient.query(Map(
      Paginate(
        Join(
          Documents(Collection("posts")),
          Index("posts_sort_by_created_desc")
        )
      ),
      Lambda(
        Arr(Value("extra"), Value("ref")),
        Obj(
          "post", Get(Var("ref")),
          "author", Get(Select(Arr(Value("data"), Value("authorRef")), Get(Var("ref"))))
        )
      )
    )).get();

    var posts = postsResult.at("data").asCollectionOf(Value.class).get();
    return posts.stream().map(this::parsePost).collect(Collectors.toList());
}

クエリを実行して、「posts」コレクションからすべてのドキュメントを取得し、「posts_sort_by_created_desc」インデックスに従って並べ替えます。次に、Lambdaを適用して、エントリごとに2つのドキュメント(投稿)で構成される応答を作成します。それ自体と投稿の作成者。

ここで、この応答をPostオブジェクトに変換できるようにする必要があります。

private Post parsePost(Value entry) {
    var author = entry.at("author");
    var post = entry.at("post");

    return new Post(
      post.at("ref").to(Value.RefV.class).get().getId(),
      post.at("data", "title").to(String.class).get(),
      post.at("data", "contents").to(String.class).get(),
      new Author(
        author.at("data", "username").to(String.class).get(),
        author.at("data", "name").to(String.class).get()
      ),
      post.at("data", "created").to(Instant.class).get(),
      post.at("ts").to(Long.class).get()
    );
}

これは、クエリから単一の結果を取得し、そのすべての値を抽出して、より豊富なオブジェクトを構築します。

「ts」フィールドは、レコードが最後に更新されたときのタイムスタンプですが、Fauna Timestampタイプではないことに注意してください。 代わりに、UNIXエポック以降のマイクロ秒数を表すLongです。 この場合、タイムスタンプに解析するのではなく、不透明なバージョン識別子として扱います。

4.4. 単一の著者の投稿を取得する

また、これまでに作成されたすべての投稿だけでなく、特定の作成者によって作成されたすべての投稿を取得する必要があります。 これは、すべてのドキュメントを照合するだけでなく、「posts_by_author」インデックスを使用することです。

また、「users_by_username」インデックスにリンクして、ユーザーレコードの参照の代わりにユーザー名でクエリを実行します。

このために、PostsServiceクラスに新しいメソッドを追加します。

List<Post> getAuthorPosts(String author) throws Exception {
    var postsResult = faunaClient.query(Map(
      Paginate(
        Join(
          Match(Index("posts_by_author"), Select(Value("ref"), Get(Match(Index("users_by_username"), Value(author))))),
          Index("posts_sort_by_created_desc")
        )
      ),
      Lambda(
        Arr(Value("extra"), Value("ref")),
        Obj(
          "post", Get(Var("ref")),
          "author", Get(Select(Arr(Value("data"), Value("authorRef")), Get(Var("ref"))))
        )
      )
    )).get();

    var posts = postsResult.at("data").asCollectionOf(Value.class).get();
    return posts.stream().map(this::parsePost).collect(Collectors.toList());
}

4.5. 投稿コントローラー

投稿コントローラーを作成できるようになりました。これにより、サービスへのHTTPリクエストで投稿を取得できるようになります。これにより、「/ posts」URLがリッスンされ、すべての投稿または次の投稿が返されます。 「作成者」パラメーターが提供されているかどうかに応じて、単一の作成者:

@RestController
@RequestMapping("/posts")
public class PostsController {
    @Autowired
    private PostsService postsService;

    @GetMapping
    public List<Post> listPosts(@RequestParam(value = "author", required = false) String author) 
        throws Exception {
        return author == null 
          ? postsService.getAllPosts() 
          : postsService.getAuthorPosts(author);
    }
}

この時点で、アプリケーションを起動し、 /postsまたは/posts?author = baeldung にリクエストを送信して、結果を取得できます。

[
    {
        "author": {
            "name": "Baeldung",
            "username": "baeldung"
        },
        "content": "Introduction to FaunaDB with Spring",
        "created": "2022-01-25T07:36:24.563534Z",
        "id": "321742264960286786",
        "title": "Introduction to FaunaDB with Spring",
        "version": 1643096184600000
    },
    {
        "author": {
            "name": "Baeldung",
            "username": "baeldung"
        },
        "content": "This is my second post",
        "created": "2022-01-25T07:34:38.303614Z",
        "id": "321742153548038210",
        "title": "My Second Post",
        "version": 1643096078350000
    },
    {
        "author": {
            "name": "Baeldung",
            "username": "baeldung"
        },
        "content": "This is my first post",
        "created": "2022-01-25T07:34:29.873590Z",
        "id": "321742144715882562",
        "title": "My First Post",
        "version": 1643096069920000
    }
]

5. 投稿の作成と更新

これまでのところ、最新の投稿を取得できる完全に読み取り専用のサービスがあります。 ただし、役立つように、投稿も作成および更新したいと思います。

5.1. 新しい投稿の作成

まず、新しい投稿の作成をサポートします。 このために、PostsServiceに新しいメソッドを追加します。

public void createPost(String author, String title, String contents) throws Exception {
    faunaClient.query(
      Create(Collection("posts"),
        Obj(
          "data", Obj(
            "title", Value(title),
            "contents", Value(contents),
            "created", Now(),
            "authorRef", Select(Value("ref"), Get(Match(Index("users_by_username"), Value(author))))
          )
        )
      )
    ).get();
}

これがおなじみのように見える場合は、以前に動物相シェルで新しい投稿を作成したときと同等のJavaです。

次に、クライアントが投稿を作成できるようにするコントローラーメソッドを追加できます。 このためには、最初に、着信要求データを表すJavaレコードが必要です。

public record UpdatedPost(String title, String content) {}

これで、 PostsController に新しいコントローラーメソッドを作成して、リクエストを処理できます。

@PostMapping
@ResponseStatus(HttpStatus.NO_CONTENT)
@PreAuthorize("isAuthenticated()")
public void createPost(@RequestBody UpdatedPost post) throws Exception {
    String name = SecurityContextHolder.getContext().getAuthentication().getName();
    postsService.createPost(name, post.title(), post.content());
}

@PreAuthorize アノテーションを使用してリクエストが認証されていることを確認してから、認証されたユーザーのユーザー名を新しい投稿の作成者として使用していることに注意してください。

この時点で、サービスを開始してPOSTをエンドポイントに送信すると、コレクションに新しいレコードが作成され、以前のハンドラーで取得できます。

5.2. 既存の投稿を更新する

新しい投稿を作成する代わりに既存の投稿を更新することも役立ちます。新しいタイトルとコンテンツでPUTリクエストを受け入れ、これらの値を持つように投稿を更新することで、これを管理します。

以前と同様に、最初に必要なのは、これをサポートするためのPostsServiceの新しいメソッドです。

public void updatePost(String id, String title, String contents) throws Exception {
    faunaClient.query(
      Update(Ref(Collection("posts"), id),
        Obj(
          "data", Obj(
            "title", Value(title),
            "contents", Value(contents)
          )
        )
      )
    ).get();
}

次に、ハンドラーをPostsControllerに追加します。

@PutMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@PreAuthorize("isAuthenticated()")
public void updatePost(@PathVariable("id") String id, @RequestBody UpdatedPost post)
    throws Exception {
    postsService.updatePost(id, post.title(), post.content());
}

投稿の作成と更新には同じリクエスト本文を使用していることに注意してください。 どちらも同じ形と意味を持っているので、これは完全に問題ありません–問題の投稿の新しい詳細。

この時点で、サービスを開始してPUTを正しいURLに送信すると、そのレコードが更新されます。 ただし、不明なIDで呼び出すと、エラーが発生します。 これは、例外ハンドラーメソッドで修正できます。

@ExceptionHandler(NotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public void postNotFound() {}

これにより、不明な投稿を更新してHTTP404を返すリクエストが発生します。

6. 過去のバージョンの投稿を取得する

投稿を更新できるようになったので、古いバージョンの投稿を確認すると便利です。

まず、PostsServiceに新しいメソッドを追加して投稿を取得します。 これは、投稿のIDと、オプションで、取得する前のバージョンを取得します。つまり、「5」のバージョンを提供する場合は、代わりにバージョン「4」を返します。

Post getPost(String id, Long before) throws Exception {
    var query = Get(Ref(Collection("posts"), id));
    if (before != null) {
        query = At(Value(before - 1), query);
    }

    var postResult = faunaClient.query(
      Let(
        "post", query
      ).in(
        Obj(
          "post", Var("post"),
          "author", Get(Select(Arr(Value("data"), Value("authorRef")), Var("post")))
        )
      )
    ).get();

  return parsePost(postResult);
}

ここでは、Faunaが特定の時点のデータを返すようにするAtメソッドを紹介します。バージョン番号はマイクロ秒単位のタイムスタンプであるため、質問するだけで特定の時点より前の値を取得できます。与えられた値の1μs前のデータの場合。

この場合も、このための着信呼び出しを処理するためのコントローラーメソッドが必要です。 これをPostsControllerに追加します。

@GetMapping("/{id}")
public Post getPost(@PathVariable("id") String id, @RequestParam(value = "before", required = false) Long before)
    throws Exception {
    return postsService.getPost(id, before);
}

そして今、私たちは個々の投稿の個々のバージョンを取得することができます。 / posts / 321742144715882562 を呼び出すと、その投稿の最新バージョンが取得されますが、 / posts / 321742144715882562?before = 1643183487660000 を呼び出すと、その投稿のバージョンがすぐに取得されます。そのバージョンに先行しました。

7. 結論

ここでは、Faunaデータベースの機能のいくつかと、それらを使用してアプリケーションを構築する方法について説明しました。 Faunaでできることはまだたくさんありますが、ここでは取り上げていませんが、試してみてください。あなたの次のプロジェクトのためにそれらを探索しますか?

いつものように、ここに示されているすべてのコードはGitHubで入手できます。