Building a web app Using Fauna and Spring for Your First web Agency Client – 使用Fauna和Spring为你的第一个网络代理客户构建一个网络应用程序

最后修改: 2022年 3月 3日

1. Introduction


In this article, we’re going to build the backend to a blogging service powered by the Fauna database service, using Spring and Java 17.

在这篇文章中,我们将使用Spring和Java 17构建一个由Fauna数据库服务驱动的博客服务的后端

2. Project Setup


We have some initial setup steps that we need to perform before we can start building our service – specifically, we need to create a Fauna database and a blank Spring application.


2.1. Creating a Fauna Database


Before starting, we’ll need a Fauna database to work with. If we don’t already have one, we’ll need to create a new account with Fauna.


Once this is done, we can create a new database. Give this a name and a region, and opt not to include the demo data as we want to build our own schema:



Next, we need to create a security key to access this from our application. We can do this from the Security tab within our database:



In here, we need to select a “Role” of “Server” and, optionally, give the key a name. This means that the key can access this database, but only this database. Alternatively, we have an option of “Admin”, which can be used to access any database in our account:

在这里,我们需要选择一个 “服务器 “的 “角色”,并且可以选择给密钥一个名称。这意味着该钥匙可以访问这个数据库,但只能访问这个数据库。或者,我们有一个 “管理员 “的选项,它可以用来访问我们账户中的任何数据库。


When this is done, we need to write down our secret. This is necessary to access the service, but it can’t be obtained again once we leave this page, for security reasons.


2.2. Creating a Spring Application


Once we have our database, we can create our application. Since this will be a Spring webapp, we’re best off bootstrapping this from Spring Initializr.

一旦我们有了数据库,我们就可以创建我们的应用程序。由于这将是一个 Spring Web 应用程序,我们最好从Spring Initializr启动这个应用程序。

We want to select the options to create a Maven project using the latest release of Spring and the latest LTS release of Java – at the time of writing, these were Spring 2.6.2 and Java 17. We also want to select Spring Web and Spring Security as dependencies for our service:

我们要选择使用最新的Spring版本和最新的Java LTS版本创建Maven项目–在撰写本文时,这些版本是Spring 2.6.2和Java 17。我们还想选择Spring Web和Spring Security作为我们服务的依赖项。


Once we’re done here, we can hit the “Generate” button to download our starter project.

一旦我们在这里完成,我们可以点击 “生成 “按钮,下载我们的启动项目。

Next, we need to add the Fauna drivers to our project. This is done by adding a dependency on them to the generated pom.xml file:



At this point, we should be able to execute mvn install and have the build successfully download everything we need.

在这一点上,我们应该能够执行mvn install,并让构建成功下载我们需要的一切。

2.3. Configuring a Fauna Client


Once we have a Spring webapp to work with, we need a Fauna client to use the database.

一旦我们有了一个Spring Web应用,我们就需要一个Fauna客户端来使用数据库。

First, we have some configuration to do. For this, we’ll add two properties to our file, providing the correct values for our dastabase:



Then, we’ll want a new Spring configuration class to construct the Fauna client:


class FaunaConfiguration {
    private String faunaUrl;

    private String faunaSecret;

