docs/daprdocs/content/en/developing-applications/building-blocks/state-management/howto-get-save-state.md

592 lines
21 KiB
Markdown

---
type: docs
title: "How-To: Save and get state"
linkTitle: "How-To: Save & get state"
weight: 200
description: "Use key value pairs to persist a state"
---
## Introduction
State management is one of the most common needs of any application: new or legacy, monolith or microservice.
Dealing with different databases libraries, testing them, handling retries and faults can be time consuming and hard.
Dapr provides state management capabilities that include consistency and concurrency options.
In this guide we'll start of with the basics: Using the key/value state API to allow an application to save, get and delete state.
## Pre-requisites
- [Dapr CLI]({{< ref install-dapr-cli.md >}})
- Initialized [Dapr environment]({{< ref install-dapr-selfhost.md >}})
## Step 1: Setup a state store
A state store component represents a resource that Dapr uses to communicate with a database.
For the purpose of this guide we'll use a Redis state store, but any state store from the [supported list]({{< ref supported-state-stores >}}) will work.
{{< tabs "Self-Hosted (CLI)" Kubernetes>}}
{{% codetab %}}
When using `dapr init` in Standalone mode, the Dapr CLI automatically provisions a state store (Redis) and creates the relevant YAML in a `components` directory, which for Linux/MacOS is `$HOME/.dapr/components` and for Windows is `%USERPROFILE%\.dapr\components`
To optionally change the state store being used, replace the YAML file `statestore.yaml` under `/components` with the file of your choice.
{{% /codetab %}}
{{% codetab %}}
To deploy this into a Kubernetes cluster, fill in the `metadata` connection details of your [desired statestore component]({{< ref supported-state-stores >}}) in the yaml below, save as `statestore.yaml`, and run `kubectl apply -f statestore.yaml`.
```yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
namespace: default
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""
```
See the instructions [here]({{< ref "setup-state-store" >}}) on how to setup different state stores on Kubernetes.
{{% /codetab %}}
{{< /tabs >}}
## Step 2: Save and retrieve a single state
The following example shows how to a single key/value pair using the Dapr state building block.
{{% alert title="Note" color="warning" %}}
It is important to set an app-id, as the state keys are prefixed with this value. If you don't set it one is generated for you at runtime, and the next time you run the command a new one will be generated and you will no longer be able to access previously saved state.
{{% /alert %}}
{{< tabs "HTTP API (Bash)" "HTTP API (PowerShell)" "Python SDK" "PHP SDK">}}
{{% codetab %}}
Begin by launching a Dapr sidecar:
```bash
dapr run --app-id myapp --dapr-http-port 3500
```
Then in a separate terminal save a key/value pair into your statestore:
```bash
curl -X POST -H "Content-Type: application/json" -d '[{ "key": "key1", "value": "value1"}]' http://localhost:3500/v1.0/state/statestore
```
Now get the state you just saved:
```bash
curl http://localhost:3500/v1.0/state/statestore/key1
```
You can also restart your sidecar and try retrieving state again to see that state persists separate from the app.
{{% /codetab %}}
{{% codetab %}}
Begin by launching a Dapr sidecar:
```bash
dapr --app-id myapp --port 3500 run
```
Then in a separate terminal save a key/value pair into your statestore:
```powershell
Invoke-RestMethod -Method Post -ContentType 'application/json' -Body '[{"key": "key1", "value": "value1"}]' -Uri 'http://localhost:3500/v1.0/state/statestore'
```
Now get the state you just saved:
```powershell
Invoke-RestMethod -Uri 'http://localhost:3500/v1.0/state/statestore/key1'
```
You can also restart your sidecar and try retrieving state again to see that state persists separate from the app.
{{% /codetab %}}
{{% codetab %}}
Save the following to a file named `pythonState.py`:
```python
from dapr.clients import DaprClient
with DaprClient() as d:
d.save_state(store_name="statestore", key="myFirstKey", value="myFirstValue" )
print("State has been stored")
data = d.get_state(store_name="statestore", key="myFirstKey").data
print(f"Got value: {data}")
```
Once saved run the following command to launch a Dapr sidecar and run the Python application:
```bash
dapr --app-id myapp run python pythonState.py
```
You should get an output similar to the following, which will show both the Dapr and app logs:
```md
== DAPR == time="2021-01-06T21:34:33.7970377-08:00" level=info msg="starting Dapr Runtime -- version 0.11.3 -- commit a1a8e11" app_id=Braidbald-Boot scope=dapr.runtime type=log ver=0.11.3
== DAPR == time="2021-01-06T21:34:33.8040378-08:00" level=info msg="standalone mode configured" app_id=Braidbald-Boot scope=dapr.runtime type=log ver=0.11.3
== DAPR == time="2021-01-06T21:34:33.8040378-08:00" level=info msg="app id: Braidbald-Boot" app_id=Braidbald-Boot scope=dapr.runtime type=log ver=0.11.3
== DAPR == time="2021-01-06T21:34:33.9750400-08:00" level=info msg="component loaded. name: statestore, type: state.redis" app_id=Braidbald-Boot scope=dapr.runtime type=log ver=0.11.3
== DAPR == time="2021-01-06T21:34:33.9760387-08:00" level=info msg="API gRPC server is running on port 51656" app_id=Braidbald-Boot scope=dapr.runtime type=log ver=0.11.3
== DAPR == time="2021-01-06T21:34:33.9770372-08:00" level=info msg="dapr initialized. Status: Running. Init Elapsed 172.9994ms" app_id=Braidbald-Boot scope=dapr.
Checking if Dapr sidecar is listening on GRPC port 51656
Dapr sidecar is up and running.
Updating metadata for app command: python pythonState.py
You are up and running! Both Dapr and your app logs will appear here.
== APP == State has been stored
== APP == Got value: b'myFirstValue'
```
{{% /codetab %}}
{{% codetab %}}
Save the following in `state-example.php`:
```php
<?php
require_once __DIR__.'/vendor/autoload.php';
$app = \Dapr\App::create();
$app->run(function(\Dapr\State\StateManager $stateManager, \Psr\Log\LoggerInterface $logger) {
$stateManager->save_state(store_name: 'statestore', item: new \Dapr\State\StateItem(
key: 'myFirstKey',
value: 'myFirstValue'
));
$logger->alert('State has been stored');
$data = $stateManager->load_state(store_name: 'statestore', key: 'myFirstKey')->value;
$logger->alert("Got value: {data}", ['data' => $data]);
});
```
Once saved run the following command to launch a Dapr sidecar and run the PHP application:
```bash
dapr --app-id myapp run -- php state-example.php
```
You should get an output similar to the following, which will show both the Dapr and app logs:
```md
✅ You're up and running! Both Dapr and your app logs will appear here.
== APP == [2021-02-12T16:30:11.078777+01:00] APP.ALERT: State has been stored [] []
== APP == [2021-02-12T16:30:11.082620+01:00] APP.ALERT: Got value: myFirstValue {"data":"myFirstValue"} []
```
{{% /codetab %}}
{{< /tabs >}}
## Step 3: Delete state
The following example shows how to delete an item by using a key with the state management API:
{{< tabs "HTTP API (Bash)" "HTTP API (PowerShell)" "Python SDK" "PHP SDK">}}
{{% codetab %}}
With the same dapr instance running from above run:
```bash
curl -X DELETE 'http://localhost:3500/v1.0/state/statestore/key1'
```
Try getting state again and note that no value is returned.
{{% /codetab %}}
{{% codetab %}}
With the same dapr instance running from above run:
```powershell
Invoke-RestMethod -Method Delete -Uri 'http://localhost:3500/v1.0/state/statestore/key1'
```
Try getting state again and note that no value is returned.
{{% /codetab %}}
{{% codetab %}}
Update `pythonState.py` with:
```python
from dapr.clients import DaprClient
with DaprClient() as d:
d.save_state(store_name="statestore", key="key1", value="value1" )
print("State has been stored")
data = d.get_state(store_name="statestore", key="key1").data
print(f"Got value: {data}")
d.delete_state(store_name="statestore", key="key1")
data = d.get_state(store_name="statestore", key="key1").data
print(f"Got value after delete: {data}")
```
Now run your program with:
```bash
dapr --app-id myapp run python pythonState.py
```
You should see an output similar to the following:
```md
Starting Dapr with id Yakchocolate-Lord. HTTP Port: 59457. gRPC Port: 59458
== DAPR == time="2021-01-06T22:55:36.5570696-08:00" level=info msg="starting Dapr Runtime -- version 0.11.3 -- commit a1a8e11" app_id=Yakchocolate-Lord scope=dapr.runtime type=log ver=0.11.3
== DAPR == time="2021-01-06T22:55:36.5690367-08:00" level=info msg="standalone mode configured" app_id=Yakchocolate-Lord scope=dapr.runtime type=log ver=0.11.3
== DAPR == time="2021-01-06T22:55:36.7220140-08:00" level=info msg="component loaded. name: statestore, type: state.redis" app_id=Yakchocolate-Lord scope=dapr.runtime type=log ver=0.11.3
== DAPR == time="2021-01-06T22:55:36.7230148-08:00" level=info msg="API gRPC server is running on port 59458" app_id=Yakchocolate-Lord scope=dapr.runtime type=log ver=0.11.3
== DAPR == time="2021-01-06T22:55:36.7240207-08:00" level=info msg="dapr initialized. Status: Running. Init Elapsed 154.984ms" app_id=Yakchocolate-Lord scope=dapr.runtime type=log ver=0.11.3
Checking if Dapr sidecar is listening on GRPC port 59458
Dapr sidecar is up and running.
Updating metadata for app command: python pythonState.py
You're up and running! Both Dapr and your app logs will appear here.
== APP == State has been stored
== APP == Got value: b'value1'
== APP == Got value after delete: b''
```
{{% /codetab %}}
{{% codetab %}}
Update `state-example.php` with the following contents:
```php
<?php
require_once __DIR__.'/vendor/autoload.php';
$app = \Dapr\App::create();
$app->run(function(\Dapr\State\StateManager $stateManager, \Psr\Log\LoggerInterface $logger) {
$stateManager->save_state(store_name: 'statestore', item: new \Dapr\State\StateItem(
key: 'myFirstKey',
value: 'myFirstValue'
));
$logger->alert('State has been stored');
$data = $stateManager->load_state(store_name: 'statestore', key: 'myFirstKey')->value;
$logger->alert("Got value: {data}", ['data' => $data]);
$stateManager->delete_keys(store_name: 'statestore', keys: ['myFirstKey']);
$data = $stateManager->load_state(store_name: 'statestore', key: 'myFirstKey')->value;
$logger->alert("Got value after delete: {data}", ['data' => $data]);
});
```
Now run it with:
```bash
dapr --app-id myapp run -- php state-example.php
```
You should see something similar the following output:
```md
✅ You're up and running! Both Dapr and your app logs will appear here.
== APP == [2021-02-12T16:38:00.839201+01:00] APP.ALERT: State has been stored [] []
== APP == [2021-02-12T16:38:00.841997+01:00] APP.ALERT: Got value: myFirstValue {"data":"myFirstValue"} []
== APP == [2021-02-12T16:38:00.845721+01:00] APP.ALERT: Got value after delete: {"data":null} []
```
{{% /codetab %}}
{{< /tabs >}}
## Step 4: Save and retrieve multiple states
Dapr also allows you to save and retrieve multiple states in the same call.
{{< tabs "HTTP API (Bash)" "HTTP API (PowerShell)" "Python SDK" "PHP SDK">}}
{{% codetab %}}
With the same dapr instance running from above save two key/value pairs into your statestore:
```bash
curl -X POST -H "Content-Type: application/json" -d '[{ "key": "key1", "value": "value1"}, { "key": "key2", "value": "value2"}]' http://localhost:3500/v1.0/state/statestore
```
Now get the states you just saved:
```bash
curl -X POST -H "Content-Type: application/json" -d '{"keys":["key1", "key2"]}' http://localhost:3500/v1.0/state/statestore/bulk
```
{{% /codetab %}}
{{% codetab %}}
With the same dapr instance running from above save two key/value pairs into your statestore:
```powershell
Invoke-RestMethod -Method Post -ContentType 'application/json' -Body '[{ "key": "key1", "value": "value1"}, { "key": "key2", "value": "value2"}]' -Uri 'http://localhost:3500/v1.0/state/statestore'
```
Now get the states you just saved:
```powershell
Invoke-RestMethod -Method Post -ContentType 'application/json' -Body '{"keys":["key1", "key2"]}' -Uri 'http://localhost:3500/v1.0/state/statestore/bulk'
```
{{% /codetab %}}
{{% codetab %}}
The `StateItem` object can be used to store multiple Dapr states with the `save_states` and `get_states` methods.
Update your `pythonState.py` file with the following code:
```python
from dapr.clients import DaprClient
from dapr.clients.grpc._state import StateItem
with DaprClient() as d:
s1 = StateItem(key="key1", value="value1")
s2 = StateItem(key="key2", value="value2")
d.save_bulk_state(store_name="statestore", states=[s1,s2])
print("States have been stored")
items = d.get_bulk_state(store_name="statestore", keys=["key1", "key2"]).items
print(f"Got items: {[i.data for i in items]}")
```
Now run your program with:
```bash
dapr --app-id myapp run python pythonState.py
```
You should see an output similar to the following:
```md
== DAPR == time="2021-01-06T21:54:56.7262358-08:00" level=info msg="starting Dapr Runtime -- version 0.11.3 -- commit a1a8e11" app_id=Musesequoia-Sprite scope=dapr.runtime type=log ver=0.11.3
== DAPR == time="2021-01-06T21:54:56.7401933-08:00" level=info msg="standalone mode configured" app_id=Musesequoia-Sprite scope=dapr.runtime type=log ver=0.11.3
== DAPR == time="2021-01-06T21:54:56.8754240-08:00" level=info msg="Initialized name resolution to standalone" app_id=Musesequoia-Sprite scope=dapr.runtime type=log ver=0.11.3
== DAPR == time="2021-01-06T21:54:56.8844248-08:00" level=info msg="component loaded. name: statestore, type: state.redis" app_id=Musesequoia-Sprite scope=dapr.runtime type=log ver=0.11.3
== DAPR == time="2021-01-06T21:54:56.8854273-08:00" level=info msg="API gRPC server is running on port 60614" app_id=Musesequoia-Sprite scope=dapr.runtime type=log ver=0.11.3
== DAPR == time="2021-01-06T21:54:56.8854273-08:00" level=info msg="dapr initialized. Status: Running. Init Elapsed 145.234ms" app_id=Musesequoia-Sprite scope=dapr.runtime type=log ver=0.11.3
Checking if Dapr sidecar is listening on GRPC port 60614
Dapr sidecar is up and running.
Updating metadata for app command: python pythonState.py
You're up and running! Both Dapr and your app logs will appear here.
== APP == States have been stored
== APP == Got items: [b'value1', b'value2']
```
{{% /codetab %}}
{{% codetab %}}
To batch load and save state with PHP, just create a "Plain Ole' PHP Object" (POPO) and annotate it with
the StateStore annotation.
Update the `state-example.php` file:
```php
<?php
require_once __DIR__.'/vendor/autoload.php';
#[\Dapr\State\Attributes\StateStore('statestore', \Dapr\consistency\EventualLastWrite::class)]
class MyState {
public string $key1 = 'value1';
public string $key2 = 'value2';
}
$app = \Dapr\App::create();
$app->run(function(\Dapr\State\StateManager $stateManager, \Psr\Log\LoggerInterface $logger) {
$obj = new MyState();
$stateManager->save_object(item: $obj);
$logger->alert('States have been stored');
$stateManager->load_object(into: $obj);
$logger->alert("Got value: {data}", ['data' => $obj]);
});
```
Run the app:
```bash
dapr --app-id myapp run -- php state-example.php
```
And see the following output:
```md
✅ You're up and running! Both Dapr and your app logs will appear here.
== APP == [2021-02-12T16:55:02.913801+01:00] APP.ALERT: States have been stored [] []
== APP == [2021-02-12T16:55:02.917850+01:00] APP.ALERT: Got value: [object MyState] {"data":{"MyState":{"key1":"value1","key2":"value2"}}} []
```
{{% /codetab %}}
{{< /tabs >}}
## Step 5: Perform state transactions
{{% alert title="Note" color="warning" %}}
State transactions require a state store that supports multi-item transactions. Visit the [supported state stores page]({{< ref supported-state-stores >}}) page for a full list. Note that the default Redis container created in a self-hosted environment supports them.
{{% /alert %}}
{{< tabs "HTTP API (Bash)" "HTTP API (PowerShell)" "Python SDK" "PHP SDK">}}
{{% codetab %}}
With the same dapr instance running from above perform two state transactions:
```bash
curl -X POST -H "Content-Type: application/json" -d '{"operations": [{"operation":"upsert", "request": {"key": "key1", "value": "newValue1"}}, {"operation":"delete", "request": {"key": "key2"}}]}' http://localhost:3500/v1.0/state/statestore/transaction
```
Now see the results of your state transactions:
```bash
curl -X POST -H "Content-Type: application/json" -d '{"keys":["key1", "key2"]}' http://localhost:3500/v1.0/state/statestore/bulk
```
{{% /codetab %}}
{{% codetab %}}
With the same dapr instance running from above save two key/value pairs into your statestore:
```powershell
Invoke-RestMethod -Method Post -ContentType 'application/json' -Body '{"operations": [{"operation":"upsert", "request": {"key": "key1", "value": "newValue1"}}, {"operation":"delete", "request": {"key": "key2"}}]}' -Uri 'http://localhost:3500/v1.0/state/statestore'
```
Now see the results of your state transactions:
```powershell
Invoke-RestMethod -Method Post -ContentType 'application/json' -Body '{"keys":["key1", "key2"]}' -Uri 'http://localhost:3500/v1.0/state/statestore/bulk'
```
{{% /codetab %}}
{{% codetab %}}
The `TransactionalStateOperation` can perform a state transaction if your state stores need to be transactional.
Update your `pythonState.py` file with the following code:
```python
from dapr.clients import DaprClient
from dapr.clients.grpc._state import StateItem
from dapr.clients.grpc._request import TransactionalStateOperation, TransactionOperationType
with DaprClient() as d:
s1 = StateItem(key="key1", value="value1")
s2 = StateItem(key="key2", value="value2")
d.save_bulk_state(store_name="statestore", states=[s1,s2])
print("States have been stored")
d.execute_state_transaction(
store_name="statestore",
operations=[
TransactionalStateOperation(key="key1", data="newValue1", operation_type=TransactionOperationType.upsert),
TransactionalStateOperation(key="key2", data="value2", operation_type=TransactionOperationType.delete)
]
)
print("State transactions have been completed")
items = d.get_bulk_state(store_name="statestore", keys=["key1", "key2"]).items
print(f"Got items: {[i.data for i in items]}")
```
Now run your program with:
```bash
dapr run python pythonState.py
```
You should see an output similar to the following:
```md
Starting Dapr with id Singerchecker-Player. HTTP Port: 59533. gRPC Port: 59534
== DAPR == time="2021-01-06T22:18:14.1246721-08:00" level=info msg="starting Dapr Runtime -- version 0.11.3 -- commit a1a8e11" app_id=Singerchecker-Player scope=dapr.runtime type=log ver=0.11.3
== DAPR == time="2021-01-06T22:18:14.1346254-08:00" level=info msg="standalone mode configured" app_id=Singerchecker-Player scope=dapr.runtime type=log ver=0.11.3
== DAPR == time="2021-01-06T22:18:14.2747063-08:00" level=info msg="component loaded. name: statestore, type: state.redis" app_id=Singerchecker-Player scope=dapr.runtime type=log ver=0.11.3
== DAPR == time="2021-01-06T22:18:14.2757062-08:00" level=info msg="API gRPC server is running on port 59534" app_id=Singerchecker-Player scope=dapr.runtime type=log ver=0.11.3
== DAPR == time="2021-01-06T22:18:14.2767059-08:00" level=info msg="dapr initialized. Status: Running. Init Elapsed 142.0805ms" app_id=Singerchecker-Player scope=dapr.runtime type=log ver=0.11.3
Checking if Dapr sidecar is listening on GRPC port 59534
Dapr sidecar is up and running.
Updating metadata for app command: python pythonState.py
You're up and running! Both Dapr and your app logs will appear here.
== APP == State transactions have been completed
== APP == Got items: [b'value1', b'']
```
{{% /codetab %}}
{{% codetab %}}
Transactional state is supported by extending `TransactionalState` base object which hooks into your
object via setters and getters to provide a transaction. Before you created your own transactional object,
but now you'll ask the Dependency Injection framework to build one for you.
Modify the `state-example.php` file again:
```php
<?php
require_once __DIR__.'/vendor/autoload.php';
#[\Dapr\State\Attributes\StateStore('statestore', \Dapr\consistency\EventualLastWrite::class)]
class MyState extends \Dapr\State\TransactionalState {
public string $key1 = 'value1';
public string $key2 = 'value2';
}
$app = \Dapr\App::create();
$app->run(function(MyState $obj, \Psr\Log\LoggerInterface $logger, \Dapr\State\StateManager $stateManager) {
$obj->begin();
$obj->key1 = 'hello world';
$obj->key2 = 'value3';
$obj->commit();
$logger->alert('Transaction committed!');
// begin a new transaction which reloads from the store
$obj->begin();
$logger->alert("Got value: {key1}, {key2}", ['key1' => $obj->key1, 'key2' => $obj->key2]);
});
```
Run the application:
```bash
dapr --app-id myapp run -- php state-example.php
```
Observe the following output:
```md
✅ You're up and running! Both Dapr and your app logs will appear here.
== APP == [2021-02-12T17:10:06.837110+01:00] APP.ALERT: Transaction committed! [] []
== APP == [2021-02-12T17:10:06.840857+01:00] APP.ALERT: Got value: hello world, value3 {"key1":"hello world","key2":"value3"} []
```
{{% /codetab %}}
{{< /tabs >}}
## Next steps
- Read the full [State API reference]({{< ref state_api.md >}})
- Try one of the [Dapr SDKs]({{< ref sdks >}})
- Build a [stateful service]({{< ref howto-stateful-service.md >}})