Skip to content

Key Paths

You must have at least one key path when defining an Item type. The first Key Path is the primary Key Path; additional Key Paths are aliases. Key Paths are used to locate Items and require careful consideration as they affect how data is laid out in the Store and how it can be accessed. In your Schema, a Key Path for an Item Type is a template composed of segments of (namespace, :field) pairs delimited by slashes. For example: /namespace1-:fieldReference1/namespace2-:pathTo.fieldRef2/....

Let’s see how this looks with example Item types Student and Course:

export const Course = itemType("Course", {
keyPath: [
"/course-:courseId/year-:academicYear/quarter-:academicQuarter",
],
fields: {
courseId: {type: CourseID, fieldNum: 1},
academicYear: {type: uint, fieldNum: 2},
academicQuarter: {type: Quarter, fieldNum: 3},
courseName: {type: string, fieldNum: 4},
description: {type: string, fieldNum: 5},
instructorIds: {type: arrayOf(uint), fieldNum: 6},
// ...any other information related to a course.
},
});
export const Student = itemType("Student", {
keyPath: [
"/student-:studentId",
],
fields: {
studentId: {type: uint, fieldNum: 1},
// ...more fields such as phone number, emergency contact
},
});

Using the schema above, we can retrieve the Student Item with studentId of 1234 by getting /student-1234. We can also fetch information about a particular course in a particular quarter of a particular year using a complete keypath such as /course-MATH321/year-2023/quarter-1, which will fetch the Item for Math-321 in the Autumn quarter of 2023. But we can also fetch all occurrences of a given course across all years and quarters via a List operation with prefix /course-MATH321 or all offerings of a course in a given year via a List with prefix /course-MATH321/year-2023.

Multiple Key Paths

Let’s continue this example to answer the question below:

Why would I want more than one Key Path (an alias Key Path)?

Defining more than one Key Path for an Item is a way to make it possible to access those Items using different fields. In this way, it’s like an index in other databases, though unlike a traditional database, you can also access multiple items at once by Listing with a key path prefix.

StatelyDB guarantees that all Key Paths for a single Item are put, updated or deleted atomically with any change to the Item. Consider the relationship between a student and a class: a class may have many students and a student may be a member of many classes. Our application requires that we are able to answer the questions “Which classes is a student taking?” and “Which students are taking a given class?” Let’s build on the previous example and introduce a third Item type that will act as the glue between Students and Courses — the EnrolledStudent:

const EnrolledStudent = itemType("EnrolledStudent", {
keyPath: [
"/course-:courseId/year-:year/quarter-:quarter/student-:studentId",
"/student-:studentId/year-:year/quarter-:quarter/course-:courseId",
],
fields: {
courseId: {type: CourseID, fieldNum: 1},
year: {type: uint, fieldNum: 2},
quarter: {type: Quarter, fieldNum: 3},
studentId: {type: uint, fieldNum: 4},
// information pertinent to the student's enrollment in the course,
// such as payment status, attendance, exam scores, etc.
},
});

Putting an EnrolledStudent Item will result in two records. One under the Student’s [Group] and one under the Course’s [Group]. This allows us to answer the questions above:

  • “Which classes is a student taking?” - A List with prefix /student-123 will give us all the courses a student has ever participated in.
  • “Which students are taking a given class?” - A List with prefix /course-PHYS341/year-2019 will return all the instances PHYS341 was offered in 2019 as well as all students who enrolled in those courses.

Beyond those questions, we can exploit the structure of these key paths to answer more specific questions. Listing with a prefix of /student-123/year-2019/quarter-3 will return only the courses student 123 participated in the spring quarter of 2019.

Again, you can think of these as a sort of index:

  • /course-:courseId/year-:year/quarter-:quarter/student-:studentId is similar to index EnrolledStudent on (courseId, year, quarter, studentId)
  • /student-:studentId/year-:year/quarter-:quarter/course-:courseId is similar to `index EnrolledStudent on (studentId, year, quarter, courseId)

The wild part is that all Items share the same Key Path space. Since we also have key paths of /course-:courseId/year-:year/quarter-:quarter (for Course) and /student-:studentId (for Student), a single List operation can pick up Students, EnrolledStudents, and Courses all at once.

Frequently Asked Questions

Being able to fetch multiple items using List is the power of an Item type with more than one key path, though it is worth answering a few related questions:

If an Item has more than one Key Path, what happens when I attempt to delete an Item by an ‘alias’ (eg: instead of the primary Key Path)?

Since an alias is a way to address an Item, if you attempt to delete any Item by an alias it will delete the Item. StatelyDB maintains consistency of an Item across all of its Key Paths. In our example above a delete issued to /student-123/year-2019/quarter-3/course-MATH321 will also delete /course-MATH321/year-2019/quarter-3/student-123 and vice versa.

What happens when a field used to calculate an ‘alias’ changes during an Item update?

To better frame this question, let’s continue to examine the example above and imagine there was a typo in the year when writing a EnrolledStudent Item. We meant to use the year 2023 but had accidentally typed 223. Thus we one Item with two Key Paths in our database:

  1. /student-123/year-223/quarter-3/course-MATH321
  2. /course-MATH321/year-223/quarter-3/student-123

…but want the Key Paths:

  1. /student-123/year-2023/quarter-3/course-MATH321
  2. /course-MATH321/year-2023/quarter-3/student-123

How can we correct this problem? If we attempt to write a new EnrolledStudent with an updated year, the primary Key Path of this Item is different than the original Item’s Key Path and thus the write will create a new item. The way to correct the problem is to write a new Item and then delete the incorrect Item.

Let’s modify our schema to make this more interesting by adding a new /classof-:graduatingYear/student-:studentId alias to the Student Item type:

export const Student = itemType("Student", {
keyPath: [
"/student-:studentId",
"/classof-:graduatingYear/student-:studentId",
],
fields: {
studentId: {type: uint, fieldNum: 1},
graduatingYear: {type: uint, fieldNum: 2}
// ... more fields
},
});

Now we have the Student Item stored at the Key Paths:

  1. /student-123
  2. /classof-223/student-123

…but we want the Item stored at the Key Paths:

  1. /student-123
  2. /classof-2023/student-123

How can we correct the problem? Easy! Correct the graduatingYear field in the offending Student Item and write it back to the store. Since graduatingYear is not the unique component of the primary Key Path, StatelyDB understands student 123 should only have one /classof-*/student-123 alias, and will atomically “move” the previous Item /classof-223/student-123 to /classof-2023/student-123.

Is metadata the same between aliased items?

Timestamp metadata is guaranteed to be consistent across all aliases of an Item. However, version metadata may differ depending on the originating Key Path as version is a property of the Group an Item is in.