Feb 26, 2019


Blog Categories

Categories

Blog Categories

Learning FQL, Part 1: FaunaDB Schema Objects

This series introduces the FaunaDB Query Language, giving examples of patterns and APIs that you’ll interact with as you use the database. This is the first article in the series, and discusses the primitive objects that you’ll work with in FaunaDB. You can find a comprehensive glossary of FaunaDB terms in the documentation. This article focuses on introducing you to the API objects that you’ll work with most.

We’ll explore the FaunaDB data model in the order that you are likely to encounter it. Databases, Classes (a.k.a. collections), Instances (a.k.a. documents), and Indexes are used by most applications. This article also describes the steps that you will follow to create and query these database objects.

There are additional objects we'll cover in a future article. Access keys are how you control which processes and users can interact with which databases and documents. User-defined functions are important for applications with additional security constraints. Authorization Tokens are useful for providing data APIs to mobile and web clients. These advanced features are important for certain applications, so we’ll cover them in another blog post. This post focuses on the most common objects you’ll encounter.

Shell Connection

After you set up a FaunaDB cluster, or create a FaunaDB Cloud account, you can use Fauna Shell to connect to your account and create databases.

fauna cloud-login 
fauna create-database my-database 
fauna shell my-database

Databases

Databases contain the structures that your application works with. FaunaDB databases can also contain other databases, giving you the ability to organize a tree of nested databases, each of which inherits access control and quality-of-service prioritization from its parent. Read more about arranging a tree of databases for shared services, SaaS, DBaaS, or multi-tenant workloads to control both security and priority according to your business needs. To avoid confusion and to enable fine-grained security, your root database should only contain other databases. That way, you can use key secrets which correspond to particular applications, instead of configuring your code with root access.

You’ve already created a database in the shell command above, and connected to it. You can create more databases inside my-database by issuing a query like:

CreateDatabase({name : "nested-inside-my-database"}) 

This creates  a new database nested inside of my-database. If you wanted to create another sibling database, located at the root of your account, you could either use the command line interface as you did above to create my-database, or you could launch fauna shell without a database name, in which case it connects to the root context. With a root shell connection, you can issue another query like the above to create a peer database:

CreateDatabase({name : "another-top-level-database"}) 

You can see example database creation queries in more programming languages in the CRUD examples doc.

When creating a database, you have the option to specify an optional priority. Should the underlying FaunaDB cluster become resource-constrained, due to either a hardware failure or a traffic spike, the highest priority databases will be impacted last. This allows you to run development and production workloads on the same cluster, without worry that less important queries can impact performance in production.

Classes (a.k.a. Collections)

Related documents in FaunaDB are stored within classes (also known as collections), which are similar to tables in relational databases, except that the different items in the class are not all required to have the same fields. Typically, classes correspond to types in your application, for example, blog posts, authors, comments, shopping carts, line items, and invoices. Creating a class is easy as you don’t have to specify constraints or field names. In Fauna Shell, the same query looks like this:

CreateClass({ name: "spells" })

You can see this query in other languages in the docs.

Classes are the container for documents (also known as instances). Classes are also the scope for indexes. Since documents contain our data, we can create some documents first, and then define an index on the class so we can query them. Alternatively, we could create an index first, and then add data to the class—it’s just a matter of taste.

Documents (a.k.a. Instances)

Documents contain your application data, which can be stored in fields with types such as string, number, boolean, date, null, etc. Data can also be structured into objects and arrays, which can be nested. Anything that can be represented in JSON can be stored in FaunaDB, as well as richer data types. Here’s an example query (formatted for Fauna Shell) that creates a document:

Create(Class("spells"), {        
    data : {
        title : "Invisibility",    
        ingredients : ["cauldron", "crystal", "newt", "stardust"],                           description : "..." }
})

This document contains a title, ingredients, and a description. Note that these fields are all presented within the data top level field. When retrieving a document, you’ll see other top level fields like ref and ts which track metadata. These fields will also be in the response returned from the Create query above. The most important thing to know is that the ref is how you can load the document in other queries. For instance, if you index the documents by ingredients, the ref for the above document would appear in the result set for “stardust,” and from there you can load the original document. Additionally, if you want to link to this document from another, you can store this document’s ref somewhere in the other document’s data field.

Stay tuned for the next post in this series to learn about creating, reading, updating, and deleting documents.

Indexes

FaunaDB’s indexes give you powerful options when it comes to finding and sorting your data, as well as reading historical changes. Documents can be indexed by term for scalable lookup. To query an index by term, you must provide an exact match, and multiple documents can be found in the same term. Easy examples are tags, or the ingredients in the spell document above. Looking up all the spells that require “stardust” would be as simple as using stardust as a term in an index query. Term indexes use O(1) lookups, so they stay fast no matter how many distinct terms are included in your set, making them good for usernames, product ids, and even ngrams for raw text search.

Documents may also be sorted by a value within a particular term. For instance, the documents within each tag or ingredient set can be sorted by title or publication date. Pagination across the sorted values is designed to be efficient. These indexes are what you would use to query for recent articles by a particular author, or the contents of a user’s inbox sorted by arrival time. If your query can be satisfied by exact lookup instead of range queries, you are likely better off working with terms. For example, if you want to load users by ZIP code, you are better off indexing ZIP code as a term than as a value.

Here is an index definition allowing us to list all of the spells with a given ingredient, sorted by their title:

CreateIndex({ 
    name: "spells_by_ingredient",
    source: Class("spells"),
    terms: [{ field: ["data", "ingredients"] }],
    values: [{ field: ["data", "title"] }] 
})

To query this for all the spells using stardust, we use the Match function to find the corresponding term entry, and then Paginate over the result set. Here is the query formatted for Fauna Shell:

Paginate(Match(Index("spells_by_ingredient"), "stardust"))

Most indexes define a term, which limits the amount of data processed by the Match function. If your index does not define a term, then all documents are indexed under the null term, and sorted according to their ref, which is used as the default value. This can be useful in development as it makes listing all members of a class easy, but it can be a performance hog in production where, generally speaking, you are better off using terms in your indexes, to prevent any one set of values growing too large.

Conclusion

These objects (databases, classes, documents, and indexes) are involved in  the bulk of your work with FaunaDB. If you’d like to learn more about them, the FaunaDB reference guide gives you more detail. The next post in this series will dive into working with documents in detail. Future articles in this series will cover data modeling questions, bulk operations, index queries, working with temporal events, and pagination.

If you enjoyed this topic and want to work on systems and challenges just like this, Fauna is hiring!