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

最后修改: 2022年 3月 3日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.介绍

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

2.项目设置

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.

在开始构建我们的服务之前,我们需要执行一些初始设置步骤–具体来说,我们需要创建一个Fauna数据库和一个空白的Spring应用程序。

2.1. Creating a Fauna Database

2.1.创建动物群数据库

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.

在开始之前,我们需要一个Fauna数据库来工作。如果我们还没有一个数据库,我们需要在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:

一旦完成这些,我们就可以创建一个新的数据库。给它一个名字和一个区域,并选择不包括演示数据,因为我们想建立我们自己的模式。

Screenshot-2022-01-17-at-07.39.16

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:

接下来,我们需要创建一个安全密钥,以便从我们的应用程序中访问它。我们可以在数据库中的安全选项卡中完成这一工作。

Screenshot-2022-01-17-at-07.42.27

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:

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

Screenshot-2022-01-17-at-07.45.21

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

2.2.创建一个Spring应用程序

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作为我们服务的依赖项。

Screenshot-2022-01-17-at-07.57.39

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:

接下来,我们需要将Fauna驱动添加到我们的项目中。这是通过在生成的pom.xml文件中添加对它们的依赖来实现的。

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

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

2.3.配置一个Fauna客户端

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 application.properties file, providing the correct values for our dastabase:

首先,我们有一些配置要做。为此,我们将在application.properties文件中添加两个属性,为我们的数据库提供正确的值。

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

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

然后,我们将需要一个新的Spring配置类来构建Fauna客户端。

@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();
    }
}

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

这使得Spring上下文中有一个FaunaClient的实例可供其他Bean使用。

3. Adding Support for Users

3.增加对用户的支持

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

3.1.创建一个用户集合

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:

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

Screenshot-2022-01-18-at-09.01.13

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查询,以查找我们的用户。比如说。

Map(
  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

3.2.针对动物群的认证

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:

为了实现这一点,我们首先需要一个UserDetailsService,根据Fauna查询用户。

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);
        }
    }
}

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

接下来,我们需要一些Spring配置来设置它。这是标准的Spring安全配置,用于连接上述UserDetailsService

@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);
    }
}

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

4.增加对列表帖子的支持

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

4.1.创建一个帖子集

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 “记录。

Screenshot-2022-01-19-at-07.53.11

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。

CreateIndex({
  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中创建一个帖子,以拥有一些起始数据。

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

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

4.2.职位服务

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

现在我们已经支持Fauna中的帖子,我们可以在我们的应用程序中建立一个服务层,以便与它一起工作。

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

首先,我们需要一些Java记录来表示我们正在获取的数据。这将包括一个Author和一个Post记录类。

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:

现在,我们可以启动我们的帖子服务。这将是一个Spring组件,它封装了FaunaClient并使用它来访问数据存储。

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

4.3. Getting All Posts

4.3.获得所有的帖子

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.

在我们的PostsService中,我们现在可以实现一个方法来获取所有的帖子。在这一点上,我们不打算担心适当的分页,而是只使用默认值–这意味着从结果集中获取前64个文档。

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

为了实现这一目标,我们将在我们的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());
}

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:

现在,我们需要能够将这个响应转换回我们的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()
    );
}

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

4.4.为单个作者获取帖子

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:

为此,我们将为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. Posts Controller

4.5.职位控制器

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 “参数。

@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);
    }
}

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

在这一点上,我们可以启动我们的应用程序,向/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. Creating and Updating Posts

5.创建和更新帖子

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

5.1.创建新帖子

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

首先,我们将支持创建新的帖子。为此,我们将为我们的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();
}

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:

接下来,我们可以添加一个控制器方法,让客户创建帖子。为此,我们首先需要一个Java记录来表示传入的请求数据。

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

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

现在,我们可以在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());
}

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.

注意,我们正在使用@PreAuthorize注解来确保请求得到认证,然后我们使用认证用户的用户名作为新帖子的作者。

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.

在这一点上,启动服务并向端点发送一个POST将导致在我们的集合中创建一个新的记录,然后我们可以用先前的处理程序来检索。

5.2. Updating Existing Posts

5.2.更新现有的帖子

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.

对我们来说,更新现有的帖子而不是创建新的帖子也是有帮助的。我们将通过接受一个带有新的标题和内容的PUT请求来管理这个问题,并更新帖子,使其具有这些值。

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

和以前一样,我们首先需要在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();
}

Next, we add our handler to the PostsController:

接下来,我们在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());
}

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:

在这一点上,启动服务并向正确的URL发送一个PUT将导致该记录被更新。然而,如果我们用一个未知的ID调用,我们会得到一个错误。我们可以用一个异常处理方法来解决这个问题。

@ExceptionHandler(NotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
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

6.检索过去版本的帖子

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(
      Let(
        "post", query
      ).in(
        Obj(
          "post", Var("post"),
          "author", Get(Select(Arr(Value("data"), Value("authorRef")), Var("post")))
        )
      )
    ).get();

  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.

在这里,我们引入了At方法,这将使Fauna返回给定时间点的数据。我们的版本号只是以微秒为单位的时间戳,所以我们可以通过简单地要求给定值之前1μs的数据来获得给定点之前的价值。

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

同样,我们需要一个控制器方法来处理传入的调用。我们将把它添加到我们的PostsController

@GetMapping("/{id}")
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.

而现在,我们可以获得单个帖子的单个版本。调用/posts/321742144715882562将得到该帖子的最新版本,但调用/posts/321742144715882562?before=1643183487660000将得到该版本之前的帖子的版本。

7. Conclusion

7.结论

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?

在这里,我们已经探讨了Fauna数据库的一些功能,以及如何用它们构建一个应用程序。Fauna还有很多我们在这里没有涉及的功能,但为什么不在你的下一个项目中尝试探索它们呢?

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

一如既往,这里显示的所有代码都可以在GitHub上获得。