-
Notifications
You must be signed in to change notification settings - Fork 319
/
datastore.adoc
1383 lines (989 loc) · 51.2 KB
/
datastore.adoc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
:spring-data-commons-ref: https://docs.spring.io/spring-data/data-commons/docs/current/reference/html
[#spring-data-cloud-datastore]
== Spring Data Cloud Datastore
NOTE: This integration is fully compatible with https://cloud.google.com/datastore/docs/[Firestore in Datastore Mode], but not with Firestore in Native Mode.
https://projects.spring.io/spring-data/[Spring Data] is an abstraction for storing and retrieving POJOs in numerous storage technologies.
Spring Framework on Google Cloud adds Spring Data support for https://cloud.google.com/firestore/[Google Cloud Firestore] in Datastore mode.
Maven coordinates for this module only, using <<getting-started.adoc#bill-of-materials, Spring Framework on Google Cloud BOM>>:
[source,xml]
----
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>spring-cloud-gcp-data-datastore</artifactId>
</dependency>
----
Gradle coordinates:
[source]
----
dependencies {
implementation("com.google.cloud:spring-cloud-gcp-data-datastore")
}
----
We provide a link:https://github.com/GoogleCloudPlatform/spring-cloud-gcp/tree/main/spring-cloud-gcp-starters/spring-cloud-gcp-starter-data-datastore[Spring Boot Starter for Spring Data Datastore], with which you can use our recommended auto-configuration setup.
To use the starter, see the coordinates below.
Maven:
[source,xml]
----
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>spring-cloud-gcp-starter-data-datastore</artifactId>
</dependency>
----
Gradle:
[source,subs="normal"]
----
dependencies {
implementation("com.google.cloud:spring-cloud-gcp-starter-data-datastore")
}
----
This setup takes care of bringing in the latest compatible version of Cloud Java Cloud Datastore libraries as well.
=== Configuration
To setup Spring Data Cloud Datastore, you have to configure the following:
* Setup the connection details to Google Cloud Datastore.
==== Cloud Datastore settings
You can use the link:https://github.com/GoogleCloudPlatform/spring-cloud-gcp/tree/main/spring-cloud-gcp-starters/spring-cloud-gcp-starter-data-datastore[Spring Boot Starter for Spring Data Datastore] to autoconfigure Google Cloud Datastore in your Spring application.
It contains all the necessary setup that makes it easy to authenticate with your Google Cloud project.
The following configuration options are available:
|===
| Name | Description | Required | Default value
| `spring.cloud.gcp.datastore.enabled` | Enables the Cloud Datastore client | No | `true`
| `spring.cloud.gcp.datastore.project-id` | Google Cloud project ID where the Google Cloud Datastore API is hosted, if different from the one in the <<spring-cloud-gcp-core,Spring Framework on Google Cloud Core Module>> | No |
| `spring.cloud.gcp.datastore.database-id` | Google Cloud project can host multiple databases. You can specify which database will be used. If not specified, the database id will be "(default)". | No |
| `spring.cloud.gcp.datastore.credentials.location` | OAuth2 credentials for authenticating with the Google Cloud Datastore API, if different from the ones in the <<spring-cloud-gcp-core,Spring Framework on Google Cloud Core Module>> | No |
| `spring.cloud.gcp.datastore.credentials.encoded-key` | Base64-encoded OAuth2 credentials for authenticating with the Google Cloud Datastore API, if different from the ones in the <<spring-cloud-gcp-core,Spring Framework on Google Cloud Core Module>> | No |
| `spring.cloud.gcp.datastore.credentials.scopes` | https://developers.google.com/identity/protocols/googlescopes[OAuth2 scope] for Spring Framework on Google CloudDatastore credentials | No | https://www.googleapis.com/auth/datastore
| `spring.cloud.gcp.datastore.namespace` | The Cloud Datastore namespace to use | No | the Default namespace of Cloud Datastore in your Google Cloud project
| `spring.cloud.gcp.datastore.host` | The `hostname:port` of the datastore service or emulator to connect to. Can be used to connect to a manually started https://cloud.google.com/datastore/docs/tools/datastore-emulator[Datastore Emulator]. If the autoconfigured emulator is enabled, this property will be ignored and `localhost:<emulator_port>` will be used. | No |
| `spring.cloud.gcp.datastore.emulator.enabled` | To enable the auto configuration to start a local instance of the Datastore Emulator. | No | `false`
| `spring.cloud.gcp.datastore.emulator.port` | The local port to use for the Datastore Emulator | No | `8081`
| `spring.cloud.gcp.datastore.emulator.consistency` | The https://cloud.google.com/sdk/gcloud/reference/beta/emulators/datastore/start?#--consistency[consistency] to use for the Datastore Emulator instance | No | `0.9`
| `spring.cloud.gcp.datastore.emulator.store-on-disk` | Configures whether or not the emulator should persist any data to disk. | No | `true`
| `spring.cloud.gcp.datastore.emulator.data-dir` | The directory to be used to store/retrieve data/config for an emulator run. | No | The default value is `<USER_CONFIG_DIR>/emulators/datastore`. See the https://cloud.google.com/sdk/gcloud/reference/beta/emulators/datastore/start[gcloud documentation] for finding your `USER_CONFIG_DIR`.
|===
==== Repository settings
Spring Data Repositories can be configured via the `@EnableDatastoreRepositories` annotation on your main `@Configuration` class.
With our Spring Boot Starter for Spring Data Cloud Datastore, `@EnableDatastoreRepositories` is automatically added.
It is not required to add it to any other class, unless there is a need to override finer grain configuration parameters provided by https://github.com/GoogleCloudPlatform/spring-cloud-gcp/blob/main/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/config/EnableDatastoreRepositories.java[`@EnableDatastoreRepositories`].
==== Autoconfiguration
Our Spring Boot autoconfiguration creates the following beans available in the Spring application context:
- an instance of `DatastoreTemplate`
- an instance of all user defined repositories extending `CrudRepository`, `PagingAndSortingRepository`, and `DatastoreRepository` (an extension of `PagingAndSortingRepository` with additional Cloud Datastore features) when repositories are enabled
- an instance of `Datastore` from the Google Cloud Java Client for Datastore, for convenience and lower level API access
==== Datastore Emulator Autoconfiguration
This Spring Boot autoconfiguration can also configure and start a local Datastore Emulator server if enabled by property.
It is useful for integration testing, but not for production.
When enabled, the `spring.cloud.gcp.datastore.host` property will be ignored and the Datastore autoconfiguration itself will be forced to connect to the autoconfigured local emulator instance.
It will create an instance of `LocalDatastoreHelper` as a bean that stores the `DatastoreOptions` to get the `Datastore` client connection to the emulator for convenience and lower level API for local access.
The emulator will be properly stopped after the Spring application context shutdown.
=== Object Mapping
Spring Data Cloud Datastore allows you to map domain POJOs to Cloud Datastore kinds and entities via annotations:
[source,java]
----
@Entity(name = "traders")
public class Trader {
@Id
@Field(name = "trader_id")
String traderId;
String firstName;
String lastName;
@Transient
Double temporaryNumber;
}
----
Spring Data Cloud Datastore will ignore any property annotated with `@Transient`.
These properties will not be written to or read from Cloud Datastore.
==== Constructors
Simple constructors are supported on POJOs.
The constructor arguments can be a subset of the persistent properties.
Every constructor argument needs to have the same name and type as a persistent property on the entity and the constructor should set the property from the given argument.
Arguments that are not directly set to properties are not supported.
[source,java]
----
@Entity(name = "traders")
public class Trader {
@Id
@Field(name = "trader_id")
String traderId;
String firstName;
String lastName;
@Transient
Double temporaryNumber;
public Trader(String traderId, String firstName) {
this.traderId = traderId;
this.firstName = firstName;
}
}
----
==== Kind
The `@Entity` annotation can provide the name of the Cloud Datastore kind that stores instances of the annotated class, one per row.
==== Keys
`@Id` identifies the property corresponding to the ID value.
You must annotate one of your POJO's fields as the ID value, because every entity in Cloud Datastore requires a single ID value:
[source,java]
----
@Entity(name = "trades")
public class Trade {
@Id
@Field(name = "trade_id")
String tradeId;
@Field(name = "trader_id")
String traderId;
String action;
Double price;
Double shares;
String symbol;
}
----
Datastore can automatically allocate integer ID values.
If a POJO instance with a `Long` ID property is written to Cloud Datastore with `null` as the ID value, then Spring Data Cloud Datastore will obtain a newly allocated ID value from Cloud Datastore and set that in the POJO for saving.
Because primitive `long` ID properties cannot be `null` and default to `0`, keys will not be allocated.
==== Fields
All accessible properties on POJOs are automatically recognized as a Cloud Datastore field.
Field naming is generated by the `PropertyNameFieldNamingStrategy` by default defined on the `DatastoreMappingContext` bean.
The `@Field` annotation optionally provides a different field name than that of the property.
==== Supported Types
Spring Data Cloud Datastore supports the following types for regular fields and elements of collections:
[options="header"]
|===
| Type| Stored as
| `com.google.cloud.Timestamp`|com.google.cloud.datastore.TimestampValue
| `com.google.cloud.datastore.Blob`|com.google.cloud.datastore.BlobValue
| `com.google.cloud.datastore.LatLng`|com.google.cloud.datastore.LatLngValue
| `java.lang.Boolean`, `boolean`|com.google.cloud.datastore.BooleanValue
| `java.lang.Double`, `double`|com.google.cloud.datastore.DoubleValue
| `java.lang.Long`, `long`|com.google.cloud.datastore.LongValue
| `java.lang.Integer`, `int`|com.google.cloud.datastore.LongValue
| `java.lang.String`|com.google.cloud.datastore.StringValue
| `com.google.cloud.datastore.Entity`|com.google.cloud.datastore.EntityValue
| `com.google.cloud.datastore.Key`|com.google.cloud.datastore.KeyValue
| `byte[]`|com.google.cloud.datastore.BlobValue
| Java `enum` values|com.google.cloud.datastore.StringValue
|===
In addition, all types that can be converted to the ones listed in the table by
`org.springframework.core.convert.support.DefaultConversionService` are supported.
==== Custom types
Custom converters can be used extending the type support for user defined types.
. Converters need to implement the `org.springframework.core.convert.converter.Converter` interface in both directions.
. The user defined type needs to be mapped to one of the basic types supported by Cloud Datastore.
. An instance of both Converters (read and write) needs to be passed to the `DatastoreCustomConversions` constructor, which then has to be made available as a `@Bean` for `DatastoreCustomConversions`.
For example:
We would like to have a field of type `Album` on our `Singer` POJO and want it to be stored as a string property:
[source, java]
----
@Entity
public class Singer {
@Id
String singerId;
String name;
Album album;
}
----
Where Album is a simple class:
[source, java]
----
public class Album {
String albumName;
LocalDate date;
}
----
We have to define the two converters:
[source, java]
----
// Converter to write custom Album type
static final Converter<Album, String> ALBUM_STRING_CONVERTER =
new Converter<Album, String>() {
@Override
public String convert(Album album) {
return album.getAlbumName() + " " + album.getDate().format(DateTimeFormatter.ISO_DATE);
}
};
// Converters to read custom Album type
static final Converter<String, Album> STRING_ALBUM_CONVERTER =
new Converter<String, Album>() {
@Override
public Album convert(String s) {
String[] parts = s.split(" ");
return new Album(parts[0], LocalDate.parse(parts[parts.length - 1], DateTimeFormatter.ISO_DATE));
}
};
----
That will be configured in our `@Configuration` file:
[source, java]
----
@Configuration
public class ConverterConfiguration {
@Bean
public DatastoreCustomConversions datastoreCustomConversions() {
return new DatastoreCustomConversions(
Arrays.asList(
ALBUM_STRING_CONVERTER,
STRING_ALBUM_CONVERTER));
}
}
----
==== Collections and arrays
Arrays and collections (types that implement `java.util.Collection`) of supported types are supported.
They are stored as `com.google.cloud.datastore.ListValue`.
Elements are converted to Cloud Datastore supported types individually. `byte[]` is an exception, it is converted to
`com.google.cloud.datastore.Blob`.
==== Custom Converter for collections
Users can provide converters from `List<?>` to the custom collection type.
Only read converter is necessary, the Collection API is used on the write side to convert a collection to the internal list type.
Collection converters need to implement the `org.springframework.core.convert.converter.Converter` interface.
Example:
Let's improve the Singer class from the previous example.
Instead of a field of type `Album`, we would like to have a field of type `Set<Album>`:
[source, java]
----
@Entity
public class Singer {
@Id
String singerId;
String name;
Set<Album> albums;
}
----
We have to define a read converter only:
[source, java]
----
static final Converter<List<?>, Set<?>> LIST_SET_CONVERTER =
new Converter<List<?>, Set<?>>() {
@Override
public Set<?> convert(List<?> source) {
return Collections.unmodifiableSet(new HashSet<>(source));
}
};
----
And add it to the list of custom converters:
[source, java]
----
@Configuration
public class ConverterConfiguration {
@Bean
public DatastoreCustomConversions datastoreCustomConversions() {
return new DatastoreCustomConversions(
Arrays.asList(
LIST_SET_CONVERTER,
ALBUM_STRING_CONVERTER,
STRING_ALBUM_CONVERTER));
}
}
----
==== Inheritance Hierarchies
Java entity types related by inheritance can be stored in the same Kind.
When reading and querying entities using `DatastoreRepository` or `DatastoreTemplate` with a superclass as the type parameter, you can receive instances of subclasses if you annotate the superclass and its subclasses with `DiscriminatorField` and `DiscriminatorValue`:
[source, java]
----
@Entity(name = "pets")
@DiscriminatorField(field = "pet_type")
abstract class Pet {
@Id
Long id;
abstract String speak();
}
@DiscriminatorValue("cat")
class Cat extends Pet {
@Override
String speak() {
return "meow";
}
}
@DiscriminatorValue("dog")
class Dog extends Pet {
@Override
String speak() {
return "woof";
}
}
@DiscriminatorValue("pug")
class Pug extends Dog {
@Override
String speak() {
return "woof woof";
}
}
----
Instances of all 3 types are stored in the `pets` Kind.
Because a single Kind is used, all classes in the hierarchy must share the same ID property and no two instances of any type in the hierarchy can share the same ID value.
Entity rows in Cloud Datastore store their respective types' `DiscriminatorValue` in a field specified by the root superclass's `DiscriminatorField` (`pet_type` in this case).
Reads and queries using a given type parameter will match each entity with its specific type.
For example, reading a `List<Pet>` will produce a list containing instances of all 3 types.
However, reading a `List<Dog>` will produce a list containing only `Dog` and `Pug` instances.
You can include the `pet_type` discrimination field in your Java entities, but its type must be convertible to a collection or array of `String`.
Any value set in the discrimination field will be overwritten upon write to Cloud Datastore.
=== Relationships
There are three ways to represent relationships between entities that are described in this section:
* Embedded entities stored directly in the field of the containing entity
* `@Descendant` annotated properties for one-to-many relationships
* `@Reference` annotated properties for general relationships without hierarchy
* `@LazyReference` similar to `@Reference`, but the entities are lazy-loaded when the property is accessed.
(Note that the keys of the children are retrieved when the parent entity is loaded.)
==== Embedded Entities
Fields whose types are also annotated with `@Entity` are converted to `EntityValue` and stored inside the parent entity.
Here is an example of Cloud Datastore entity containing an embedded entity in JSON:
[source, json]
----
{
"name" : "Alexander",
"age" : 47,
"child" : {"name" : "Philip" }
}
----
This corresponds to a simple pair of Java entities:
[source, java]
----
import com.google.cloud.spring.data.datastore.core.mapping.Entity;
import org.springframework.data.annotation.Id;
@Entity("parents")
public class Parent {
@Id
String name;
Child child;
}
@Entity
public class Child {
String name;
}
----
`Child` entities are not stored in their own kind.
They are stored in their entirety in the `child` field of the `parents` kind.
Multiple levels of embedded entities are supported.
NOTE: Embedded entities don't need to have `@Id` field, it is only required for top level entities.
Example:
Entities can hold embedded entities that are their own type.
We can store trees in Cloud Datastore using this feature:
[source, java]
----
import com.google.cloud.spring.data.datastore.core.mapping.Embedded;
import com.google.cloud.spring.data.datastore.core.mapping.Entity;
import org.springframework.data.annotation.Id;
@Entity
public class EmbeddableTreeNode {
@Id
long value;
EmbeddableTreeNode left;
EmbeddableTreeNode right;
Map<String, Long> longValues;
Map<String, List<Timestamp>> listTimestamps;
public EmbeddableTreeNode(long value, EmbeddableTreeNode left, EmbeddableTreeNode right) {
this.value = value;
this.left = left;
this.right = right;
}
}
----
===== Maps
Maps will be stored as embedded entities where the key values become the field names in the embedded entity.
The value types in these maps can be any regularly supported property type, and the key values will be converted to String using the configured converters.
Also, a collection of entities can be embedded; it will be converted to `ListValue` on write.
Example:
Instead of a binary tree from the previous example, we would like to store a general tree
(each node can have an arbitrary number of children) in Cloud Datastore.
To do that, we need to create a field of type `List<EmbeddableTreeNode>`:
[source, java]
----
import com.google.cloud.spring.data.datastore.core.mapping.Embedded;
import org.springframework.data.annotation.Id;
public class EmbeddableTreeNode {
@Id
long value;
List<EmbeddableTreeNode> children;
Map<String, EmbeddableTreeNode> siblingNodes;
Map<String, Set<EmbeddableTreeNode>> subNodeGroups;
public EmbeddableTreeNode(List<EmbeddableTreeNode> children) {
this.children = children;
}
}
----
Because Maps are stored as entities, they can further hold embedded entities:
- Singular embedded objects in the value can be stored in the values of embedded Maps.
- Collections of embedded objects in the value can also be stored as the values of embedded Maps.
- Maps in the value are further stored as embedded entities with the same rules applied recursively for their values.
==== Ancestor-Descendant Relationships
Parent-child relationships are supported via the `@Descendants` annotation.
Unlike embedded children, descendants are fully-formed entities residing in their own kinds.
The parent entity does not have an extra field to hold the descendant entities.
Instead, the relationship is captured in the descendants' keys, which refer to their parent entities:
[source, java]
----
import com.google.cloud.spring.data.datastore.core.mapping.Descendants;
import com.google.cloud.spring.data.datastore.core.mapping.Entity;
import org.springframework.data.annotation.Id;
@Entity("orders")
public class ShoppingOrder {
@Id
long id;
@Descendants
List<Item> items;
}
@Entity("purchased_item")
public class Item {
@Id
Key purchasedItemKey;
String name;
Timestamp timeAddedToOrder;
}
----
For example, an instance of a GQL key-literal representation for `Item` would also contain the parent `ShoppingOrder` ID value:
----
Key(orders, '12345', purchased_item, 'eggs')
----
The GQL key-literal representation for the parent `ShoppingOrder` would be:
----
Key(orders, '12345')
----
The Cloud Datastore entities exist separately in their own kinds.
The `ShoppingOrder`:
----
{
"id" : 12345
}
----
The two items inside that order:
----
{
"purchasedItemKey" : Key(orders, '12345', purchased_item, 'eggs'),
"name" : "eggs",
"timeAddedToOrder" : "2014-09-27 12:30:00.45-8:00"
}
{
"purchasedItemKey" : Key(orders, '12345', purchased_item, 'sausage'),
"name" : "sausage",
"timeAddedToOrder" : "2014-09-28 11:30:00.45-9:00"
}
----
The parent-child relationship structure of objects is stored in Cloud Datastore using Datastore's https://cloud.google.com/datastore/docs/concepts/entities#ancestor_paths[ancestor relationships].
Because the relationships are defined by the Ancestor mechanism, there is no extra column needed in either the parent or child entity to store this relationship.
The relationship link is part of the descendant entity's key value.
These relationships can be many levels deep.
Properties holding child entities must be collection-like, but they can be any of the supported inter-convertible collection-like types that are supported for regular properties such as `List`, arrays, `Set`, etc...
Child items must have `Key` as their ID type because Cloud Datastore stores the ancestor relationship link inside the keys of the children.
Reading or saving an entity automatically causes all subsequent levels of children under that entity to be read or saved, respectively.
If a new child is created and added to a property annotated `@Descendants` and the key property is left null, then a new key will be allocated for that child.
The ordering of the retrieved children may not be the same as the ordering in the original property that was saved.
Child entities cannot be moved from the property of one parent to that of another unless the child's key property is set to `null` or a value that contains the new parent as an ancestor.
Since Cloud Datastore entity keys can have multiple parents, it is possible that a child entity appears in the property of multiple parent entities.
Because entity keys are immutable in Cloud Datastore, to change the key of a child you must delete the existing one and re-save it with the new key.
==== Key Reference Relationships
General relationships can be stored using the `@Reference` annotation.
[source, java]
----
import org.springframework.data.annotation.Reference;
import org.springframework.data.annotation.Id;
@Entity
public class ShoppingOrder {
@Id
long id;
@Reference
List<Item> items;
@Reference
Item specialSingleItem;
}
@Entity
public class Item {
@Id
Key purchasedItemKey;
String name;
Timestamp timeAddedToOrder;
}
----
`@Reference` relationships are between fully-formed entities residing in their own kinds.
The relationship between `ShoppingOrder` and `Item` entities are stored as a Key field inside `ShoppingOrder`, which are resolved to the underlying Java entity type by Spring Data Cloud Datastore:
----
{
"id" : 12345,
"specialSingleItem" : Key(item, "milk"),
"items" : [ Key(item, "eggs"), Key(item, "sausage") ]
}
----
Reference properties can either be singular or collection-like.
These properties correspond to actual columns in the entity and Cloud Datastore Kind that hold the key values of the referenced entities.
The referenced entities are full-fledged entities of other Kinds.
Similar to the `@Descendants` relationships, reading or writing an entity will recursively read or write all of the referenced entities at all levels.
If referenced entities have `null` ID values, then they will be saved as new entities and will have ID values allocated by Cloud Datastore.
There are no requirements for relationships between the key of an entity and the keys that entity holds as references.
The order of collection-like reference properties is not preserved when reading back from Cloud Datastore.
=== Datastore Operations & Template
`DatastoreOperations` and its implementation, `DatastoreTemplate`, provides the Template pattern familiar to Spring developers.
Using the auto-configuration provided by Spring Boot Starter for Datastore, your Spring application context will contain a fully configured `DatastoreTemplate` object that you can autowire in your application:
[source,java]
----
@SpringBootApplication
public class DatastoreTemplateExample {
@Autowired
DatastoreTemplate datastoreTemplate;
public void doSomething() {
this.datastoreTemplate.deleteAll(Trader.class);
//...
Trader t = new Trader();
//...
this.datastoreTemplate.save(t);
//...
List<Trader> traders = datastoreTemplate.findAll(Trader.class);
//...
}
}
----
The Template API provides convenience methods for:
- Write operations (saving and deleting)
- Read-write transactions
==== GQL Query
In addition to retrieving entities by their IDs, you can also submit queries.
[source,java]
----
<T> Iterable<T> query(Query<? extends BaseEntity> query, Class<T> entityClass);
<A, T> Iterable<T> query(Query<A> query, Function<A, T> entityFunc);
Iterable<Key> queryKeys(Query<Key> query);
----
These methods, respectively, allow querying for:
* entities mapped by a given entity class using all the same mapping and converting features
* arbitrary types produced by a given mapping function
* only the Cloud Datastore keys of the entities found by the query
==== Find by ID(s)
Using `DatastoreTemplate` you can find entities by id. For example:
[source,java]
----
Trader trader = this.datastoreTemplate.findById("trader1", Trader.class);
List<Trader> traders = this.datastoreTemplate.findAllById(Arrays.asList("trader1", "trader2"), Trader.class);
List<Trader> allTraders = this.datastoreTemplate.findAll(Trader.class);
----
Cloud Datastore uses key-based reads with strong consistency, but queries with eventual consistency.
In the example above the first two reads utilize keys, while the third is run by using a query based on the corresponding Kind of `Trader`.
===== Indexes
By default, all fields are indexed.
To disable indexing on a particular field, `@Unindexed` annotation can be used.
Example:
[source, java]
----
import com.google.cloud.spring.data.datastore.core.mapping.Unindexed;
public class ExampleItem {
long indexedField;
@Unindexed
long unindexedField;
@Unindexed
List<String> unindexedListField;
}
----
When using queries directly or via Query Methods, Cloud Datastore requires https://cloud.google.com/datastore/docs/concepts/indexes[composite custom indexes] if the select statement is not `SELECT *` or if there is more than one filtering condition in the `WHERE` clause.
===== Read with offsets, limits, and sorting
`DatastoreRepository` and custom-defined entity repositories implement the Spring Data `PagingAndSortingRepository`, which supports offsets and limits using page numbers and page sizes.
Paging and sorting options are also supported in `DatastoreTemplate` by supplying a `DatastoreQueryOptions` to `findAll`.
===== Partial read
This feature is not supported yet.
==== Write / Update
The write methods of `DatastoreOperations` accept a POJO and writes all of its properties to Datastore.
The required Datastore kind and entity metadata is obtained from the given object's actual type.
If a POJO was retrieved from Datastore and its ID value was changed and then written or updated, the operation will occur as if against a row with the new ID value.
The entity with the original ID value will not be affected.
[source, java]
----
Trader t = new Trader();
this.datastoreTemplate.save(t);
----
The `save` method behaves as update-or-insert.
In contrast, the `insert` method will fail if an entity already exists.
===== Partial Update
This feature is not supported yet.
==== Transactions
Read and write transactions are provided by `DatastoreOperations` via the `performTransaction` method:
[source,java]
----
@Autowired
DatastoreOperations myDatastoreOperations;
public String doWorkInsideTransaction() {
return myDatastoreOperations.performTransaction(
transactionDatastoreOperations -> {
// Work with transactionDatastoreOperations here.
// It is also a DatastoreOperations object.
return "transaction completed";
}
);
}
----
The `performTransaction` method accepts a `Function` that is provided an instance of a `DatastoreOperations` object.
The final returned value and type of the function is determined by the user.
You can use this object just as you would a regular `DatastoreOperations` with an exception:
- It cannot perform sub-transactions.
Because of Cloud Datastore's consistency guarantees, there are https://cloud.google.com/datastore/docs/concepts/transactions#what_can_be_done_in_a_transaction[limitations] to the operations and relationships among entities used inside transactions.
===== Declarative Transactions with @Transactional Annotation
This feature requires a bean of `DatastoreTransactionManager`, which is provided when using `spring-cloud-gcp-starter-data-datastore`.
`DatastoreTemplate` and `DatastoreRepository` support running methods with the `@Transactional` https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction-declarative[annotation] as transactions.
If a method annotated with `@Transactional` calls another method also annotated, then both methods will work within the same transaction.
`performTransaction` cannot be used in `@Transactional` annotated methods because Cloud Datastore does not support transactions within transactions.
Other Google Cloud database-related integrations like Spanner and Firestore can introduce `PlatformTransactionManager` beans, and can interfere with Datastore Transaction Manager. To disambiguate, explicitly specify the name of the transaction manager bean for such `@Transactional` methods. Example:
[source,java]
----
@Transactional(transactionManager = "datastoreTransactionManager")
----
==== Read-Write Support for Maps
You can work with Maps of type `Map<String, ?>` instead of with entity objects by directly reading and writing them to and from Cloud Datastore.
NOTE: This is a different situation than using entity objects that contain Map properties.
The map keys are used as field names for a Datastore entity and map values are converted to Datastore supported types.
Only simple types are supported (i.e. collections are not supported).
Converters for custom value types can be added (see <<Custom types>> section).
Example:
[source,java]
----
Map<String, Long> map = new HashMap<>();
map.put("field1", 1L);
map.put("field2", 2L);
map.put("field3", 3L);
keyForMap = datastoreTemplate.createKey("kindName", "id");
// write a map
datastoreTemplate.writeMap(keyForMap, map);
// read a map
Map<String, Long> loadedMap = datastoreTemplate.findByIdAsMap(keyForMap, Long.class);
----
=== Repositories
{spring-data-commons-ref}/#repositories[Spring Data Repositories] are an abstraction that can reduce boilerplate code.
For example:
[source,java]
----
public interface TraderRepository extends DatastoreRepository<Trader, String> {
}
----
Spring Data generates a working implementation of the specified interface, which can be autowired into an application.
The `Trader` type parameter to `DatastoreRepository` refers to the underlying domain type.
The second type parameter, `String` in this case, refers to the type of the key of the domain type.
[source,java]
----
public class MyApplication {
@Autowired
TraderRepository traderRepository;
public void demo() {
this.traderRepository.deleteAll();
String traderId = "demo_trader";
Trader t = new Trader();
t.traderId = traderId;
this.tradeRepository.save(t);
Iterable<Trader> allTraders = this.traderRepository.findAll();
int count = this.traderRepository.count();
}
}
----
Repositories allow you to define custom Query Methods (detailed in the following sections) for retrieving, counting, and deleting based on filtering and paging parameters.
Filtering parameters can be of types supported by your configured custom converters.
==== Query methods by convention
[source, java]
----
public interface TradeRepository extends DatastoreRepository<Trade, String[]> {
List<Trader> findByAction(String action);
// throws an exception if no results
Trader findOneByAction(String action);
// because of the annotation, returns null if no results
@Nullable
Trader getByAction(String action);
Optional<Trader> getOneByAction(String action);
int countByAction(String action);
boolean existsByAction(String action);
List<Trade> findTop3ByActionAndSymbolAndPriceGreaterThanAndPriceLessThanOrEqualOrderBySymbolDesc(
String action, String symbol, double priceFloor, double priceCeiling);
Page<TestEntity> findByAction(String action, Pageable pageable);
Slice<TestEntity> findBySymbol(String symbol, Pageable pageable);
List<TestEntity> findBySymbol(String symbol, Sort sort);
Stream<TestEntity> findBySymbol(String symbol);
}
----
In the example above the {spring-data-commons-ref}/#repositories.query-methods[query methods] in `TradeRepository` are generated based on the name of the methods using the {spring-data-commons-ref}#repositories.query-methods.query-creation[Spring Data Query creation naming convention].
NOTE: You can refer to nested fields using https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-property-expressions[Spring Data JPA Property Expressions]
Cloud Datastore only supports filter components joined by AND, and the following operations:
* `equals`
* `greater than or equals`
* `greater than`
* `less than or equals`
* `less than`
* `is null`
After writing a custom repository interface specifying just the signatures of these methods, implementations are generated for you and can be used with an auto-wired instance of the repository.
Because of Cloud Datastore's requirement that explicitly selected fields must all appear in a composite index together, `find` name-based query methods are run as `SELECT *`.
Delete queries are also supported.
For example, query methods such as `deleteByAction` or `removeByAction` delete entities found by `findByAction`.
Delete queries are run as separate read and delete operations instead of as a single transaction because Cloud Datastore cannot query in transactions unless ancestors for queries are specified.
As a result, `removeBy` and `deleteBy` name-convention query methods cannot be used inside transactions via either `performInTransaction` or `@Transactional` annotation.
Delete queries can have the following return types:
* An integer type that is the number of entities deleted
* A collection of entities that were deleted
* 'void'
Methods can have `org.springframework.data.domain.Pageable` parameter to control pagination and sorting, or `org.springframework.data.domain.Sort` parameter to control sorting only.
See https://docs.spring.io/spring-data/data-commons/docs/current/reference/html/#repositories.query-methods[Spring Data documentation] for details.
For returning multiple items in a repository method, we support Java collections as well as `org.springframework.data.domain.Page` and `org.springframework.data.domain.Slice`.
If a method's return type is `org.springframework.data.domain.Page`, the returned object will include current page, total number of results and total number of pages.