diff --git a/README.md b/README.md index 33af7257..21a7baa5 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ This repository holds the source code for the C++ SDK Preview. * **Intuitive to Developers:** Realm’s object-oriented data model is simple to learn, doesn’t need an ORM, and lets you write less code. * **Designed for Offline Use:** Realm’s local database persists data on-disk, so apps work as well offline as they do online. * **Built for Mobile:** Realm is fully-featured, lightweight, and efficiently uses memory, disk space, and battery life. -* **[Device Sync](https://www.mongodb.com/docs/atlas/app-services/sync/)**: Makes it simple to keep data in sync across users, devices, and your backend in real-time. [Get started for free](https://www.mongodb.com/docs/realm/sdk/cpp/sync/) and [create the cloud backend](http://mongodb.com/realm/register?utm_medium=github_atlas_CTA&utm_source=realm_cpp_github). ## Object-Oriented: Streamline Your Code @@ -109,9 +108,9 @@ realm.write([&cars](){ ## Getting Started -See the detailed instructions in our [docs](https://www.mongodb.com/docs/atlas/device-sdks/sdk/cpp/). +See the detailed instructions in our [docs](docs/README.md). -The API reference is located [here](https://www.mongodb.com/docs/realm-sdks/cpp/latest/). +The `cpprealm` API reference docs can be generated locally using Doxygen. ## Installing Realm diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..57dcdd1d --- /dev/null +++ b/docs/README.md @@ -0,0 +1,41 @@ +# Realm SDK for C++ +Use Realm SDK for C++ to write applications that access data +stored locally on devices. + +## Get Started with the C++ SDK +These docs provide minimal-explanation code examples of how to work with the C++ SDK. +Use the SDK's open-source database - Realm - as an object store on the +device. + +> **TIP:** See the [Quick Start](/docs/guides/quick-start.md) to integrate +> Realm into your project and get started. +> + +### Install the C++ SDK +To get started, Swift Package Manager or Cmake to install the C++ SDK +in your project. + +Include the header in the translation unit where you want to +use it to get started. + +### Define an Object Schema +Use C++ to idiomatically define an object type. + +### Open a Database +The SDK's database - Realm - stores objects in files on your device. +Configure and open a database +to get started reading and writing data. + +### Read and Write Data +- Create, read, update, and delete objects from the database. +- Filter data using the SDK's query engine. + +### React to Changes +Live objects mean that your data is always up-to-date. +You can register a notification handler +to watch for changes and perform some logic, such as updating +your UI. + +## Recommended Reading +### C++ API Reference +Explore generated Doxygen reference docs for the C++ APIs. diff --git a/docs/guides/crud.md b/docs/guides/crud.md new file mode 100644 index 00000000..ce70d11d --- /dev/null +++ b/docs/guides/crud.md @@ -0,0 +1,9 @@ +# CRUD - C++ SDK +Use the following pages to learn and reference CRUD operations: + +- [Create](crud/create.md) +- [Read](crud/read.md) +- [Update](crud/update.md) +- [Delete](crud/delete.md) +- [Filter Data](crud/filter-data.md) +- [Threading](crud/threading.md) diff --git a/docs/guides/crud/create.md b/docs/guides/crud/create.md new file mode 100644 index 00000000..f24af1cb --- /dev/null +++ b/docs/guides/crud/create.md @@ -0,0 +1,462 @@ +# CRUD - Create - C++ SDK +## Write Transactions +Realm uses a highly efficient storage engine +to persist objects. You can **create** objects in a realm, +**update** objects in a realm, and eventually **delete** +objects from a realm. Because these operations modify the +state of the realm, we call them writes. + +Realm handles writes in terms of **transactions**. A +transaction is a list of read and write operations that +Realm treats as a single indivisible operation. In other +words, a transaction is *all or nothing*: either all of the +operations in the transaction succeed or none of the +operations in the transaction take effect. + +All writes must happen in a transaction. + +A realm allows only one open transaction at a time. Realm +blocks other writes on other threads until the open +transaction is complete. Consequently, there is no race +condition when reading values from the realm within a +transaction. + +When you are done with your transaction, Realm either +**commits** it or **cancels** it: + +- When Realm **commits** a transaction, Realm writes +all changes to disk. +- When Realm **cancels** a write transaction or an operation in +the transaction causes an error, all changes are discarded +(or "rolled back"). + +## Create a New Object +### Create an Object +To create an object, you must instantiate it using the `realm` namespace. +Move the object into the realm using the +Realm.add() function +inside of a write transaction. + +When you move an object into a realm, this consumes the object as an +rvalue. You must use the managed object for any data access or observation. +In this example, copying the `dog` object into the realm consumes +it as an rvalue. You can return the managed object to continue to work +with it. + +```cpp +// Create an object using the `realm` namespace. +auto dog = realm::Dog{.name = "Rex", .age = 1}; + +std::cout << "dog: " << dog.name << "\n"; + +// Open the database with compile-time schema checking. +auto config = realm::db_config(); +auto realm = realm::db(std::move(config)); + +// Persist your data in a write transaction +// Optionally return the managed object to work with it immediately +auto managedDog = realm.write([&] { return realm.add(std::move(dog)); }); + +``` + +#### Model +For more information about modeling an object, refer to: +Define a New Object Type. + +```cpp +namespace realm { +struct Dog { + std::string name; + int64_t age; +}; +REALM_SCHEMA(Dog, name, age) +} // namespace realm + +``` + +### Create an Embedded Object +To create an embedded object, assign the raw pointer of the embedded +object to a parent object's property. Move the parent object into +the realm using the Realm.add() function +inside of a write transaction. + +In this example, we assign the raw pointer of the embedded object - +`ContactDetails *` - to the embedded object property of the parent +object - `Business.contactDetails`. + +Then, we add the `business` object to the realm. This copies the +`business` and `contactDetails` objects to the realm. + +Because `ContactDetails` is an embedded object, it does not have +its own lifecycle independent of the main `Business` object. +If you delete the `Business` object, this also deletes the +`ContactDetails` object. + +```cpp +auto config = realm::db_config(); +auto realm = realm::db(std::move(config)); + +auto contactDetails = realm::ContactDetails{ + .emailAddress = "email@example.com", .phoneNumber = "123-456-7890"}; +auto business = realm::Business(); +business._id = realm::object_id::generate(); +business.name = "my business"; +business.contactDetails = &contactDetails; + +realm.write([&] { realm.add(std::move(business)); }); + +``` + +#### Model +For more information about modeling an embedded object, refer to: +Define an Embedded Object. + +```cpp +namespace realm { +struct ContactDetails { + // Because ContactDetails is an embedded object, it cannot have its own _id + // It does not have a lifecycle outside of the top-level object + std::string emailAddress; + std::string phoneNumber; +}; +REALM_EMBEDDED_SCHEMA(ContactDetails, emailAddress, phoneNumber) + +struct Business { + realm::object_id _id; + std::string name; + ContactDetails *contactDetails; +}; +REALM_SCHEMA(Business, _id, name, contactDetails) +} // namespace realm + +``` + +### Create an Object with a To-One Relationship +To create an object with a to-one relationship to another object, +assign the raw pointer of the related object to the relationship +property of the main object. Move the object into the realm using +the Realm.add() function +inside of a write transaction. + +In this example, we assign the raw pointer of the related object - +`FavoriteToy *` - to the relationship property of the main object +- `Dog.favoriteToy`. Then, when we add the `dog` object to the +realm, this copies both the `dog` and `favoriteToy` to the realm. + +The related `favoriteToy` object has its own lifecycle independent +of the main `dog` object. If you delete the main object, the related +object remains. + +```cpp +auto config = realm::db_config(); +auto realmInstance = realm::db(std::move(config)); + +auto favoriteToy = realm::FavoriteToy{ + ._id = realm::uuid("68b696c9-320b-4402-a412-d9cee10fc6a5"), + .name = "Wubba"}; + +auto dog = realm::Dog{ + ._id = realm::uuid("68b696d7-320b-4402-a412-d9cee10fc6a3"), + .name = "Lita", + .age = 10, + .favoriteToy = &favoriteToy}; + +realmInstance.write([&] { realmInstance.add(std::move(dog)); }); + +``` + +You can optionally create an inverse relationship to refer to the main object +from the related object. For more information, refer to: +Create an Object with an Inverse Relationship. + +#### Model +For more information about modeling a to-one relationship, refer to: +Define a To-One Relationship. + +```cpp +struct FavoriteToy { + realm::primary_key _id; + std::string name; +}; +REALM_SCHEMA(FavoriteToy, _id, name) + +struct Dog { + realm::primary_key _id; + std::string name; + int64_t age; + + // Define a relationship as a link to another SDK object + FavoriteToy* favoriteToy; +}; +REALM_SCHEMA(Dog, _id, name, age, favoriteToy) + +``` + +### Create an Object with a To-Many Relationship +To create an object with a to-many relationship to one or more objects: + +- Initialize the main object and the related objects +- Use the push_back +member function available to the Realm object lists +to append the raw pointers of the related objects to the main object's +list property +- Move the object into the realm using the +Realm.add() function +inside of a write transaction. + +In this example, we append the raw pointers of the related objects - +`Employee *` - to the relationship property of the main object +- `Company.employees`. This creates a one-way connection from the +`Company` object to the `Employee` objects. + +Then, we add the `Company` to the realm. This copies the +`Company` and `Employee` objects to the realm. + +The related `Employee` objects have their own lifecycle independent +of the main `Company` object. If you delete the main object, the +related objects remain. + +```cpp +auto config = realm::db_config(); +auto realmInstance = realm::db(std::move(config)); + +auto employee1 = realm::Employee{ + ._id = 23456, .firstName = "Pam", .lastName = "Beesly"}; + +auto employee2 = realm::Employee{ + ._id = 34567, .firstName = "Jim", .lastName = "Halpert"}; + +auto company = + realm::Company{._id = 45678, .name = "Dunder Mifflin"}; + +// Use the `push_back` member function available to the +// `ListObjectPersistable` template to append `Employee` objects to +// the `Company` `employees` list property. +company.employees.push_back(&employee1); +company.employees.push_back(&employee2); + +realmInstance.write([&] { realmInstance.add(std::move(company)); }); + +``` + +You can optionally create an inverse relationship to refer to the main object +from the related object. For more information, refer to: +Create an Object with an Inverse Relationship. + +#### Model +For more information about modeling a to-many relationship, refer to: +Define a To-Many Relationship. + +```cpp +namespace realm { +struct Employee { + realm::primary_key _id; + std::string firstName; + std::string lastName; + + // You can use this property as you would any other member + // Omitting it from the schema means the SDK ignores it + std::string jobTitle_notPersisted; +}; +// The REALM_SCHEMA omits the `jobTitle_notPersisted` property +// The SDK does not store and cannot retrieve a value for this property +REALM_SCHEMA(Employee, _id, firstName, lastName) +} // namespace realm + +``` + +```cpp +struct Company { + int64_t _id; + std::string name; + // To-many relationships are a list, represented here as a + // vector container whose value type is the SDK object + // type that the list field links to. + std::vector employees; +}; +REALM_SCHEMA(Company, _id, name, employees) + +``` + +### Create an Object with an Inverse Relationship +To create an object with a inverse relationship to another object, +assign the raw pointer of the related object to the relationship +property of the main object. Move the object into the realm using the +Realm.add() function +inside of a write transaction. + +In this example, we create two `Person` objects that each have a to-one +relationship to the same `Dog` object. The `Dog` has an inverse +relationship to each `Person` object. The inverse relationship backlink +is automatically updated when a linked `Person` object updates its +`Dog` relationship. + +```cpp +auto config = realm::db_config(); +auto realm = realm::db(std::move(config)); + +auto dog = realm::Dog{.name = "Bowser"}; + +auto [jack, jill] = realm.write([&realm]() { + auto person = + realm::Person{.name = "Jack", .age = 27, .dog = nullptr}; + + realm::Person person2; + person2.name = "Jill"; + person2.age = 28; + person2.dog = nullptr; + + return realm.insert(std::move(person), std::move(person2)); +}); + +realm.write([&dog, jack = &jack]() { jack->dog = &dog; }); + +// After assigning `&dog` to jack's `dog` property, +// the backlink automatically updates to reflect +// the inverse relationship through the dog's `owners` +// property +CHECK(jack.dog->owners.size() == 1); + +realm.write([&dog, jill = &jill]() { jill->dog = &dog; }); + +// After assigning the same `&dog` to jill's `dog` +// property, the backlink automatically updates +CHECK(jill.dog->owners.size() == 2); +CHECK(jack.dog->owners.size() == 2); + +// Removing the relationship from the parent object +// automatically updates the inverse relationship +realm.write([jack = &jack]() { jack->dog = nullptr; }); +CHECK(jack.dog == nullptr); +CHECK(jill.dog->owners.size() == 1); + +``` + +#### Model +For more information about modeling an inverse relationship, refer to: +Define an Inverse Relationship. + +```cpp +struct Dog; +struct Person { + realm::primary_key _id; + std::string name; + int64_t age = 0; + Dog* dog; +}; +REALM_SCHEMA(Person, _id, name, age, dog) +struct Dog { + realm::primary_key _id; + std::string name; + int64_t age = 0; + linking_objects<&Person::dog> owners; +}; +REALM_SCHEMA(Dog, _id, name, age, owners) + +``` + +### Create an Object with a Map Property +When you create an object that has a +map property, +you can set the values for keys in a few ways: + +- Set keys and values on the object and then add the object to the realm +- Set the object's keys and values directly inside a write transaction + +```cpp +auto config = realm::db_config(); +auto realm = realm::db(std::move(config)); + +auto employee = realm::Employee{ + ._id = 8675309, .firstName = "Tommy", .lastName = "Tutone"}; + +employee.locationByDay = { + {"Monday", realm::Employee::WorkLocation::HOME}, + {"Tuesday", realm::Employee::WorkLocation::OFFICE}, + {"Wednesday", realm::Employee::WorkLocation::HOME}, + {"Thursday", realm::Employee::WorkLocation::OFFICE}}; + +realm.write([&] { + realm.add(std::move(employee)); + employee.locationByDay["Friday"] = realm::Employee::WorkLocation::HOME; +}); + +``` + +Realm disallows the use of `.` or `$` characters in map keys. +You can use percent encoding and decoding to store a map key that contains +one of these disallowed characters. + +```cpp +// Percent encode . or $ characters to use them in map keys +auto mapKey = "Monday.Morning"; +auto encodedMapKey = "Monday%2EMorning"; + +``` + +#### Model +For more information about supported map data types, refer to: +Map/Dictionary. + +```cpp +namespace realm { +struct Employee { + enum class WorkLocation { HOME, OFFICE }; + + int64_t _id; + std::string firstName; + std::string lastName; + std::map locationByDay; +}; +REALM_SCHEMA(Employee, _id, firstName, lastName, locationByDay) +} // namespace realm + +``` + +### Create an Object with a Set Property +You can create objects that contain +set +properties as you would any Realm object, but you can only mutate a set +property within a write transaction. This means you can only set the value(s) +of a set property within a write transaction. + +```cpp +auto realm = realm::db(std::move(config)); + +// Create an object that has a set property +auto docsRealmRepo = + realm::Repository{.ownerAndName = "mongodb/docs-realm"}; + +// Add the object to the database and get the managed object +auto managedDocsRealm = + realm.write([&]() { return realm.add(std::move(docsRealmRepo)); }); + +// Insert items into the set +auto openPullRequestNumbers = {3059, 3062, 3064}; + +realm.write([&] { + for (auto number : openPullRequestNumbers) { + // You can only mutate the set in a write transaction. + // This means you can't set values at initialization, + // but must do it during a write. + managedDocsRealm.openPullRequestNumbers.insert(number); + } +}); + +``` + +#### Model +For more information about supported set data types, refer to: +Set. + +```cpp +namespace realm { +struct Repository { + std::string ownerAndName; + std::set openPullRequestNumbers; +}; +REALM_SCHEMA(Repository, ownerAndName, openPullRequestNumbers) +} // namespace realm + +``` + diff --git a/docs/guides/crud/delete.md b/docs/guides/crud/delete.md new file mode 100644 index 00000000..c9bbc1b1 --- /dev/null +++ b/docs/guides/crud/delete.md @@ -0,0 +1,98 @@ +# CRUD - Delete - C++ SDK +## Delete Realm Objects +Deleting Realm Objects must occur within write transactions. For +more information about write transactions, see: Write Transactions. + +### Delete an Object +To delete an object from a realm, pass the object to +Realm.remove() function +inside of a write transaction. + +```cpp +realm.write([&] { realm.remove(managedDog); }); + +``` + +### Delete an Inverse Relationship +You can't delete an inverse relationship directly. Instead, an +inverse relationship automatically updates by removing the relationship +through the related object. + +In this example, a `Person` has a to-one relationship to a `Dog`, +and the `Dog` has an inverse relationship to `Person`. +Setting the `Person.dog` relationship to `nullptr` removes the inverse +relationship from the `Dog` object. + +```cpp +auto config = realm::db_config(); +auto realm = realm::db(std::move(config)); + +auto dog = realm::Dog{.name = "Wishbone"}; + +auto [joe] = realm.write([&realm]() { + auto person = + realm::Person{.name = "Joe", .age = 27, .dog = nullptr}; + return realm.insert(std::move(person)); +}); + +// Assign an object with an inverse relationship +// to automatically set the value of the inverse relationship +realm.write([&dog, joe = &joe]() { joe->dog = &dog; }); +CHECK(joe.dog->owners.size() == 1); + +// ... Later ... + +// Removing the relationship from the parent object +// automatically updates the inverse relationship +realm.write([joe = &joe]() { joe->dog = nullptr; }); +CHECK(realm.objects()[0].owners.size() == 0); + +``` + +#### Model +This example uses the following model: + +```cpp +struct Dog; +struct Person { + realm::primary_key _id; + std::string name; + int64_t age = 0; + Dog* dog; +}; +REALM_SCHEMA(Person, _id, name, age, dog) +struct Dog { + realm::primary_key _id; + std::string name; + int64_t age = 0; + linking_objects<&Person::dog> owners; +}; +REALM_SCHEMA(Dog, _id, name, age, owners) + +``` + +### Delete Map Keys/Values +To delete a map key, +pass the key name to `erase()`: + +```cpp +realm.write([&] { tommy.locationByDay.erase("Tuesday"); }); + +``` + +### Delete Set Values +You can delete a set element +with `erase()`, or remove all elements from a set with `clear()`. + +```cpp +// Remove an element from the set with erase() +auto it3064 = managedDocsRealm.openPullRequestNumbers.find(3064); +CHECK(it3064 != managedDocsRealm.openPullRequestNumbers.end()); +realm.write([&] { managedDocsRealm.openPullRequestNumbers.erase(it3065); }); +CHECK(managedDocsRealm.openPullRequestNumbers.size() == 4); + +// Clear the entire contents of the set +realm.write([&] { managedDocsRealm.openPullRequestNumbers.clear(); }); +CHECK(managedDocsRealm.openPullRequestNumbers.size() == 0); + +``` diff --git a/docs/guides/crud/filter-data.md b/docs/guides/crud/filter-data.md new file mode 100644 index 00000000..610c7c11 --- /dev/null +++ b/docs/guides/crud/filter-data.md @@ -0,0 +1,155 @@ +# Filter Data - C++ SDK +To filter data in your realm, you can leverage +Realm's query engine. You can also sort filtered data. For an example of +how to sort query results, refer to Sort Lists and Query Results. + +> Note: +> The C++ SDK does not yet support the full range of query expressions +that the other SDKs provide. +> + +## About the Examples on This Page +The examples in this page use a simple data set for a +todo list app. The two Realm object types are `Project` +and `Item`. An `Item` has: + +- A name +- A completed flag +- An optional assignee's name +- A number representing priority, where higher is more important +- A count of minutes spent working on it + +A `Project` has a name and a to-many relationship to zero or more `Items`. + +The schemas for `Project` and `Item` are: + +```cpp +struct Item { + std::string name; + bool isComplete; + std::optional assignee; + int64_t priority; + int64_t progressMinutes; +}; +REALM_SCHEMA(Item, name, isComplete, assignee, priority, progressMinutes) + +struct Project { + std::string name; + std::vector items; +}; +REALM_SCHEMA(Project, name, items) + +``` + +You can set up the realm for these examples with the following code: + +```cpp +auto config = realm::db_config(); +auto realm = realm::db(std::move(config)); + +auto item1 = realm::Item{.name = "Save the cheerleader", + .assignee = std::string("Peter"), + .isComplete = false, + .priority = 6, + .progressMinutes = 30}; + +auto project = realm::Project{.name = "New project"}; + +project.items.push_back(&item1); + +realm.write([&] { realm.add(std::move(project)); }); + +auto items = realm.objects(); +auto projects = realm.objects(); + +``` + +## Filter Data +### Comparison Operators +Value comparisons + +|Operator|Description| +| --- | --- | +|==|Evaluates to `true` if the left-hand expression is equal to the right-hand expression.| +|>|Evaluates to `true` if the left-hand numerical or date expression is greater than the right-hand numerical or date expression. For dates, this evaluates to `true` if the left-hand date is later than the right-hand date.| +|>=|Evaluates to `true` if the left-hand numerical or date expression is greater than or equal to the right-hand numerical or date expression. For dates, this evaluates to `true` if the left-hand date is later than or the same as the right-hand date.| +|<|Evaluates to `true` if the left-hand numerical or date expression is less than the right-hand numerical or date expression. For dates, this evaluates to `true` if the left-hand date is earlier than the right-hand date.| +|<=|Evaluates to `true` if the left-hand numeric expression is less than or equal to the right-hand numeric expression. For dates, this evaluates to `true` if the left-hand date is earlier than or the same as the right-hand date.| +|!=|Evaluates to `true` if the left-hand expression is not equal to the right-hand expression.| + +> Example: +> The following example uses the query engine's +comparison operators to: +> +> - Find high priority tasks by comparing the value of the `priority` property +value with a threshold number, above which priority can be considered high. +> - Find just-started or short-running tasks by seeing if the `progressMinutes` +property falls within a certain range. +> - Find unassigned tasks by finding tasks where the `assignee` property is +equal to `std::nullopt`. +> - Find tasks assigned to specific teammates Ali or Jamie by seeing if the +`assignee` property is in a list of names. +> +> ```cpp +> auto highPriorityItems = +> items.where([](auto const& item) { return item.priority > 5; }); +> +> auto quickItems = items.where([](auto const& item) { +> return item.progressMinutes > 1 && item.progressMinutes < 30; +> }); +> +> auto unassignedItems = items.where( +> [](auto const& item) { return item.assignee == std::nullopt; }); +> +> auto aliOrJamieItems = items.where([](auto const& item) { +> return item.assignee == std::string("Ali") || +> item.assignee == std::string("Jamie"); +> }); +> +> ``` +> + +### Logical Operators +You can use the logical operators listed in the following table to make compound +predicates: + +|Operator|Description| +| --- | --- | +|&&|Evaluates to `true` if both left-hand and right-hand expressions are `true`.| +|!|Negates the result of the given expression.| +|\\|\\||Evaluates to `true` if either expression returns `true`.| + +> Example: +> We can use the query language's logical operators to find +all of Ali's completed tasks. That is, we find all tasks +where the `assignee` property value is equal to 'Ali' AND +the `isComplete` property value is `true`: +> +> ```cpp +> auto completedItemsForAli = items.where([](auto const& item) { +> return item.assignee == std::string("Ali") && item.isComplete == true; +> }); +> +> ``` +> + +### String Operators +You can compare string values using these string operators. + +|Operator|Description| +| --- | --- | +|.contains(_ value: String)|Evaluates to `true` if the left-hand string expression is found anywhere in the right-hand string expression.| +|==|Evaluates to `true` if the left-hand string is lexicographically equal to the right-hand string.| +|!=|Evaluates to `true` if the left-hand string is not lexicographically equal to the right-hand string.| + +> Example: +> The following example uses the query engine's string operators to find: +> +> - Projects with names that contain 'ie' +> +> ```cpp +> auto containIe = +> items.where([](auto const& item) { return item.name.contains("ie"); }); +> +> ``` +> diff --git a/docs/guides/crud/read.md b/docs/guides/crud/read.md new file mode 100644 index 00000000..7fc38772 --- /dev/null +++ b/docs/guides/crud/read.md @@ -0,0 +1,204 @@ +# CRUD - Read - C++ SDK +You can read back the data that you have stored in Realm by finding, +filtering, and sorting objects. + +A read from a realm generally consists of the following +steps: + +- Get all objects of a certain type from the realm. +- Optionally, filter the results. + +Query operations return a +results collection. These +collections are live, meaning they always contain the latest +results of the associated query. + +## Read Characteristics +Design your app's data access patterns around these three key +read characteristics to read data as efficiently as possible. + +### Results Are Not Copies +Results to a query are not copies of your data. Modifying +the results of a query modifies the data on disk +directly. This memory mapping also means that results are +**live**: that is, they always reflect the current state on +disk. + +### Results Are Lazy +Realm only runs a query when you actually request the +results of that query. This lazy evaluation enables you to write +highly performant code for handling large data sets and complex +queries. You can chain several filter operations without requiring +extra work to process the intermediate state. + +### References Are Retained +One benefit of Realm's object model is that +Realm automatically retains all of an object's +relationships as direct +references. This enables you to traverse your graph of relationships +directly through the results of a query. + +A **direct reference**, or pointer, allows you to access a +related object's properties directly through the reference. + +Other databases typically copy objects from database storage +into application memory when you need to work with them +directly. Because application objects contain direct +references, you are left with a choice: copy the object +referred to by each direct reference out of the database in +case it's needed, or just copy the foreign key for each +object and query for the object with that key if it's +accessed. If you choose to copy referenced objects into +application memory, you can use up a lot of resources for +objects that are never accessed, but if you choose to only +copy the foreign key, referenced object lookups can cause +your application to slow down. + +Realm bypasses all of this using zero-copy +live objects. Realm object accessors point +directly into database storage using memory mapping, so there is no +distinction between the objects in Realm and the results +of your query in application memory. Because of this, you can traverse +direct references across an entire realm from any query result. + +### Limiting Query Results +As a result of lazy evaluation, you do not need any special mechanism to +limit query results with Realm. For example, if your query +matches thousands of objects, but you only want to load the first ten, +simply access only the first ten elements of the results collection. + +### Pagination +Thanks to lazy evaluation, the common task of pagination becomes quite +simple. For example, suppose you have a results collection associated +with a query that matches thousands of objects in your realm. You +display one hundred objects per page. To advance to any page, simply +access the elements of the results collection starting at the index that +corresponds to the target page. + +## Read Realm Objects +### Query All Objects of a Given Type +To query for objects of a given type in a realm, pass the object type +`YourClassName` to the realm::query member function. + +This returns a Results object +representing all objects of the given type in the realm. + +```cpp +auto managedBusinesses = realm.objects(); + +``` + +### Filter Queries Based on Object Properties +A filter selects a subset of results based on the value(s) of one or +more object properties. Realm provides a full-featured +query engine that you can use to define filters. + +```cpp +auto businessesNamedMongoDB = managedBusinesses.where( + [](auto &business) { return business.name == "my business"; }); + +``` + +### Supported Query Operators +Currently, the Realm C++ SDK supports the following query operators: + +- Equality (`==`, `!=`) +- Greater than/less than (`>`, `>=`, `<`, `<=`) +- Compound queries (`||`, `&&`) + +### Check the Size of the Results Set and Access Results +Realm Results exposes member +functions to work with results. You may want to check the size of a results +set, or access the object at a specific index. + +```cpp +auto managedBusinesses = realm.objects(); +auto businessesNamedMongoDB = managedBusinesses.where( + [](auto &business) { return business.name == "my business"; }); +CHECK(businessesNamedMongoDB.size() >= 1); +auto mongoDB = businessesNamedMongoDB[0]; + +``` + +Additionally, you can iterate through the results, or observe a results +set for changes. + +### Read a Map Property +You can iterate and check the values of a realm map property +as you would a standard C++ [map](https://en.cppreference.com/w/cpp/container/map): + +```cpp +auto employees = realm.objects(); +auto employeesNamedTommy = employees.where( + [](auto &employee) { return employee.firstName == "Tommy"; }); +auto tommy = employeesNamedTommy[0]; +// You can get an iterator for an element matching a key using `find()` +auto tuesdayIterator = tommy.locationByDay.find("Tuesday"); + +// You can access values for keys like any other map type +auto mondayLocation = tommy.locationByDay["Monday"]; + +``` + +### Read a Set Property +You can iterate, check the size of a set, and find values in a set property: + +```cpp +auto repositories = realm.objects(); + +auto repositoriesNamedDocsRealm = repositories.where([](auto &repository) { + return repository.ownerAndName == "mongodb/docs-realm"; +}); + +auto docsRealm = repositoriesNamedDocsRealm[0]; + +// You can check the size of the set +auto numberOfPullRequests = docsRealm.openPullRequestNumbers.size(); + +// Find an element in the set whose value is 3064 +auto it = managedDocsRealm.openPullRequestNumbers.find(3064); + +// Get a copy of the set that exists independent of the managed set +auto openRealmPullRequests = docsRealm.openPullRequestNumbers.detach(); + +``` + +## Sort Lists and Query Results +A sort operation allows you to configure the order in which Realm returns +list objects and query results. You can sort based on one or more properties +of the objects in the list or results collection. Realm only guarantees a +consistent order if you explicitly sort. + +Unlike using [std::sort](https://en.cppreference.com/w/cpp/algorithm/sort), +Realm's sort implementation preserves the lazy loading of objects. It does +not pull the entire set of results or list objects into memory, but only +loads them into memory when you access them. + +To sort, call the `.sort()` function on a list or results set with one or more +sort_descriptors. + +A `sort_descriptor` includes: + +- The desired key path to sort by, as a string. +- A bool to specify sort order, where``true`` is ascending and `false` +is descending. + +In this example, we sort a results set on `priority` in descending order. + +```cpp +auto items = realm.objects(); + +// Sort with `false` returns objects in descending order. +auto itemsSorted = items.sort("priority", false); + +``` + +You can also sort a list or results set by multiple sort descriptors. In +this example, we sort a list property on `assignee` in ascending order, +and then on `priority` in descending order. + +```cpp +auto sortedListProperty = + specificProject.items.sort({{"assignee", true}, {"priority", false}}); + +``` diff --git a/docs/guides/crud/threading.md b/docs/guides/crud/threading.md new file mode 100644 index 00000000..95f8d6fd --- /dev/null +++ b/docs/guides/crud/threading.md @@ -0,0 +1,480 @@ +# Threading - C++ SDK +To create performant apps, developers must write thread-safe and maintainable +multithreaded code that avoids issues like deadlocking and race conditions. +Realm provides tools specifically designed for performant multithreaded apps. + +## Three Rules to Follow +Before exploring Realm's tools for multithreaded apps, you need to +understand and follow these three rules: + +Realm's Multiversion Concurrency Control (MVCC) +architecture eliminates the need to lock for read operations. The values you +read will never be corrupted or in a partially-modified state. You can freely +read from the same Realm file on any thread without the need for locks or +mutexes. Unnecessarily locking would be a performance bottleneck since each +thread might need to wait its turn before reading. + +You can write to a Realm file from any thread, but there can be only one +writer at a time. Consequently, synchronous write transactions block each +other. A synchronous write on the UI thread may result in your app appearing +unresponsive while it waits for a write on a background thread to complete. + +Live objects, collections, and realm instances are **thread-confined**: that +is, they are only valid on the thread on which they were created. Practically +speaking, this means you cannot pass live instances to other threads. However, +Realm offers several mechanisms for sharing objects across +threads. + +## Communication Across Threads +To access the same Realm file from different threads, you must instantiate a +realm instance on every thread that needs access. As long as you specify the same +configuration, all realm instances will map to the same file on disk. + +One of the key rules when working with Realm in a multithreaded +environment is that objects are thread-confined: **you cannot access the +instances of a realm, collection, or object that originated on other threads.** +Realm's Multiversion Concurrency Control (MVCC) +architecture means that there could be many active versions of an object at any +time. Thread-confinement ensures that all instances in that thread are of the +same internal version. + +When you need to communicate across threads, you have several options depending +on your use case: + +- To modify an object on two threads, query +for the object on both threads. +- To react to changes made on any thread, use Realm's +notifications. +- To see changes that happened on another thread in the current thread's realm +instance, refresh your realm instance. +- To share an instance of a realm or specific object with another thread, share +a thread_safe_reference to the realm +instance or object. +- To send a fast, read-only view of the object to other threads, +"freeze" the object. + +### Pass Instances Across Threads +Instances of `realm::realm`, `realm::results`, and `realm::object` are +*thread-confined*. That means you may only use them on the thread where you +created them. + +You can copy thread-confined instances to another thread as follows: + +1. Initialize a thread_safe_reference +with the thread-confined object. +2. Pass the reference to the target thread. +3. Resolve the reference on the target thread. If the referred object is a realm +instance, resolve it by calling `.resolve()`; otherwise, move the reference +to `realm.resolve()`. The returned object is now thread-confined on the +target thread, as if it had been created on the target thread instead of the +original thread. + +> Important: +> You must resolve a `thread_safe_reference` exactly once. Otherwise, +the source realm will remain pinned until the reference gets +deallocated. For this reason, `thread_safe_reference` should be +short-lived. +> + +```cpp +// Put a managed object into a thread safe reference +auto threadSafeItem = + realm::thread_safe_reference{managedItem}; + +// Move the thread safe reference to a background thread +auto thread = + std::thread([threadSafeItem = std::move(threadSafeItem), path]() mutable { + // Open the database again on the background thread + auto backgroundConfig = realm::db_config(); + backgroundConfig.set_path(path); + auto backgroundRealm = realm::db(std::move(backgroundConfig)); + + // Resolve the Item instance via the thread safe + // reference + auto item = backgroundRealm.resolve(std::move(threadSafeItem)); + + // ... use item ... + }); + +// Wait for thread to complete +thread.join(); + +``` + +Another way to work with an object on another thread is to query for it +again on that thread. But if the object does not have a primary +key, it is not trivial to query for it. You can use `thread_safe_reference` +on any object, regardless of whether it has a primary key. + +### Use the Same Realm Across Threads +You cannot share realm instances across threads. + +To use the same Realm file across threads, open a different realm instance on +each thread. As long as you use the same configuration, all Realm +instances will map to the same file on disk. + +### Pass Immutable Copies Across Threads +Live, thread-confined objects work fine in most cases. +However, some apps -- those based on reactive, event +stream-based architectures, for example -- need to send +immutable copies around to many threads for processing +before ultimately ending up on the UI thread. Making a deep +copy every time would be expensive, and Realm does not allow +live instances to be shared across threads. In this case, +you can **freeze** and **thaw** objects, collections, and realms. + +Freezing creates an immutable view of a specific object, +collection, or realm. The frozen object, collection, or realm still +exists on disk, and does not need to be deeply copied when passed around +to other threads. You can freely share the frozen object across threads +without concern for thread issues. When you freeze a realm, its child +objects also become frozen. + +Frozen objects are not live and do not automatically update. +They are effectively snapshots of the object state at the +time of freezing. Thawing an object returns a live version of the frozen +object. + +To freeze a realm, collection, or object, call the `.freeze()` method: + +```cpp +auto realm = realm::db(std::move(config)); + +// Get an immutable copy of the database that can be passed across threads. +auto frozenRealm = realm.freeze(); + +if (frozenRealm.is_frozen()) { + // Do something with the frozen database. + // You may pass a frozen realm, collection, or objects + // across threads. Or you may need to `.thaw()` + // to make it mutable again. +} + +// You can freeze collections. +auto managedItems = realm.objects(); +auto frozenItems = managedItems.freeze(); + +CHECK(frozenItems.is_frozen()); + +// You can read from frozen databases. +auto itemsFromFrozenRealm = + frozenRealm.objects(); + +CHECK(itemsFromFrozenRealm.is_frozen()); + +// You can freeze objects. +auto managedItem = managedItems[0]; +auto frozenItem = managedItem.freeze(); + +CHECK(frozenItem.is_frozen()); + +// Frozen objects have a reference to a frozen realm. +CHECK(frozenItem.get_realm().is_frozen()); + +``` + +When working with frozen objects, an attempt to do any of +the following throws an exception: + +- Opening a write transaction on a frozen realm. +- Modifying a frozen object. +- Adding a change listener to a frozen realm, collection, or object. + +You can use `.is_frozen()` to check if the object is frozen. This is always +thread-safe. + +```cpp +if (frozenRealm.is_frozen()) { + // Do something with the frozen database. + // You may pass a frozen realm, collection, or objects + // across threads. Or you may need to `.thaw()` + // to make it mutable again. +} + +``` + +Frozen objects remain valid as long as the live realm that +spawned them stays open. Therefore, avoid closing the live +realm until all threads are done with the frozen objects. +You can close a frozen realm before the live realm is closed. + +> Important: +> Caching too many frozen objects can have a negative +impact on the realm file size. "Too many" depends on your +specific target device and the size of your Realm +objects. If you need to cache a large number of versions, +consider copying what you need out of the realm instead. +> + +#### Modify a Frozen Object +To modify a frozen object, you must thaw the object. Alternately, you can +query for it on an unfrozen realm, then modify it. Calling `.thaw()` +on a live object, collection, or realm returns itself. + +Thawing an object or collection also thaws the realm it references. + +```cpp +// Read from a frozen database. +auto frozenItems = frozenRealm.objects(); + +// The collection that we pull from the frozen database is also frozen. +CHECK(frozenItems.is_frozen()); + +// Get an individual item from the collection. +auto frozenItem = frozenItems[0]; + +// To modify the item, you must first thaw it. +// You can also thaw collections and realms. +auto thawedItem = frozenItem.thaw(); + +// Check to make sure the item is valid. An object is +// invalidated when it is deleted from its managing database, +// or when its managing realm has invalidate() called on it. +REQUIRE(thawedItem.is_invalidated() == false); + +// Thawing the item also thaws the frozen database it references. +auto thawedRealm = thawedItem.get_realm(); +REQUIRE(thawedRealm.is_frozen() == false); + +// With both the object and its managing database thawed, you +// can safely modify the object. +thawedRealm.write([&] { thawedItem.name = "Save the world"; }); + +``` + +#### Append to a Frozen Collection +When you append to a frozen collection, +you must thaw both the object containing the collection and the object +that you want to append. + +The same rule applies when passing frozen objects across threads. A common +case might be calling a function on a background thread to do some work +instead of blocking the UI. + +#### Append To Collection + +In this example, we query for two objects in a frozen Realm: + +- A `Project` object that has a list property of `Item` objects +- An `Item` object + +We must thaw both objects before we can append the `Item` to +the `items` list collection on the `Project`. If we thaw only the +`Project` object but not the `Item`, Realm throws an error. + +```cpp +// Get frozen objects. +// Here, we're getting them from a frozen database, +// but you might also be passing them across threads. +auto frozenItems = frozenRealm.objects(); + +// The collection that we pull from the frozen database is also frozen. +CHECK(frozenItems.is_frozen()); + +// Get the individual objects we want to work with. +auto specificFrozenItems = frozenItems.where( + [](auto const& item) { return item.name == "Save the cheerleader"; }); +auto frozenProjects = + frozenRealm.objects().where( + [](auto const& project) { + return project.name == "Heroes: Genesis"; + }); +; +auto frozenItem = specificFrozenItems[0]; +auto frozenProject = frozenProjects[0]; + +// Thaw the frozen objects. You must thaw both the object +// you want to append and the object whose collection +// property you want to append to. +auto thawedItem = frozenItem.thaw(); +auto thawedProject = frozenProject.thaw(); + +auto managingRealm = thawedProject.get_realm(); +managingRealm.write([&] { thawedProject.items.push_back(thawedItem); }); + +``` + +#### Models + +```cpp +struct Item { + std::string name; +}; +REALM_SCHEMA(Item, name) + +``` + +```cpp +struct Project { + std::string name; + std::vector items; +}; +REALM_SCHEMA(Project, name, items) + +``` + +## Schedulers (Run Loops) +Some platforms or frameworks automatically set up a **scheduler** (or **run +loop**), which continuously processes events during the lifetime of your +app. The Realm C++ SDK detects and uses schedulers on the following +platforms or frameworks: + +- macOS, iOS, tvOS, watchOS +- Android +- Qt + +Realm uses the scheduler to schedule work. + +If your platform does not have a supported scheduler, or you otherwise want to +use a custom scheduler, you can implement realm::scheduler and pass the instance to the realm::db_config you use to +configure the realm. Realm will use the scheduler you pass to it. + +```cpp + struct MyScheduler : realm::scheduler { + MyScheduler() { + // ... Kick off task processor thread(s) and run until the scheduler + // goes out of scope ... + } + + ~MyScheduler() override { + // ... Call in the processor thread(s) and block until return ... + } + + void invoke(std::function&& task) override { + // ... Add the task to the (lock-free) processor queue ... + } + + [[nodiscard]] bool is_on_thread() const noexcept override { + // ... Return true if the caller is on the same thread as a processor + // thread ... + } + + bool is_same_as(const realm::scheduler* other) const noexcept override { + // ... Compare scheduler instances ... + } + + [[nodiscard]] bool can_invoke() const noexcept override { + // ... Return true if the scheduler can accept tasks ... + } + // ... + }; + + int main() { + // Set up a custom scheduler. + auto scheduler = std::make_shared(); + + // Pass the scheduler instance to the realm configuration. + auto config = realm::db_config{path, scheduler}; + + // Start the program main loop. + auto done = false; + while (!done) { + // This assumes the scheduler is implemented so that it + // continues processing tasks on background threads until + // the scheduler goes out of scope. + + // Handle input here. + // ... + if (shouldQuitProgram) { + done = true; + } + } + } + +``` + +## Refreshing Realms +On any thread controlled by a scheduler or run loop, Realm automatically +refreshes objects at the start of every run loop iteration. Between run loop +iterations, you will be working on the snapshot, so individual methods always +see a consistent view and never have to worry about what happens on other +threads. + +When you initially open a realm on a thread, its state will be the most recent +successful write commit, and it will remain on that version until refreshed. If +a thread is not controlled by a run loop, then the realm.refresh() method must be called manually in order to advance the +transaction to the most recent state. + +```cpp +realm.refresh(); + +``` + +> Note: +> Failing to refresh realms on a regular basis could lead to some transaction +versions becoming "pinned", preventing Realm from reusing the disk space used +by that version and leading to larger file sizes. +> + +## Realm's Threading Model in Depth +Realm provides safe, fast, lock-free, and concurrent access +across threads with its [Multiversion Concurrency +Control (MVCC)](https://en.wikipedia.org/wiki/Multiversion_concurrency_control) +architecture. + +### Compared and Contrasted with Git +If you are familiar with a distributed version control +system like [Git](https://git-scm.com/), you may already +have an intuitive understanding of MVCC. Two fundamental +elements of Git are: + +- Commits, which are atomic writes. +- Branches, which are different versions of the commit history. + +Similarly, Realm has atomically-committed writes in the form +of transactions. Realm also has many different versions of the +history at any given time, like branches. + +Unlike Git, which actively supports distribution and +divergence through forking, a realm only has one true latest +version at any given time and always writes to the head of +that latest version. Realm cannot write to a previous +version. This means your data converges on one +latest version of the truth. + +### Internal Structure +A realm is implemented using a [B+ tree](https://en.wikipedia.org/wiki/B%2B_tree) data structure. The top-level node represents a +version of the realm; child nodes are objects in that +version of the realm. The realm has a pointer to its latest +version, much like how Git has a pointer to its HEAD commit. + +Realm uses a copy-on-write technique to ensure +[isolation](https://en.wikipedia.org/wiki/Isolation_(database_systems)) and +[durability](https://en.wikipedia.org/wiki/Durability_(database_systems)). +When you make changes, Realm copies the relevant part of the +tree for writing. Realm then commits the changes in two +phases: + +- Realm writes changes to disk and verifies success. +- Realm then sets its latest version pointer to point to the newly-written version. + +This two-step commit process guarantees that even if the +write failed partway, the original version is not corrupted +in any way because the changes were made to a copy of the +relevant part of the tree. Likewise, the realm's root +pointer will point to the original version until the new +version is guaranteed to be valid. + +> Example: +> The following diagram illustrates the commit process: +> +> ![Realm copies the relevant part of the tree for writes, then replaces the latest version by updating a pointer.](../../../images/mvcc-diagram.png) +> +> 1. The realm is structured as a tree. The realm has a pointer +to its latest version, V1. +> 2. When writing, Realm creates a new version V2 based on V1. +Realm makes copies of objects for modification (A 1, +C 1), while links to unmodified objects continue to +point to the original versions (B, D). +> 3. After validating the commit, Realm updates the +pointer to the new latest version, V2. Realm then discards +old nodes no longer connected to the tree. +> + +Realm uses zero-copy techniques +like memory mapping to handle data. When you read a value +from the realm, you are virtually looking at the value on +the actual disk, not a copy of it. This is the basis for +live objects. This is also why a realm +head pointer can be set to point to the new version after +the write to disk has been validated. diff --git a/docs/guides/crud/update.md b/docs/guides/crud/update.md new file mode 100644 index 00000000..0e05e39d --- /dev/null +++ b/docs/guides/crud/update.md @@ -0,0 +1,261 @@ +# CRUD - Update - C++ SDK +Updates to Realm Objects must occur within write transactions. For +more information about write transactions, see: Write +Transactions. + +## Update an Object +You can modify properties of a Realm object inside of a write transaction. + +```cpp +// Query for the object you want to update +auto dogs = realm.objects(); +auto dogsNamedMaui = + dogs.where([](auto &dog) { return dog.name == "Maui"; }); +CHECK(dogsNamedMaui.size() >= 1); +// Access an object in the results set. +auto maui = dogsNamedMaui[0]; + +std::cout << "Dog " << maui.name.detach() << " is " << maui.age.detach() + << " years old\n"; + +// Assign a new value to a member of the object in a write transaction +int64_t newAge = 2; +realm.write([&] { maui.age = newAge; }); + +``` + +**Model** + +This example uses the following model: + +```cpp +struct Dog { + std::string name; + int64_t age; +}; +REALM_SCHEMA(Dog, name, age) + +``` + +### Update an Embedded Object Property +To update a property in an embedded object, modify the property in a +write transaction. + +```cpp +auto managedBusinesses = realm.objects(); +auto businessesNamedmyBusiness = managedBusinesses.where( + [](auto &business) { return business.name == "my business"; }); +CHECK(businessesNamedmyBusiness.size() >= 1); +auto myBusiness = businessesNamedmyBusiness[0]; + +realm.write( + [&] { myBusiness.contactDetails->emailAddress = "info@example.com"; }); + +std::cout << "New email address: " + << myBusiness.contactDetails->emailAddress.detach() << "\n"; + +``` + +**Model** + +This example uses the following model: + +```cpp +namespace realm { +struct ContactDetails { + // Because ContactDetails is an embedded object, it cannot have its own _id + // It does not have a lifecycle outside of the top-level object + std::string emailAddress; + std::string phoneNumber; +}; +REALM_EMBEDDED_SCHEMA(ContactDetails, emailAddress, phoneNumber) + +struct Business { + realm::object_id _id; + std::string name; + ContactDetails *contactDetails; +}; +REALM_SCHEMA(Business, _id, name, contactDetails) +} // namespace realm + +``` + +### Overwrite an Embedded Object Property +To overwrite an embedded object, reassign the embedded object property +to the raw pointer of a new instance in a write transaction. + +```cpp +auto businesses = realm.objects(); +auto theBusinesses = businesses.where( + [](auto &business) { return business.name == "my business"; }); +auto theBusiness = theBusinesses[0]; + +realm.write([&] { + auto newContactDetails = realm::ContactDetails{ + .emailAddress = "info@example.com", .phoneNumber = "234-567-8901"}; + // Overwrite the embedded object + theBusiness.contactDetails = &newContactDetails; +}); + +``` + +**Model** + +This example uses the following model: + +```cpp +auto managedBusinesses = realm.objects(); +auto businessesNamedmyBusiness = managedBusinesses.where( + [](auto &business) { return business.name == "my business"; }); +CHECK(businessesNamedmyBusiness.size() >= 1); +auto myBusiness = businessesNamedMyBusiness[0]; + +realm.write( + [&] { myBusiness.contactDetails->emailAddress = "info@example.com"; }); + +std::cout << "New email address: " + << myBusiness.contactDetails->emailAddress.detach() << "\n"; + +``` + +### Update an Inverse Relationship +You can't update an inverse relationship property directly. Instead, an +inverse relationship automatically updates by changing assignment through +its relevant related object. + +In this example, a `Person` object has a to-one relationship to a +`Dog` object, and `Dog` has an inverse relationship to `Person`. The +inverse relationship automatically updates when the `Person` object updates +its `Dog` relationship. + +```cpp +auto config = realm::db_config(); +auto realm = realm::db(std::move(config)); + +auto dog = realm::Dog{.name = "Wishbone"}; + +auto [joe] = realm.write([&realm]() { + auto person = + realm::Person{.name = "Joe", .age = 27, .dog = nullptr}; + return realm.insert(std::move(person)); +}); + +realm.write([&dog, joe = &joe]() { joe->dog = &dog; }); + +// After assigning `&dog` to jack's `dog` property, +// the backlink automatically updates to reflect +// the inverse relationship through the dog's `owners` +// property. +CHECK(joe.dog->owners.size() == 1); + +``` + +**Model** + +This example uses the following model: + +```cpp +struct Dog; +struct Person { + realm::primary_key _id; + std::string name; + int64_t age = 0; + Dog* dog; +}; +REALM_SCHEMA(Person, _id, name, age, dog) +struct Dog { + realm::primary_key _id; + std::string name; + int64_t age = 0; + linking_objects<&Person::dog> owners; +}; +REALM_SCHEMA(Dog, _id, name, age, owners) + +``` + +### Update a Map Property +You can update a realm map property +as you would a standard C++ [map](https://en.cppreference.com/w/cpp/container/map): + +```cpp +// You can check that a key exists using `find` +auto findTuesday = tommy.locationByDay.find("Tuesday"); +if (findTuesday != tommy.locationByDay.end()) + realm.write([&] { + tommy.locationByDay["Tuesday"] = + realm::Employee::WorkLocation::HOME; + }); +; + +``` + +**Model** + +This example uses the following model: + +```cpp +namespace realm { +struct Employee { + enum class WorkLocation { HOME, OFFICE }; + + int64_t _id; + std::string firstName; + std::string lastName; + std::map locationByDay; +}; +REALM_SCHEMA(Employee, _id, firstName, lastName, locationByDay) +} // namespace realm + +``` + +### Update a Set Property +You can update a set property +in a write transaction. You can `.insert()` elements into the set, `erase()` +elements from a set, or use `std::set` algorithms to mutate sets: + +- [std::set_union](https://en.cppreference.com/w/cpp/algorithm/set_union) +- [std::set_intersection](https://en.cppreference.com/w/cpp/algorithm/set_intersection) +- [std::set_difference](https://en.cppreference.com/w/cpp/algorithm/set_difference) +- [std::set_symmetric_difference](https://en.cppreference.com/w/cpp/algorithm/set_symmetric_difference) + +```cpp +// Add elements to the set in a write transaction +realm.write([&] { managedDocsRealm.openPullRequestNumbers.insert(3066); }); +CHECK(managedDocsRealm.openPullRequestNumbers.size() == 4); + +// Use std::set algorithms to update a set +// In this example, use std::set_union to add elements to the set +// 3064 already exists, so it won't be added, but 3065 and 3067 are +// unique values and will be added to the set. +auto newOpenPullRequests = std::set({3064, 3065, 3067}); +realm.write([&] { + std::set_union( + docsRealm.openPullRequestNumbers.begin(), + docsRealm.openPullRequestNumbers.end(), newOpenPullRequests.begin(), + newOpenPullRequests.end(), + std::inserter(managedDocsRealm.openPullRequestNumbers, + managedDocsRealm.openPullRequestNumbers.end())); +}); +CHECK(managedDocsRealm.openPullRequestNumbers.size() == 6); + +// Erase elements from a set +auto it3065 = managedDocsRealm.openPullRequestNumbers.find(3065); +CHECK(it3065 != managedDocsRealm.openPullRequestNumbers.end()); +realm.write([&] { managedDocsRealm.openPullRequestNumbers.erase(it3065); }); + +``` + +**Model** + +This example uses the following model: + +```cpp +namespace realm { +struct Repository { + std::string ownerAndName; + std::set openPullRequestNumbers; +}; +REALM_SCHEMA(Repository, ownerAndName, openPullRequestNumbers) +} // namespace realm + +``` diff --git a/docs/guides/images/geoboxes-query.png b/docs/guides/images/geoboxes-query.png new file mode 100644 index 00000000..0027f68f Binary files /dev/null and b/docs/guides/images/geoboxes-query.png differ diff --git a/docs/guides/images/geoboxes.png b/docs/guides/images/geoboxes.png new file mode 100644 index 00000000..d4ebce77 Binary files /dev/null and b/docs/guides/images/geoboxes.png differ diff --git a/docs/guides/images/geocircles-query.png b/docs/guides/images/geocircles-query.png new file mode 100644 index 00000000..99644c77 Binary files /dev/null and b/docs/guides/images/geocircles-query.png differ diff --git a/docs/guides/images/geocircles.png b/docs/guides/images/geocircles.png new file mode 100644 index 00000000..490ba2f6 Binary files /dev/null and b/docs/guides/images/geocircles.png differ diff --git a/docs/guides/images/geopoints.png b/docs/guides/images/geopoints.png new file mode 100644 index 00000000..57f99e27 Binary files /dev/null and b/docs/guides/images/geopoints.png differ diff --git a/docs/guides/images/geopolygons-query.png b/docs/guides/images/geopolygons-query.png new file mode 100644 index 00000000..7005c6d2 Binary files /dev/null and b/docs/guides/images/geopolygons-query.png differ diff --git a/docs/guides/images/geopolygons.png b/docs/guides/images/geopolygons.png new file mode 100644 index 00000000..a40b6812 Binary files /dev/null and b/docs/guides/images/geopolygons.png differ diff --git a/docs/guides/images/mvcc-diagram.png b/docs/guides/images/mvcc-diagram.png new file mode 100644 index 00000000..adae20b5 Binary files /dev/null and b/docs/guides/images/mvcc-diagram.png differ diff --git a/docs/guides/install.md b/docs/guides/install.md new file mode 100644 index 00000000..90c1cffe --- /dev/null +++ b/docs/guides/install.md @@ -0,0 +1,144 @@ +# Install the C++ SDK +Realm SDK for C++ enables client applications written in C++ to access +data stored on devices. This page details how to +install the C++ SDK in your project and get started. + +## Requirements +- Minimum C++ standard: C++17. +- For development on macOS: Xcode 11.x or later. +- For development on Windows: Microsoft Visual C++ (MSVC). +- Otherwise, we recommend git and [CMake](https://cmake.org). + +## Install +> Tip: +> The SDK uses Realm Core database for device data persistence. When you +install the C++ SDK, the package names reflect Realm naming. +> + +#### Swiftpm + +When developing with Xcode, you can use Swift Package Manager (SPM) to +install realm-cpp. + +##### Add Package Dependency +In Xcode, select `File` > `Add Packages...`. + +##### Specify the Repository +Copy and paste the following into the search/input box. + +```sh +https://github.com/realm/realm-cpp +``` + +##### Select the Package Products +Under Package Product, select `realm-cpp-sdk`. Under +Add to Target, select the target you would like to add +the SDK to. For example, the target might be the main executable of +your app. Click Add Package. + +#### Cmake + +You can use CMake with the FetchContent module to manage the SDK and its +dependencies in your C++ project. + +Create or modify your `CMakeLists.txt` in the root directory of your +project: + +1. Add `Include(FetchContent)` to include the FetchContent module +in your project build. +2. Use `FetchContent_Declare` to locate the SDK dependency +and specify the version tag you want to use. +3. Use the `FetchContent_MakeAvailable()` command to check whether +the named dependencies have been populated, and if not, populate them. +4. Finally, `target_link_libraries()` links the SDK dependency to +your target executable. + +To get the most recent version tag, refer to the releases on GitHub: +[realm/realm-cpp](https://github.com/realm/realm-cpp/releases). + +Set the minimum C++ standard to 17 with `set(CMAKE_CXX_STANDARD 17)`. + +In a Windows install, add the required compiler flags listed below. + +```cmake +cmake_minimum_required(VERSION 3.15) + +project(MyDeviceSDKCppProject) + +# Minimum C++ standard +set(CMAKE_CXX_STANDARD 17) + +# In a Windows install, set these compiler flags: +if(MSVC) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zc:preprocessor /bigobj") +endif() + +# Include the FetchContent module so you can download the C++ SDK +Include(FetchContent) + +# Declare the version of the C++ SDK you want to download +FetchContent_Declare( + cpprealm + GIT_REPOSITORY https://github.com/realm/realm-cpp.git + GIT_TAG v1.0.0 +) + +# The MakeAvailable command ensures the named dependencies have been populated +FetchContent_MakeAvailable(cpprealm) + +# Create an executable target called myApp with the source file main.cpp +add_executable(myApp main.cpp) + +target_link_libraries(myApp PRIVATE cpprealm) +``` + +Run CMake in a gitignored directory, such as `build`, to generate the build +configurations that you can then use to compile your app: + +```bash +# build/ is in .gitignore +mkdir build +cd build +cmake .. # Create Makefile by reading the CMakeLists.txt in the parent directory (../) +make # Actually build the app +``` + +You can use CMake to generate more than simple Makefiles by using the `-G` +flag. See the [CMake documentation](https://cmake.org/documentation/) for more +information. + +## Usage +### Include the Header +Make the C++ SDK available in your code by including the +`cpprealm/sdk.hpp` header in the translation unit where you want to use it: + +```cpp +#include + +``` + +## Build an Android App +The C++ SDK supports building Android apps. To build an Android +app: + +- Add `` to your `AndroidManifest.xml` +- Add the subdirectory of the C++ SDK to your native library's `CMakeLists.txt` +and link it as a target library: `set(CMAKE_CXX_STANDARD 17) +add_subdirectory("realm-cpp") +... +target_link_libraries( + # Specifies the target library. + myapplication + # make sure to link the C++ SDK. + cpprealm +)` +- Ensure that the git submodules are initialized inside of the `realm-cpp` folder before building. +- When instantiating the database or the SDK App, you must pass the `filesDir.path` as the `path` +parameter in the respective constructor or database open template. + +For an example of how to use the C++ SDK in an Android app, refer to +the [Android RealmExample App](https://github.com/realm/realm-cpp/tree/main/examples/Android) +in the `realm-cpp` GitHub repository. + +Specifically, refer to the `MainActivity.kt` & `native-lib.cpp` files +in the Android example app for code examples. diff --git a/docs/guides/logging.md b/docs/guides/logging.md new file mode 100644 index 00000000..7e03c6e6 --- /dev/null +++ b/docs/guides/logging.md @@ -0,0 +1,42 @@ +# Logging - C++ SDK +You can set or change your app's log level to develop or debug your +application. You might want to change the log level to log different +amounts of data depending on the app's environment. + +## Set the Realm Log Level +You can set the level of detail reported by the Realm C++ SDK. Pass a +realm::logger::level +to the `set_default_level_threshold()` member function: + +```cpp +auto logLevel = realm::logger::level::info; +realm::set_default_level_threshold(logLevel); + +``` + +## Customize the Logging Function +To set a custom logger function, create a +realm::logger +and override the virtual `do_log()` member function: + +```cpp +struct MyCustomLogger : realm::logger { + // This could be called from any thread, so may not output visibly to the + // console. Handle output in a queue or other cross-thread context if needed. + void do_log(realm::logger::level level, const std::string &msg) override { + std::cout << "Realm log entry: " << msg << std::endl; + } +}; + +``` + +Then, initialize an instance of the logger and set it as the default logger +for your realm: + +```cpp +auto config = realm::db_config(); +auto thisRealm = realm::db(config); +auto myLogger = std::make_shared(); +realm::set_default_logger(myLogger); + +``` diff --git a/docs/guides/model-data.md b/docs/guides/model-data.md new file mode 100644 index 00000000..fe1b47c6 --- /dev/null +++ b/docs/guides/model-data.md @@ -0,0 +1,6 @@ +# Model Data - C++ SDK +Use the following pages to learn and reference Realm data modeling concepts and APIs: + +- [Object Types and Schemas](model-data/object-models.md) +- [Supported Data Types](model-data/supported-types.md) +- [Relationships](model-data/relationships.md) diff --git a/docs/guides/model-data/object-models.md b/docs/guides/model-data/object-models.md new file mode 100644 index 00000000..2bff527a --- /dev/null +++ b/docs/guides/model-data/object-models.md @@ -0,0 +1,198 @@ +# Object Models - C++ SDK +Realm SDK applications model data as objects composed of +field-value pairs that each contain one or more supported data types. + +## Object Types and Schemas +Every database object has an *object type* that refers to the object's +class. Objects of the same type share an object schema that defines the properties and relationships of those +objects. + +### Database Schema +A **database schema** is a list of valid object schemas that the database may +contain. Every database object must conform to an object type that's included +in its database's schema. + +When opening a database, you must specify which models are available by +passing the models to the template you use to open the database. Those +models must have schemas, and this list of schemas becomes the database schema. + +If the database already contains data when you open it, the SDK +validates each object to ensure that an object schema was provided for +its type and that it meets all of the constraints specified in the schema. + +For more information about how to open the database, refer to +Configure & Open a Realm - C++ SDK + +### Object Model +Your object model is the core structure that gives the database +information about how to interpret and store the objects in your app. +The C++ SDK object model is a regular C++ class or a struct that contains +a collection of properties. The properties that you want to persist must +use supported data types. Properties +are also the mechanism for establishing relationships between object types. + +When you define your C++ class or struct, you must also provide an +object schema. The schema is a C++ macro that gives the SDK information +about which properties to persist, and what type of database object it +is. + +You must define your SDK object model within the `realm` namespace. + +### Object Schema +A C++ SDK **object schema** maps properties for a specific object type. +The SDK schemas are macros that give the SDK the information it needs to +store and retrieve the objects. A schema must accompany every object model +you want to persist, and it may be one of: + +- `REALM_SCHEMA` +- `REALM_EMBEDDED_SCHEMA` +- `REALM_ASYMMETRIC_SCHEMA` + +You must define the schema and your object model within the `realm` namespace. + +## Define a New Object Type +In the C++ SDK, you can define your models as regular C++ structs or classes. +Provide an Object Schema with the object type name and +the names of any properties that you want to persist to the database. When you +add the object to the database, the SDK ignores any properties that you omit +from the schema. + +You must declare your object and the schema within the `realm` namespace. +You must then use the `realm` namespace when you initialize and perform CRUD +operations with the object. + +```cpp +namespace realm { +struct Dog { + std::string name; + int64_t age; +}; +REALM_SCHEMA(Dog, name, age) + +struct Person { + realm::primary_key _id; + std::string name; + int64_t age; + + // Create relationships by pointing an Object field to another struct or class + Dog *dog; +}; +REALM_SCHEMA(Person, _id, name, age, dog) +} // namespace realm + +``` + +> Note: +> Class names are limited to a maximum of 57 UTF-8 characters. +> + +### Specify a Primary Key +You can designate a property as the **primary key** of your object. + +Primary keys allow you to efficiently find, update, and upsert objects. + +Primary keys are subject to the following limitations: + +- You can define only one primary key per object model. +- Primary key values must be unique across all instances of an object +in the database. The C++ SDK throws an error if you try to +insert a duplicate primary key value. +- Primary key values are immutable. To change the primary key value of +an object, you must delete the original object and insert a new object +with a different primary key value. +- Embedded objects cannot define a primary key. + +The C++ SDK supports primary keys of the following types, and their +optional variants: + +- `int64_t` +- `realm::object_id` +- `realm::uuid` +- `std::string` + +Additionally, a required `realm::enum` property can be a primary key, but +`realm::enum` cannot be optional if it is used as a primary key. + +Set a property as a primary key with the `primary_key` template: + +```cpp +struct Person { + realm::primary_key _id; + std::string name; + int64_t age; + + // Create relationships by pointing an Object field to another struct or class + Dog *dog; +}; +REALM_SCHEMA(Person, _id, name, age, dog) + +``` + +### Ignore a Property +Your model may include properties that the database does not store. + +The database ignores any properties not included in the Object Schema. + +```cpp +namespace realm { +struct Employee { + realm::primary_key _id; + std::string firstName; + std::string lastName; + + // You can use this property as you would any other member + // Omitting it from the schema means the SDK ignores it + std::string jobTitle_notPersisted; +}; +// The REALM_SCHEMA omits the `jobTitle_notPersisted` property +// The SDK does not store and cannot retrieve a value for this property +REALM_SCHEMA(Employee, _id, firstName, lastName) +} // namespace realm + +``` + +### Define an Embedded Object +An **embedded object** is a special type of object that models complex +data about a specific object. Embedded objects are similar to +relationships, but they provide additional constraints and +map more naturally to the denormalized document model + +The C++ SDK enforces unique ownership constraints that treat each embedded +object as nested data inside of a single, specific parent object. An +embedded object inherits the lifecycle of its parent object and cannot +exist as an independent database object. The SDK automatically deletes +embedded objects if their parent object is deleted or when overwritten +by a new embedded object instance. + +You can declare an object as an embedded object +that does not have a lifecycle independent of the object in which it +is embedded. This differs from a to-one +or to-many relationship, in which the +related objects have independent lifecycles. + +Provide a `REALM_EMBEDDED_SCHEMA` with the struct or class name and the +names of any properties that you want the database to persist. + +Define a property as an embedded object on the parent object by setting +a pointer to the embedded object's type. + +```cpp +namespace realm { +struct ContactDetails { + // Because ContactDetails is an embedded object, it cannot have its own _id + // It does not have a lifecycle outside of the top-level object + std::string emailAddress; + std::string phoneNumber; +}; +REALM_EMBEDDED_SCHEMA(ContactDetails, emailAddress, phoneNumber) + +struct Business { + realm::object_id _id; + std::string name; + ContactDetails *contactDetails; +}; +REALM_SCHEMA(Business, _id, name, contactDetails) +} // namespace realm + +``` + diff --git a/docs/guides/model-data/relationships.md b/docs/guides/model-data/relationships.md new file mode 100644 index 00000000..92bab0c7 --- /dev/null +++ b/docs/guides/model-data/relationships.md @@ -0,0 +1,124 @@ +# Relationships - C++ SDK +Realm doesn't use bridge tables or explicit joins to define +relationships as you would in a relational database. Realm +handles relationships through embedded objects or reference properties to +other Realm objects. You read from and write to these +properties directly. This makes querying relationships as performant as +querying against any other property. + +## Relationship Types +Realm supports **to-one**, **to-many**, and **inverse** +relationships. Realm also provides a special type of object, called an +embedded object, that is conceptually +similar to a relationship but provides additional constraints. + +### To-One Relationship +A **to-one** relationship means that an object relates to one other object. +You define a to-one relationship for an object type in its object +model. Specify a property where the type is the related Realm +object type. For example, a dog might have a to-one relationship with +a favorite toy. + +### To-Many Relationship +A **to-many** relationship means that an object relates to more than one +other object. In Realm, a to-many relationship is a list of +references to other objects. For example, a person might have many dogs. + +You can represent a to-many relationship between two Realm +types as a list, map, or a set. Lists, maps, and sets are mutable: within +a write transaction, you can add and remove elements to and from these +collection types. Lists, maps, and sets are not associated with a query +and are declared as a property of the object model. + +### Inverse Relationship +Relationship definitions in Realm are unidirectional. An +**inverse relationship** links an object back to an object that refers +to it. + +An inverse relationship property is an automatic backlink relationship. +Realm automatically updates implicit relationships whenever an object is +added or removed in a corresponding to-many list or to-one relationship +property. You cannot manually set the value of an inverse relationship +property. + +## Declare Relationship Properties + +### Define a To-One Relationship +A **to-one** relationship maps one property to a single instance of +another object type. For example, you can model a dog having at most one +favorite toy as a to-one relationship. + +Setting a relationship field to null removes the connection between objects. +Realm does not delete the referenced object, though, unless it is +an embedded object. + +> Important: +> When you declare a to-one relationship in your object model, it must +be an optional property. If you try to make a to-one relationship +required, Realm throws an exception at runtime. +> + +```cpp +struct FavoriteToy { + realm::primary_key _id; + std::string name; +}; +REALM_SCHEMA(FavoriteToy, _id, name) + +struct Dog { + realm::primary_key _id; + std::string name; + int64_t age; + + // Define a relationship as a link to another SDK object + FavoriteToy* favoriteToy; +}; +REALM_SCHEMA(Dog, _id, name, age, favoriteToy) + +``` + +### Define a To-Many Relationship +A **to-many** relationship maps one property to zero or more instances +of another object type. For example, you can model a company having any +number of employees as a to-many relationship. + +```cpp +struct Company { + int64_t _id; + std::string name; + // To-many relationships are a list, represented here as a + // vector container whose value type is the SDK object + // type that the list field links to. + std::vector employees; +}; +REALM_SCHEMA(Company, _id, name, employees) + +``` + +### Define an Inverse Relationship +To define an inverse relationship, use `linking_objects` in your object +model. The `linking_objects` definition specifies the object type and +property name of the relationship that it inverts. + +In this example, we define a `Person` having a to-one relationship with +a `Dog`. The `Dog` has an inverse relationship to any `Person` +objects through its `owners` property. + +```cpp +struct Dog; +struct Person { + realm::primary_key _id; + std::string name; + int64_t age = 0; + Dog* dog; +}; +REALM_SCHEMA(Person, _id, name, age, dog) +struct Dog { + realm::primary_key _id; + std::string name; + int64_t age = 0; + linking_objects<&Person::dog> owners; +}; +REALM_SCHEMA(Dog, _id, name, age, owners) + +``` diff --git a/docs/guides/model-data/supported-types.md b/docs/guides/model-data/supported-types.md new file mode 100644 index 00000000..f2ae5c00 --- /dev/null +++ b/docs/guides/model-data/supported-types.md @@ -0,0 +1,160 @@ +# Supported Types - C++ SDK +The Realm C++ SDK currently supports these property types. + +Optionals use the class template +[std::optional](https://en.cppreference.com/w/cpp/utility/optional). + +## Property Cheat Sheet +You can use the following types to define your object model +properties. + +|Type|Required|Optional| +| --- | --- | --- | +|Bool|`bool boolName;`|`std::optional optBoolName;`| +|Int64|`int64_t intName;`|`std::optional optIntName;`| +|Double|`double doubleName;`|`std::optional optDoubleName;`| +|String|`std::string stringName;`|`std::optional optStringName;`| +|Enum|`Enum enumName;`|`std::optional optEnumName;`| +|Binary Data|`std::vector binaryDataName;`|`std::optional> optBinaryDataName;`| +|Date|`std::chrono::time_point dateName;`|`std::optional> optDateName;`| +|Decimal128|`realm::decimal128 decimal128Name;`|`std::optional optDecimal128Name;`| +|UUID|`realm::uuid uuidName;`|`std::optional optUuidName;`| +|Object ID|`realm::object_id objectIdName;`|`std::optional optObjectIdName;`| +|Mixed Data Type|`realm::mixed mixedName;`|N/A| +|Map|`std::map mapName;`|N/A| +|List|`std::vector listTypeName;`|N/A| +|Set|`std::set setTypeName;`|N/A| +|User-defined Object|N/A|`MyClass* opt_obj_name;`| +|User-defined Embedded Object|N/A|`MyEmbeddedClass* opt_embedded_object_name;`| + +### Supported Type Implementation Details +Some of the supported types above are aliases for: + +- `mixed`: A union-like object that can represent a value any of the +supported types. It is implemented using the class template +[std::variant](https://en.cppreference.com/w/cpp/utility/variant). +This implementation means that a `mixed` property holds a value of +one of its alternative types, or in the case of error - no value. +- For dates, use the [chrono library](https://en.cppreference.com/w/cpp/chrono) +to store a `time_point` relative to the `system_clock`: +`>` + +## Map/Dictionary +The +Map +is an associative array that contains key-value pairs with unique keys. + +You can declare a Map as a property of an object: + +```cpp +namespace realm { +struct Dog { + std::string name; + std::map favoriteParkByCity; +}; +REALM_SCHEMA(Dog, name, favoriteParkByCity) +} // namespace realm + +``` + +String is the only supported type for a map key, but map values can be: + +- Required versions of any of the SDK's supported data types +- Optional user-defined object links +- Optional embedded objects + +Realm disallows the use of `.` or `$` characters in map keys. +You can use percent encoding and decoding to store a map key that contains +one of these disallowed characters. + +```cpp +// Percent encode . or $ characters to use them in map keys +auto mapKey = "Monday.Morning"; +auto encodedMapKey = "Monday%2EMorning"; + +``` + +## Collection Types +Realm has several types to represent groups of objects, +which we call **collections**. A collection is an object that contains +zero or more instances of one Realm type. Realm collections are **homogenous**: +all objects in a collection are of the same type. + +You can filter and sort any collection using Realm's +query engine. Collections are +live, so they always reflect the current state +of the realm instance on the current thread. You can also +listen for changes in the collection by subscribing to collection +notifications. + +### Results +The C++ SDK Results +collection is a type representing objects retrieved from queries. A +`Results` collection represents the lazily-evaluated results of a +query operation. Results are immutable: you cannot add or remove elements +to or from the results collection. Results have an associated query that +determines their contents. + +For more information, refer to the Read documentation. + +### Set +The C++ SDK set collection represents a +to-many relationship containing +distinct values. A C++ SDK set supports the following types (and their +optional versions): + +- Binary Data +- Bool +- Double +- Date +- Int64 +- Mixed +- ObjectId +- Object +- String +- UUID + +Like the C++ [std::set](https://en.cppreference.com/w/cpp/container/set), +the C++ SDK set is a generic type that is parameterized on the type it stores. + +You can only call set mutation methods during a write transaction. You can +register a change listener on a mutable set. + +### Collections as Properties +The C++ SDK also offers several collection types you can use as properties +in your data model: + +1. List, a type representing +to-many relationships in models. +2. Set, +a type representing to-many relationships +in models where values are unique. +3. linking_objects, a type +representing inverse relationships in models. +4. Map, +a type representing an associative array of key-value pairs with unique keys. + +### Collections are Live +Like live objects, Realm collections +are usually **live**: + +- Live results collections always reflect the current results of the associated query. +- Live lists always reflect the current state of the relationship on the realm instance. + +The one case when a collection is **not** live is when the collection is +unmanaged. For example, a List property of a Realm object that has not +been added to a realm yet or that has been moved from a realm is not live. + +Combined with collection notifications, live collections enable +clean, reactive code. For example, suppose your view displays the +results of a query. You can keep a reference to the results collection +in your view code, then read the results collection as needed without +having to refresh it or validate that it is up-to-date. + +> Important: +> Since results update themselves automatically, do not +store the positional index of an object in the collection +or the count of objects in a collection. The stored index +or count value could be outdated by the time you use +it. +> diff --git a/docs/guides/quick-start.md b/docs/guides/quick-start.md new file mode 100644 index 00000000..f0ddc105 --- /dev/null +++ b/docs/guides/quick-start.md @@ -0,0 +1,117 @@ +# Quick Start - C++ SDK +This Quick Start demonstrates how to use Realm with the Realm C++ SDK. +Before you begin, ensure you have Installed the C++ SDK. + +## Import Realm +Make the Realm C++ SDK available in your code by including the +`cpprealm/sdk.hpp` header in the translation unit where you want to use it: + +```cpp +#include + +``` + +## Define Your Object Model +You can define your object model directly in code. + +```cpp +namespace realm { +struct Todo { + realm::primary_key _id{realm::object_id::generate()}; + std::string name; + std::string status; +}; +REALM_SCHEMA(Todo, _id, name, status); +} // namespace realm + +``` + +## Open a Realm +When you open a realm, you must specify a db_config. You can +optionally open a realm at a specific path. + +```cpp +auto config = realm::db_config(); +auto realm = realm::db(std::move(config)); + +``` + +For more information, see: Configure and Open a Realm. + +## Create, Read, Update, and Delete Objects +Once you have opened a realm, you can modify it and its objects in a write transaction +block. + +To instantiate a new Todo object and add it to the realm in a write block: + +```cpp +auto todo = realm::Todo{.name = "Create my first todo item", + .status = "In Progress"}; + +realm.write([&] { realm.add(std::move(todo)); }); + +``` + +You can retrieve a live results collection of +all todos in the realm: + +```cpp +auto todos = realm.objects(); + +``` + +You can also filter that collection using where: + +```cpp +auto todosInProgress = todos.where( + [](auto const& todo) { return todo.status == "In Progress"; }); + +``` + +To modify a todo, update its properties in a write transaction block: + +```cpp +auto todoToUpdate = todosInProgress[0]; +realm.write([&] { todoToUpdate.status = "Complete"; }); + +``` + +Finally, you can delete a todo: + +```cpp +realm.write([&] { realm.remove(specificTodo); }); + +``` + +## Watch for Changes +You can watch an object for changes with the `observe` method. + +```cpp +auto token = specificTodo.observe([&](auto&& change) { + try { + if (change.error) { + rethrow_exception(change.error); + } + if (change.is_deleted) { + std::cout << "The object was deleted.\n"; + } else { + for (auto& propertyChange : change.property_changes) { + std::cout << "The object's " << propertyChange.name + << " property has changed.\n"; + } + } + } catch (std::exception const& e) { + std::cerr << "Error: " << e.what() << "\n"; + } +}); + +``` + +## Close a Realm +To close a realm and release all underlying resources, call `db::close()`. +Closing the database invalidates any remaining objects. + +```cpp +realm.close(); + +``` diff --git a/docs/guides/react-to-changes.md b/docs/guides/react-to-changes.md new file mode 100644 index 00000000..9c972d9f --- /dev/null +++ b/docs/guides/react-to-changes.md @@ -0,0 +1,262 @@ +# React to Changes - C++ SDK +All Realm objects are **live objects**, which means they +automatically update whenever they're modified. Realm emits a +notification event whenever any property changes. You can register a +notification handler to listen for these notification events, and update +your UI with the latest data. + +## Register an Object Change Listener +You can register a notification handler on a specific object +within a realm. Realm notifies your handler: + +- When the object is deleted. +- When any of the object's properties change. + +```cpp +auto token = object.observe([&](auto&& change) { ... } +``` + +The handler receives an object_change +object that contains information about the changes, such as whether the +object was deleted. It may include a list +of PropertyChange +objects that contain information about what fields changed, the new values +of those fields (except on List properties), and potentially the old values +of the fields. + +```cpp +if (change.error) { + rethrow_exception(change.error); +} +if (change.is_deleted) { + std::cout << "The object was deleted.\n"; +} else { + for (auto& propertyChange : change.property_changes) { + std::cout << "The object's " << propertyChange.name + << " property has changed.\n"; + auto newPropertyValue = + std::get(*propertyChange.new_value); + std::cout << "The new value is " << newPropertyValue << "\n"; + } +} + +``` + +When you make changes, refresh() the +realm to emit a notification. + +```cpp +auto config = realm::db_config(); +auto realm = realm::db(std::move(config)); + +// Create an object and move it into the database. +auto dog = realm::Dog{.name = "Max"}; +realm.write([&] { realm.add(std::move(dog)); }); + +auto dogs = realm.objects(); +auto specificDog = dogs[0]; +// Set up the listener & observe object notifications. +auto token = specificDog.observe([&](auto&& change) { + try { + if (change.error) { + rethrow_exception(change.error); + } + if (change.is_deleted) { + std::cout << "The object was deleted.\n"; + } else { + for (auto& propertyChange : change.property_changes) { + std::cout << "The object's " << propertyChange.name + << " property has changed.\n"; + auto newPropertyValue = + std::get(*propertyChange.new_value); + std::cout << "The new value is " << newPropertyValue << "\n"; + } + } + } catch (std::exception const& e) { + std::cerr << "Error: " << e.what() << "\n"; + } +}); + +// Update the dog's name to see the effect. +realm.write([&] { specificDog.name = "Wolfie"; }); + +// Deleting the object triggers a delete notification. +realm.write([&] { realm.remove(specificDog); }); + +// Refresh the database after the change to trigger the notification. +realm.refresh(); + +// Unregister the token when done observing. +token.unregister(); + +``` + +## Register a Collection Change Listener +You can register a notification handler on a collection. A collection is +a list, map, or set property that can contain any supported data type, including primitives or other objects. + +Realm notifies your handler whenever a write transaction +removes, adds, or changes objects in the collection. + +Notifications describe the changes since the prior notification with +three lists of indices: the indices of the objects that were removed, +inserted, and modified. + +> Important: +> In collection notification handlers, always apply changes +in the following order: deletions, insertions, then +modifications. Handling insertions before deletions may +result in unexpected behavior. +> + +Collection notifications provide a collection_change +struct that reports the index of the objects that are removed, added, or +modified. It can also notify you if the collection was deleted. + +In this example, the `Person` object has a list of `Dog` objects as a property: + +```cpp +struct Person { + std::string name; + std::vector dogs; +}; +REALM_SCHEMA(Person, name, dogs) + +``` + +Removing a dog, adding a new dog, or modifying a dog triggers the notification +handler: + +```cpp +// Set up the listener & observe a collection. +auto token = person.dogs.observe([&](auto&& changes) { + if (changes.collection_root_was_deleted) { + std::cout << "The collection was deleted.\n"; + } else { + // Handle deletions, then insertions, then modifications. + for (auto& collectionChange : changes.deletions) { + std::cout << "The object at index " << std::to_string(collectionChange) + << " was removed\n"; + } + for (auto& collectionChange : changes.insertions) { + std::cout << "The object at index " << std::to_string(collectionChange) + << " was inserted\n"; + } + for (auto& collectionChange : changes.modifications) { + std::cout << "The object at index " << std::to_string(collectionChange) + << " was modified\n"; + } + } +}); + +// Remove an object from the collection, and then add an object to see +// deletions and insertions. +realm.write([&] { + person.dogs.clear(); + person.dogs.push_back(&dog2); +}); + +// Modify an object to see a modification. +realm.write([&] { dog2.age = 2; }); + +// Refresh the realm after the change to trigger the notification. +realm.refresh(); + +// Unregister the token when done observing. +token.unregister(); + +``` + +## Register a Results Collection Change Listener +You can register a notification handler on a results collection. + +Realm notifies your handler whenever a write transaction +removes, adds, or changes objects in the collection. + +Notifications describe the changes since the prior notification with +three lists of indices: the indices of the objects that were deleted, +inserted, and modified. + +> Important: +> In collection notification handlers, always apply changes +in the following order: deletions, insertions, then +modifications. Handling insertions before deletions may +result in unexpected behavior. +> + +Results collection notifications provide a results_change struct that +reports the index of the objects that are deleted, added, or modified. +It can also notify you if the collection was deleted. + +```cpp +// Get a results collection to observe +auto dogs = realm.objects(); +// Set up the listener & observe results notifications. +auto token = dogs.observe([&](auto&& changes) { + try { + if (changes.collection_root_was_deleted) { + std::cout << "The collection was deleted.\n"; + } else { + // Handle deletions, then insertions, then modifications. + for (auto& resultsChange : changes.deletions) { + std::cout << "The object at index " << std::to_string(resultsChange) + << " was deleted\n"; + } + for (auto& resultsChange : changes.insertions) { + std::cout << "The object at index " << std::to_string(resultsChange) + << " was inserted\n"; + } + for (auto& resultsChange : changes.modifications) { + std::cout << "The object at index " << std::to_string(resultsChange) + << " was modified\n"; + } + } + } catch (std::exception const& e) { + std::cerr << "Error: " << e.what() << "\n"; + } +}); + +// Delete and then add an object to see deletions and insertions. +realm.write([&] { + realm.remove(firstDog); + realm.add(std::move(dog2)); +}); + +// Modify an object to see a modification. +realm.write([&] { dog2.age = 2; }); + +// Refresh the database after the change to trigger the notification. +realm.refresh(); + +// Unregister the token when done observing. +token.unregister(); + +``` + +## Stop Watching for Changes +Observation stops when the token returned by an `observe` call becomes +invalid. You can explicitly invalidate a token by calling its +`unregister()` member function. + +```cpp +// Unregister the token when done observing. +token.unregister(); + +``` + +> Important: +> Notifications stop when the token's destructor is called. For example, +if the token is in a local variable that goes out of scope. You can +use `std::move` to transfer the token to a variable in a different +scope. +> + +## Change Notification Limits +Changes in nested documents deeper than four levels down do not trigger +change notifications. + +If you have a data structure where you need to listen for changes five +levels down or deeper, workarounds include: + +- Refactor the schema to reduce nesting. +- Add something like "push-to-refresh" to enable users to manually refresh data. diff --git a/docs/guides/realm-files.md b/docs/guides/realm-files.md new file mode 100644 index 00000000..7cb36cbf --- /dev/null +++ b/docs/guides/realm-files.md @@ -0,0 +1,6 @@ +# Work with Realm Files - C++ SDK +Use the following pages to learn and reference Realm database concepts and APIs: + +- [Configure & Open a Realm](realm-files/configure-and-open-a-realm.md) +- [Reduce Realm File Size](realm-files/compact-realm.md) +- [Encrypt a Realm](realm-files/encrypt-a-realm.md) diff --git a/docs/guides/realm-files/compact-realm.md b/docs/guides/realm-files/compact-realm.md new file mode 100644 index 00000000..94be400f --- /dev/null +++ b/docs/guides/realm-files/compact-realm.md @@ -0,0 +1,82 @@ +# Reduce Realm File Size - C++ SDK +Over time, the storage space used by Realm might become fragmented +and take up more space than necessary. To rearrange the internal storage and +potentially reduce the file size, the realm file needs to be compacted. + +Realm's default behavior is to automatically compact a realm file +to prevent it from growing too large. You can use manual compaction strategies when +automatic compaction is not sufficient for your use case. + +## Automatic Compaction +The SDK automatically compacts Realm files in the background by continuously reallocating data +within the file and removing unused file space. Automatic compaction is sufficient for minimizing the Realm file size +for most applications. + +Automatic compaction begins when the size of unused space in the file is more than twice the size of user +data in the file. Automatic compaction only takes place when +the file is not being accessed. + +## Manual Compaction Options +Manual compaction can be used for applications that require stricter +management of file size. + +Realm manual compaction works by: + +1. Reading the entire contents of the realm file +2. Writing the contents to a new file at a different location +3. Replacing the original file + +If the file contains a lot of data, this can be an expensive operation. + +Use the +should_compact_on_launch() +method on the database configuration to attempt to compact the database. +Specify conditions to execute this method, such as: + +- The size of the file on disk +- How much free space the file contains + +The following example shows setting the conditions to compact a realm if the +file is above 100 MB and 50% or less of the space in the realm file is used. + +```cpp +// Create a database configuration. +auto config = realm::db_config(); + +config.should_compact_on_launch([&](uint64_t totalBytes, uint64_t usedBytes) { + // totalBytes refers to the size of the file on disk in bytes (data + free + // space). usedBytes refers to the number of bytes used by data in the file + // Compact if the file is over 100MB in size and less than 50% 'used' + auto oneHundredMB = 100 * 1024 * 1024; + return (totalBytes > oneHundredMB) && (usedBytes / totalBytes) < 0.5; +}); + +// The database is compacted on the first open if the configuration block +// conditions were met. +auto realm = realm::db(config); + +``` + +### Tips for Manually Compacting a Realm +Manually compacting a realm can be a resource-intensive operation. +Your application should not compact every time you open +a realm. Instead, try to optimize compacting so your application does +it just often enough to prevent the file size from growing too large. +If your application runs in a resource-constrained environment, +you may want to compact when you reach a certain file size or when the +file size negatively impacts performance. + +These recommendations can help you start optimizing compaction for your +application: + +- Set the max file size to a multiple of your average realm state +size. If your average realm state size is 10MB, you might set the max +file size to 20MB or 40MB, depending on expected usage and device +constraints. +- As a starting point, compact realms when more than 50% of the realm file +size is no longer in use. Divide the currently used bytes by the total +file size to determine the percentage of space that is currently used. +Then, check for that to be less than 50%. This means that greater than +50% of your realm file size is unused space, and it is a good time to +compact. After experimentation, you may find a different percentage +works best for your application. diff --git a/docs/guides/realm-files/configure-and-open-a-realm.md b/docs/guides/realm-files/configure-and-open-a-realm.md new file mode 100644 index 00000000..7468568e --- /dev/null +++ b/docs/guides/realm-files/configure-and-open-a-realm.md @@ -0,0 +1,129 @@ +# Configure & Open a Realm - C++ SDK +A **realm** is the core data structure used to organize data in +Realm. A realm is a collection of the objects that you use +in your application, called Realm objects, as well as additional metadata +that describe the objects. + +When opening a realm, you must specify a db_config. +The `db_config` may contain information such as: + +- An optional path where the realm is stored on device +- An optional list of models that the realm should manage +- An optional scheduler if you need to customize the run loop + +You can use the default `db_config` constructor if you do not need +to specify a realm file path or other +configuration details. + +## Realm Files +Realm stores a binary encoded version of every object and type in a +realm in a single `.realm` file. The file is located at a specific +path that you can define when you open the realm. +You can open, view, and edit the contents of these files with +. + +### Auxiliary Files +The SDK creates additional files for each realm: + +- **realm files**, suffixed with "realm", e.g. default.realm: +contain object data. +- **lock files**, suffixed with "lock", e.g. default.realm.lock: +keep track of which versions of data in a realm are +actively in use. This prevents realm from reclaiming storage space +that is still used by a client application. +- **note files**, suffixed with "note", e.g. default.realm.note: +enable inter-thread and inter-process notifications. +- **management files**, suffixed with "management", e.g. default.realm.management: +internal state management. + +### Open the Default Realm +Opening a realm without specifying an optional path opens the default realm +in the current directory. + +When opening a realm, the C++ SDK can automatically infer which +models are available in the project. + +```cpp +auto config = realm::db_config(); +auto realm = realm::db(std::move(config)); + +``` + +> Tip: +> When building an Android app that uses the Realm C++ SDK, +you must pass the `filesDir.path` to the `path` parameter in the +db_config +constructor. For more information, refer to: Build an Android App. +> + +### Open a Realm at a File Path +You can use `set_path()` to specify a path for the `db_config` +to use when opening the realm. + +```cpp +auto relative_realm_path_directory = "custom_path_directory/"; +std::filesystem::create_directories(relative_realm_path_directory); +// Construct a path +std::filesystem::path path = + std::filesystem::current_path().append(relative_realm_path_directory); +// Add a name for the database file +path = path.append("employee_objects"); +// Add the .realm extension +path = path.replace_extension("realm"); +// Set the path on the config, and open the database at the path +auto config = realm::db_config(); +config.set_path(path); +auto realmInstance = realm::db(std::move(config)); + +``` + +> Tip: +> When building an Android app that uses the Realm C++ SDK, +you must pass the `filesDir.path` as the `path` parameter in the +db_config +constructor. For more information, refer to: Build an Android App. +> + +## Provide a Subset of Models to a Realm +> Tip: +> Some applications have tight constraints on their memory footprints. +To optimize your realm memory usage for low-memory environments, open the +realm with a subset of models. +> + +By default, the C++ SDK automatically adds all object types that have a +Object Schema in your executable to your +Database Schema. + +However, if you want the realm to manage only a subset of models, you +can specify those models by passing them into the template parameter list +of the `realm::open()` function. + +``` +auto config = realm::db_config(); +auto realm = realm::open(std::move(config)); + +``` + +## Close a Realm +To avoid memory leaks, close the database when you're done using it. Closing +the database invalidates any remaining objects. Close the database with +`db::close()`. + +```cpp +// Create a database configuration. +auto config = realm::db_config(); +auto realm = realm::db(config); + +// Use the database... + +// ... later, close it. +realm.close(); + +// You can confirm that the database is closed if needed. +CHECK(realm.is_closed()); + +// Objects from the database become invalidated when you close the database. +CHECK(specificDog.is_invalidated()); + +``` diff --git a/docs/guides/realm-files/encrypt-a-realm.md b/docs/guides/realm-files/encrypt-a-realm.md new file mode 100644 index 00000000..a2c78569 --- /dev/null +++ b/docs/guides/realm-files/encrypt-a-realm.md @@ -0,0 +1,60 @@ +# Encrypt a Realm - C++ SDK +You can encrypt the realm file on disk with AES-256 + +SHA-2 by supplying a 64-byte encryption key when opening a +realm. + +Realm transparently encrypts and decrypts data with standard +[AES-256 encryption](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) using the +first 256 bits of the given 512-bit encryption key. Realm +uses the other 256 bits of the 512-bit encryption key to validate +integrity using a [hash-based message authentication code +(HMAC)](https://en.wikipedia.org/wiki/HMAC). + +> Warning: +> Do not use cryptographically-weak hashes for realm encryption keys. +For optimal security, we recommend generating random rather than derived +encryption keys. +> + +Encrypt a realm by calling the `set_encryption_key()` function on +your db_config: + +```cpp +// Check if we already have a key stored in the platform's secure storage. +// If we don't, generate a new one. +// Use your preferred method to generate a key. This example key is +// NOT representative of a secure encryption key. It only exists to +// illustrate the form your key might take. +std::array exampleKey = { + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, + 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, + 0, 0, 0, 0, 6, 6, 0, 0, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, 0, 0}; + +// Store the key securely to be used next time we want to open the database. +// We don't illustrate this here because it varies depending on the platform. + +// Create a database configuration. +auto config = realm::db_config(); +// Set the encryption key in your config. +config.set_encryption_key(exampleKey); + +// Open or create a database with the config containing the encryption key. +auto realm = realm::db(config); + +``` + +> Tip: +> The C++ SDK does not yet support encrypting a realm that already +exists on device. You must encrypt the realm the first time you open it. +> + +## Store & Reuse Keys +You **must** pass the same encryption key every time you open the encrypted realm. +If you don't provide a key or specify the wrong key for an encrypted +realm, the Realm SDK throws an error. + +Apps should store the encryption key securely on the device so that other +apps cannot read the key. + +## Performance Impact +Reads and writes on encrypted realms can be up to 10% slower than unencrypted realms. diff --git a/examples/Qt/coffee/README.md b/examples/Qt/coffee/README.md index 6c79764c..4fc00471 100644 --- a/examples/Qt/coffee/README.md +++ b/examples/Qt/coffee/README.md @@ -1,4 +1,10 @@ # Coffee Machine - Operated and powered by Atlas Device Sync +> [!WARNING] +> We announced the deprecation of Atlas Device Sync + Realm SDKs in September 2024. For more information please see: +> - [SDK Deprecation](https://www.mongodb.com/docs/atlas/device-sdks/deprecation) +> - [Device Sync Deprecation](https://www.mongodb.com/docs/atlas/app-services/sync/device-sync-deprecation) +> + The example extends the [Qt provided Coffee Machine](https://doc.qt.io/qt-6/qtdoc-demos-coffee-example.html) with [Atlas Device Sync](https://www.mongodb.com/docs/atlas/app-services/sync/) - essentially turning the coffee machine into a fleet of coffee machines that can be operated and controlled remotely by an operator. diff --git a/examples/SPM/README.md b/examples/SPM/README.md index e82fa0bd..e2a8f1ac 100644 --- a/examples/SPM/README.md +++ b/examples/SPM/README.md @@ -1,3 +1,9 @@ +> [!WARNING] +> We announced the deprecation of Atlas Device Sync + Realm SDKs in September 2024. For more information please see: +> - [SDK Deprecation](https://www.mongodb.com/docs/atlas/device-sdks/deprecation) +> - [Device Sync Deprecation](https://www.mongodb.com/docs/atlas/app-services/sync/device-sync-deprecation) +> + You will first need to create a [Atlas Device Sync App and turn development mode on for it.](https://www.mongodb.com/docs/atlas/app-services/sync/get-started/) Make sure to enable anonymous authentication. Then replace 'YOUR_APP_ID' in main.cpp with the one you just created. diff --git a/examples/cmake/README.md b/examples/cmake/README.md index 4e9f29d0..44ad1e8d 100644 --- a/examples/cmake/README.md +++ b/examples/cmake/README.md @@ -1,3 +1,9 @@ +> [!WARNING] +> We announced the deprecation of Atlas Device Sync + Realm SDKs in September 2024. For more information please see: +> - [SDK Deprecation](https://www.mongodb.com/docs/atlas/device-sdks/deprecation) +> - [Device Sync Deprecation](https://www.mongodb.com/docs/atlas/app-services/sync/device-sync-deprecation) +> + You will first need to create a [Atlas Device Sync App and turn development mode on for it.](https://www.mongodb.com/docs/atlas/app-services/sync/get-started/) Make sure to enable anonymous authentication.