    FaunaClient getFaunaClient() throws MalformedURLException {
        return FaunaClient.builder()

This makes an instance of FaunaClient available to the Spring context for other beans to use.


3. Adding Support for Users


Before adding support for posts to our API, we need support for the users who will author them. For this, we’ll make use of Spring Security and connect it up to a Fauna collection representing the user records.

在向我们的API添加对帖子的支持之前,我们需要对撰写帖子的用户提供支持。为此,我们将利用Spring Security并将其连接到代表用户记录的Fauna集合。

3.1. Creating a Users Collection


The first thing we want to do is to create the collection. This is done by navigating to the Collections screen in our database, using the “New Collection” button, and filling out the form. In this case, we want to create a “users” collection with the default settings:

我们要做的第一件事是创建集合。这是通过导航到我们数据库中的集合屏幕,使用 “新集合 “按钮,并填写表格来完成的。在这种情况下,我们想用默认设置创建一个 “用户 “集合。

Next, we’ll add a user record. For this, we press the “New Document” button in our collection and provide the following JSON:

接下来,我们将添加一个用户记录。为此,我们在我们的集合中按下 “新文档 “按钮,并提供以下JSON。

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

Note that we’re storing passwords in plaintext here. Keep in mind that this is a terrible practice and is only done for the convenience of this tutorial.


Finally, we need an index. Any time we want to access records by any field apart from the reference, we need to create an index that lets us do that. Here, we want to access records by username. This is done by pressing the “New Index” button and filling out the form:

最后,我们需要一个索引。任何时候我们想通过引用以外的任何字段来访问记录,我们都需要创建一个索引,让我们做到这一点。在这里,我们想通过用户名来访问记录。这可以通过按下 “新索引 “按钮并填写表格来完成。


Now, we’ll be able to write FQL queries using the “users_by_username” index to look up our users. For example:

现在,我们将能够使用 “users_by_username “索引编写FQL查询,以查找我们的用户。比如说。

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

The above will return the record we created earlier.


3.2. Authenticating Against Fauna


Now that we have a collection of users in Fauna, we can configure Spring Security to authenticate against this.

现在我们在Fauna中拥有一个用户集合,我们可以配置Spring Security来对其进行验证。

To achieve this, we first need a UserDetailsService that looks users up against Fauna:


public class FaunaUserDetailsService implements UserDetailsService {
    private final FaunaClient faunaClient;

    // standard constructors

    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")))))

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

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

Next, we need some Spring configuration to set it up. This is standard Spring Security config to wire up the above UserDetailsService:


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

    private FaunaClient faunaClient;

    protected void configure(HttpSecurity http) throws Exception {

    public UserDetailsService userDetailsService() {
        return new FaunaUserDetailsService(faunaClient);

At this point, we can add the standard @PreAuthorize annotations to our code and accept or reject requests based on whether the authentication details exist in our “users” collection in Fauna.

在这一点上,我们可以将标准的@PreAuthorize注解添加到我们的代码中,并根据Fauna中 “用户 “集合中是否存在认证细节来接受或拒绝请求。

4. Adding Support for Listing Posts


Our blogging service wouldn’t be outstanding if it didn’t support the concept of Posts. These are the actual blog posts that have been written and can be read by others.

如果我们的博客服务不支持 “帖子 “的概念,那么它就不会很出色。这些是已经写好的实际博客文章,可以被其他人阅读。

4.1. Creating a Posts Collection


As before, we first need a collection to store the posts in. This is created the same, only called “posts” instead of “users”. We’re going to have four fields:

和以前一样,我们首先需要一个集合来存储帖子。这个集合的创建是一样的,只是叫 “帖子 “而不是 “用户”。我们将有四个字段。

  • title – The title of the post.
  • content – The content of the post.
  • created – The timestamp at which the post was authored.
  • authorRef – The reference to the “users” record for the post’s author.

We’re also going to want two indices. The first is “posts_by_author”, which will let us search for “posts” records that have a particular author:

我们还将需要两个索引。第一个是 “post_by_author”,它将让我们搜索有一个特定作者的 “post “记录。


The second index will be “posts_sort_by_created_desc”. This will allow us to sort results by the created date, such that more recently created posts are returned first. We need to create this differently because it relies on a feature not available in the web UI – indicating that the index stores values in reverse order.

第二个索引将是 “post_sort_by_created_desc”。这将允许我们按创建日期对结果进行排序,这样,最近创建的帖子将被首先返回。我们需要以不同的方式创建这个索引,因为它依赖于网页用户界面中没有的功能–表明索引以相反的顺序存储值。

For this, we’ll need to execute a piece of FQL in the Fauna Shell:

为此,我们需要在Fauna Shell中执行一段FQL。

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

Everything that the web UI does can equally be done in this way, allowing for more control over precisely what is done.


We can then create a post in the Fauna Shell to have some starting data:

然后我们可以在Fauna Shell中创建一个帖子,以拥有一些起始数据。

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

Here, we need to ensure that the value for “authorRef” is the correct value from our “users” record we created earlier. We do this by querying the “users_by_username” index to get the ref by looking up our username.

在这里,我们需要确保 “authorRef “的值是我们先前创建的 “users “记录的正确值。我们通过查询 “users_by_username “索引,通过查找我们的用户名来获得Ref。

4.2. Posts Service


Now that we have support for posts within Fauna, we can build a service layer in our application to work with it.


First, we need some Java records to represent the data we’re fetching. This will consist of an Author and a Post record class:


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

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

Now, we can start our Posts Service. This will be a Spring component that wraps the FaunaClient and uses it to access the datastore:


public class PostsService {
    private FaunaClient faunaClient;

4.3. Getting All Posts


Within our PostsService, we can now implement a method to fetch all posts. At this point, we’re not going to worry about proper pagination and instead only use the defaults – which means the first 64 documents from the resultset.


To achieve this, we’ll add the following method to our PostsService class:


List<Post> getAllPosts() throws Exception {
    var postsResult = faunaClient.query(Map(
        Arr(Value("extra"), Value("ref")),
          "post", Get(Var("ref")),
          "author", Get(Select(Arr(Value("data"), Value("authorRef")), Get(Var("ref"))))

    var posts ="data").asCollectionOf(Value.class).get();

This executes a query to retrieve every document from the “posts” collection, sorted according to the “posts_sort_by_created_desc” index. It then applies a Lambda to build the response, consisting of two documents for each entry – the post itself and the post’s author.

这将执行一个查询,以检索 “post “集合中的每个文档,并根据 “post_sort_by_created_desc “索引进行排序。然后它应用Lambda来构建响应,每个条目由两个文档组成–帖子本身和帖子的作者。

Now, we need to be able to convert this response back into our Post objects:


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

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

This takes a single result from our query, extracts all of its values, and constructs our richer objects.


Note that the “ts” field is a timestamp of when the record was last updated, but it isn’t the Fauna Timestamp type. Instead, it’s a Long representing the number of microseconds since the UNIX epoch. In this case, we’re treating it as an opaque version identifier instead of parsing it into a timestamp.

注意,”ts “字段是记录最后更新的时间戳,但它不是Fauna的Timestamp类型。相反,它是一个Long,代表UNIX纪元以来的微秒数。在这种情况下,我们把它当作一个不透明的版本标识符,而不是把它解析成一个时间戳。

4.4. Gettings Posts for a Single Author


We also want to retrieve all posts authored by a specific author, rather than just every post that has ever been written. This is a matter of using our “posts_by_author” index instead of just matching every document.

我们还想检索由特定作者撰写的所有帖子,而不是仅仅检索曾经写过的每一篇帖子。这是一个使用我们的 “post_by_author “索引的问题,而不是仅仅匹配每个文档。

We’ll also link to the “users_by_username” index to query by username instead of the ref of the user record.

我们还将链接到 “users_by_username “索引,通过用户名而不是用户记录的参考文献进行查询。

For this, we’ll add a new method to the PostsService class:


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

    var posts ="data").asCollectionOf(Value.class).get();

4.5. Posts Controller


We’re now able to write our posts controller, which will allow HTTP requests to our service to retrieve posts. This will listen on the “/posts” URL and will return either all posts or else the posts for a single author, depending on whether or not an “author” parameter is provided:

我们现在能够编写我们的帖子控制器,它将允许对我们的服务进行HTTP请求以检索帖子。这将监听”/posts “URL,并将返回所有帖子或单个作者的帖子,这取决于是否提供了 “author “参数。

public class PostsController {
    private PostsService postsService;

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

At this point, we can start our application and make requests to /posts or /posts?author=baeldung and get results:


        "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. Creating and Updating Posts


So far, we have an entirely read-only service that will let us fetch the most recent posts. However, to be helpful, we want to create and update posts as well.


5.1. Creating New Posts


First, we’ll support creating new posts. For this, we’ll add a new method to our PostsService:


public void createPost(String author, String title, String contents) throws Exception {
          "data", Obj(
            "title", Value(title),
            "contents", Value(contents),
            "created", Now(),
            "authorRef", Select(Value("ref"), Get(Match(Index("users_by_username"), Value(author))))

If this looks familiar, it’s the Java equivalent to when we created a new post in the Fauna shell earlier.

如果这看起来很熟悉,它相当于我们之前在Fauna shell中创建一个新帖子时的Java。

Next, we can add a controller method to let clients create posts. For this, we first need a Java record to represent the incoming request data:


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

Now, we can create a new controller method in PostsController to handle the requests:


public void createPost(@RequestBody UpdatedPost post) throws Exception {
    String name = SecurityContextHolder.getContext().getAuthentication().getName();
    postsService.createPost(name, post.title(), post.content());

Note that we’re using the @PreAuthorize annotation to ensure that the request is authenticated, and then we’re using the username of the authenticated user as the author of the new post.


At this point, starting the service and sending a POST to the endpoint will cause a new record to be created in our collection, which we can then retrieve with the earlier handlers.


5.2. Updating Existing Posts


It would also be helpful for us to update existing posts instead of creating new ones. We’ll manage this by accepting a PUT request with the new title and contents and updating the post to have these values.


As before, the first thing we need is a new method on the PostsService to support this:


public void updatePost(String id, String title, String contents) throws Exception {
      Update(Ref(Collection("posts"), id),
          "data", Obj(
            "title", Value(title),
            "contents", Value(contents)

Next, we add our handler to the PostsController:


public void updatePost(@PathVariable("id") String id, @RequestBody UpdatedPost post)
    throws Exception {
    postsService.updatePost(id, post.title(), post.content());

Note that we’re using the same request body to create and update posts. This is perfectly fine since both have the same shape and meaning – the new details for the post in question.


At this point, starting the service and sending a PUT to the correct URL will cause that record to be updated. However, if we call with an unknown ID, we’ll get an error. We can fix this with an exception handler method:


public void postNotFound() {}

This will now cause a request to update an unknown post to return an HTTP 404.

现在,这将导致更新一个未知帖子的请求返回HTTP 404。

6. Retrieving Past Versions of Posts


Now that we’re able to update posts, it can be helpful to see old versions of them.


First, we’ll add a new method to our PostsService to retrieve posts. This takes the ID of the post and, optionally, the version before which we want to get – in other words, if we provide a version of “5”, then we want to return version “4” instead:

首先,我们将为我们的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(
        "post", query
          "post", Var("post"),
          "author", Get(Select(Arr(Value("data"), Value("authorRef")), Var("post")))

  return parsePost(postResult);

Here, we introduce the At method, which will make Fauna return the data at a given point in time. Our version numbers are just timestamps in microseconds, so we can get the value before a given point by simply asking for the data 1μs before the value we were given.


Again, we need a controller method to handle the incoming calls for this. We’ll add this to our PostsController:


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

And now, we can get individual versions of individual posts. A call to /posts/321742144715882562 will get the most recent version of that post, but a call to /posts/321742144715882562?before=1643183487660000 will get the version of the post that immediately preceded that version.


7. Conclusion


Here, we’ve explored some of the features of the Fauna database and how to build an application with them. There is still a lot that Fauna can do that we haven’t covered here, but why not try exploring them for your next project?


As always, all of the code shown here is available over on GitHub.