Datasets
One attribute usually holds one value. Sometimes one thing has several values of the same kind at once, a temperature per sensor, a place name per language, or a speed per measurement source. NGSI-LD calls each of those values an instance of the attribute, and tells them apart with a datasetId. The instances live side by side in one entity, each with its own value, its own observedAt and its own sub-attributes.
This guide shows how to create instances, write one of them, read and query them, delete them, and keep their histories apart. The data model guide introduces the idea, and this one is the working manual.
Creating instances
Write the instances as an array, each with its own datasetId. The value is any URI you choose, and it means what you say it means, so naming it after the sensor or the source is usual.
- http
- curl
- rust
- python
POST /ngsi-ld/v1/entities
Content-Type: application/json
{
"id": "urn:ngsi-ld:Vehicle:A4567",
"type": "Vehicle",
"speed": [
{
"type": "Property",
"value": 55,
"datasetId": "urn:ngsi-ld:Property:gps"
},
{
"type": "Property",
"value": 54.5,
"datasetId": "urn:ngsi-ld:Property:speedometer"
}
]
}
curl -i http://localhost:1056/ngsi-ld/v1/entities \
-H 'Content-Type: application/json' \
-d '{
"id": "urn:ngsi-ld:Vehicle:A4567",
"type": "Vehicle",
"speed": [
{
"type": "Property",
"value": 55,
"datasetId": "urn:ngsi-ld:Property:gps"
},
{
"type": "Property",
"value": 54.5,
"datasetId": "urn:ngsi-ld:Property:speedometer"
}
]
}'
let entity = Entity::builder()
.id(iri!("urn:ngsi-ld:Vehicle:A4567"))
.entity_type("Vehicle")
.property(
"speed",
Property::builder(55).dataset_id(iri!("urn:ngsi-ld:Property:gps")),
)
.property(
"speed",
Property::builder(Value::Number("54.5".parse()?))
.dataset_id(iri!("urn:ngsi-ld:Property:speedometer")),
)
.build()?;
let outcome = client.entities().create(&entity).send().await?;
response = requests.post(
f"{BASE}/entities",
json={
"id": "urn:ngsi-ld:Vehicle:A4567",
"type": "Vehicle",
"speed": [
{
"type": "Property",
"value": 55,
"datasetId": "urn:ngsi-ld:Property:gps",
},
{
"type": "Property",
"value": 54.5,
"datasetId": "urn:ngsi-ld:Property:speedometer",
},
],
},
)
assert response.status_code == 201
HTTP/1.1 201 Created
Location: /ngsi-ld/v1/entities/urn:ngsi-ld:Vehicle:A4567
An instance without a datasetId is the default instance. At most one can exist, and the ordinary single-value attribute you write every day is one. To name the default instance explicitly, write "datasetId": "@none", which stores an instance without a datasetId of its own.
Every instance reads back with its datasetId, so an answer to GET /entities/urn:ngsi-ld:Vehicle:A4567 holds speed as an array of two members in JSON terms, one per instance. The default instance reads back without the member.
Writing one instance
A write names the instance it means with datasetId in the attribute's body. Appending an instance whose datasetId no instance carries adds it beside the others.
- http
- curl
- rust
- python
POST /ngsi-ld/v1/entities/urn:ngsi-ld:Vehicle:A4567/attrs
Content-Type: application/json
{
"speed": {
"type": "Property",
"value": 60,
"datasetId": "urn:ngsi-ld:Property:newspeedometer"
}
}
curl -i http://localhost:1056/ngsi-ld/v1/entities/urn:ngsi-ld:Vehicle:A4567/attrs \
-H 'Content-Type: application/json' \
-d '{
"speed": {
"type": "Property",
"value": 60,
"datasetId": "urn:ngsi-ld:Property:newspeedometer"
}
}'
let fragment = EntityFragment::builder()
.property(
"speed",
Property::builder(60).dataset_id(iri!("urn:ngsi-ld:Property:newspeedometer")),
)
.build()?;
let outcome = client
.entities()
.append_attributes(&vehicle, &fragment)
.send()
.await?;
response = requests.post(
f"{VEHICLE}/attrs",
json={
"speed": {
"type": "Property",
"value": 60,
"datasetId": "urn:ngsi-ld:Property:newspeedometer",
},
},
)
assert response.status_code == 204
HTTP/1.1 204 No Content
The vehicle now holds three speeds, and a read shows all three. A write that leaves datasetId out, or writes @none, targets the default instance, and replaces the default that is there. Two writes with the same datasetId target the same instance, so the second replaces the first.
Update behaves the same way. When the body carries a datasetId no instance of the attribute carries, that instance is new, and update adds it. An update of speed with urn:ngsi-ld:Property:newspeedometer adds the third instance just as the append above did.
Partial attribute update and attribute replace read the datasetId of the fragment, so the same targeting works there. A query-parameter datasetId on those writes is refused with 400 as an invalid request, because the parameter exists for reads, not for these writes.
Merge is the exception. A merge fragment cannot change or remove a datasetId. Asking for it with the NGSI-LD Null is refused with 400 BadRequestData, and the instance named stays as it was.
Reading instances
Retrieve an entity with a datasetId query parameter to answer only the instances carrying it.
- http
- curl
- rust
- python
GET /ngsi-ld/v1/entities/urn:ngsi-ld:Vehicle:A4567?datasetId=urn:ngsi-ld:Property:gps
Accept: application/json
curl -i 'http://localhost:1056/ngsi-ld/v1/entities/urn:ngsi-ld:Vehicle:A4567?datasetId=urn:ngsi-ld:Property:gps' \
-H 'Accept: application/json'
let gps = DatasetId::from(iri!("urn:ngsi-ld:Property:gps"));
let retrieved = client
.entities()
.retrieve(&vehicle)
.dataset_ids(vec![gps])
.send()
.await?;
response = requests.get(
VEHICLE,
params={"datasetId": "urn:ngsi-ld:Property:gps"},
headers={"Accept": "application/json"},
)
assert response.status_code == 200
{
"id": "urn:ngsi-ld:Vehicle:A4567",
"type": "Vehicle",
"speed": {
"type": "Property",
"value": 55,
"datasetId": "urn:ngsi-ld:Property:gps"
}
}
The same parameter on a query narrows every attribute of every answer at once. A building holding name and locatedAt in three language datasets each answers, with datasetId=urn:ngsi-ld:Dataset:french-name, only its French name and its French location. datasetId=@none answers the default instances, and without the parameter every instance is answered. With attrs, the parameter selects among the instances of the named attributes.
Querying and subscribing
A q term is satisfied when any instance of the attribute satisfies it. A vehicle whose default speed is 10 and whose urn:ngsi-ld:Dataset:1 speed is 50 is selected by q=speed>40, because one instance passes. The same holds for sub-attributes reached through a path, where a term reads every instance on its way in. A geoquery matches when any instance of the target GeoProperty is inside the area, and an instance past its expiresAt takes no part in any of this.
The datasetId parameter decides what comes back, never what matches. The query selects entities on q and the geoquery alone, then hands back only the selected instances.
Subscriptions can carry the parameter too. A subscription with a datasetId notifies only about the instances carrying it, and a periodic notification carries only those instances of each attribute. When a write removes instances, the notification shows exactly what was removed, each nullified instance stamped with its datasetId.
Deleting instances
Delete an attribute without a parameter and only the default instance goes. To delete one instance, name it.
- http
- curl
- rust
- python
DELETE /ngsi-ld/v1/entities/urn:ngsi-ld:Vehicle:A4567/attrs/speed?datasetId=urn:ngsi-ld:Property:gps
curl -i -X DELETE 'http://localhost:1056/ngsi-ld/v1/entities/urn:ngsi-ld:Vehicle:A4567/attrs/speed?datasetId=urn:ngsi-ld:Property:gps'
let gps = Uri::from(iri!("urn:ngsi-ld:Property:gps"));
let outcome = client
.entities()
.delete_attribute(&vehicle, &speed)
.instances(InstanceSelection::Dataset(&gps))
.send()
.await?;
response = requests.delete(
f"{VEHICLE}/attrs/speed",
params={"datasetId": "urn:ngsi-ld:Property:gps"},
)
assert response.status_code == 204
HTTP/1.1 204 No Content
The speedometer instance stays, and if the attribute still holds one, the answer to a read is a single object rather than an array. To remove every instance at once, including the default, use deleteAll.
- http
- curl
- rust
- python
DELETE /ngsi-ld/v1/entities/urn:ngsi-ld:Vehicle:A4567/attrs/speed?deleteAll=true
curl -i -X DELETE 'http://localhost:1056/ngsi-ld/v1/entities/urn:ngsi-ld:Vehicle:A4567/attrs/speed?deleteAll=true'
let outcome = client
.entities()
.delete_attribute(&vehicle, &speed)
.instances(InstanceSelection::All)
.send()
.await?;
response = requests.delete(
f"{VEHICLE}/attrs/speed",
params={"deleteAll": "true"},
)
assert response.status_code == 204
A datasetId naming no instance answers 404 ResourceNotFound, and sending datasetId and deleteAll together is refused, because one names an instance and the other removes them all. In Rust the two are one choice, so asking for both can't be written.
Under merge, the JSON-null deletion targets instances as well. A null fragment with a datasetId deletes the instance it names, a null without one deletes the default instance, and the concise null form deletes every instance of the attribute. Deleting the last instance removes the attribute from the entity.
The temporal side
The Temporal API writes history, and history can hold parallel series from different sources just as the current state can. A temporal write of an instance with a datasetId adds its observations as their own series, beside the default one already recorded.
- http
- curl
- rust
- python
POST /ngsi-ld/v1/temporal/entities
Content-Type: application/json
{
"id": "urn:ngsi-ld:Vehicle:A4567",
"type": "Vehicle",
"speed": [
{
"type": "Property",
"value": 110,
"observedAt": "2020-08-01T12:05:00Z",
"datasetId": "urn:ngsi-ld:Dataset:speed"
}
]
}
curl -i http://localhost:1056/ngsi-ld/v1/temporal/entities \
-H 'Content-Type: application/json' \
-d '{
"id": "urn:ngsi-ld:Vehicle:A4567",
"type": "Vehicle",
"speed": [
{
"type": "Property",
"value": 110,
"observedAt": "2020-08-01T12:05:00Z",
"datasetId": "urn:ngsi-ld:Dataset:speed"
}
]
}'
let history = TemporalEntity::builder()
.id(iri!("urn:ngsi-ld:Vehicle:A4567"))
.entity_type("Vehicle")
.property(
"speed",
Property::builder(110)
.observed_at("2020-08-01T12:05:00Z".parse()?)
.dataset_id(iri!("urn:ngsi-ld:Dataset:speed")),
)
.build()?;
let outcome = client.temporal().create_or_update(&history).send().await?;
response = requests.post(
f"{BASE}/temporal/entities",
json={
"id": "urn:ngsi-ld:Vehicle:A4567",
"type": "Vehicle",
"speed": [
{
"type": "Property",
"value": 110,
"observedAt": "2020-08-01T12:05:00Z",
"datasetId": "urn:ngsi-ld:Dataset:speed",
},
],
},
)
assert response.status_code in (201, 204)
HTTP/1.1 204 No Content
A read of the temporal entity now answers both series, the datasetId carried on the new entries only. Temporal aggregation works each dataset on its own, so an average never mixes two sensors. The datasetId parameter of a temporal read narrows the answer the way it does on the Core API, and the temporal delete and delete-attribute operations take the same datasetId and deleteAll rules.
History entries are addressed by instanceId rather than datasetId. Each instance gets one when it is written, a URI that stays with the instance for its life. Andromeda mints one under urn:ngsi-ld:attribute:instance:, and you can supply your own on a temporal write instead. The same instanceId on two instances of one attribute is refused. Read it from an answer, then modify or delete that one entry in time through its own path.
- http
- curl
- rust
- python
DELETE /ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Vehicle:A4567/attrs/speed/urn:ngsi-ld:attribute:instance:7
curl -i -X DELETE http://localhost:1056/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Vehicle:A4567/attrs/speed/urn:ngsi-ld:attribute:instance:7
let instance = InstanceId::from(iri!("urn:ngsi-ld:attribute:instance:7"));
let outcome = client
.temporal()
.delete_instance(&vehicle, &speed, &instance)
.send()
.await?;
response = requests.delete(
f"{BASE}/temporal/entities/urn:ngsi-ld:Vehicle:A4567/attrs/speed/urn:ngsi-ld:attribute:instance:7",
)
assert response.status_code == 204
HTTP/1.1 204 No Content
Deleting an instance that is already gone answers 404 ResourceNotFound. The instanceId names one entry in the history, which is why the temporal operations use it where the Core API uses datasetId.
What gets refused
400BadRequestDatafor a JSONnullanywhere, including where adatasetIdwould sit.400BadRequestDatafor a merge fragment that tries to change or remove adatasetId.400InvalidRequestfor adatasetIdquery parameter on create, update, merge, replace, or any batch operation, which accept none.404ResourceNotFoundfor a delete or a null fragment naming an instance the attribute does not have.400BadRequestDatafor the sameinstanceIdsupplied on two instances of one attribute in a temporal write.