google-cloud-datastore 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,171 @@
1
+ # Copyright 2016 Google Inc. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ require "google/cloud/datastore/credentials"
17
+ require "google/datastore/v1/datastore_services_pb"
18
+ require "google/cloud/core/grpc_backoff"
19
+
20
+ module Google
21
+ module Cloud
22
+ module Datastore
23
+ ##
24
+ # @private Represents the gRPC Datastore service, including all the API
25
+ # methods.
26
+ class Service
27
+ attr_accessor :project, :credentials, :host, :retries, :timeout
28
+
29
+ ##
30
+ # Creates a new Service instance.
31
+ def initialize project, credentials, host: nil, retries: nil,
32
+ timeout: nil
33
+ @project = project
34
+ @credentials = credentials
35
+ @host = host || "datastore.googleapis.com"
36
+ @retries = retries
37
+ @timeout = timeout
38
+ end
39
+
40
+ def creds
41
+ return credentials if insecure?
42
+ GRPC::Core::ChannelCredentials.new.compose \
43
+ GRPC::Core::CallCredentials.new credentials.client.updater_proc
44
+ end
45
+
46
+ def datastore
47
+ return mocked_datastore if mocked_datastore
48
+ @datastore ||= Google::Datastore::V1::Datastore::Stub.new(
49
+ host, creds, timeout: timeout)
50
+ end
51
+ attr_accessor :mocked_datastore
52
+
53
+ def insecure?
54
+ credentials == :this_channel_is_insecure
55
+ end
56
+
57
+ ##
58
+ # Allocate IDs for incomplete keys.
59
+ # (This is useful for referencing an entity before it is inserted.)
60
+ def allocate_ids *incomplete_keys
61
+ allocate_req = Google::Datastore::V1::AllocateIdsRequest.new(
62
+ project_id: project,
63
+ keys: incomplete_keys
64
+ )
65
+
66
+ execute { datastore.allocate_ids allocate_req }
67
+ end
68
+
69
+ ##
70
+ # Look up entities by keys.
71
+ def lookup *keys, consistency: nil, transaction: nil
72
+ lookup_req = Google::Datastore::V1::LookupRequest.new(
73
+ project_id: project,
74
+ keys: keys
75
+ )
76
+ lookup_req.read_options = generate_read_options consistency,
77
+ transaction
78
+
79
+ execute { datastore.lookup lookup_req }
80
+ end
81
+
82
+ # Query for entities.
83
+ def run_query query, namespace = nil, consistency: nil, transaction: nil
84
+ run_req = Google::Datastore::V1::RunQueryRequest.new(
85
+ project_id: project)
86
+ if query.is_a? Google::Datastore::V1::Query
87
+ run_req["query"] = query
88
+ elsif query.is_a? Google::Datastore::V1::GqlQuery
89
+ run_req["gql_query"] = query
90
+ else
91
+ fail ArgumentError, "Unable to query with a #{query.class} object."
92
+ end
93
+ run_req.read_options = generate_read_options consistency, transaction
94
+
95
+ run_req.partition_id = Google::Datastore::V1::PartitionId.new(
96
+ namespace_id: namespace) if namespace
97
+
98
+ execute { datastore.run_query run_req }
99
+ end
100
+
101
+ ##
102
+ # Begin a new transaction.
103
+ def begin_transaction
104
+ tx_req = Google::Datastore::V1::BeginTransactionRequest.new(
105
+ project_id: project
106
+ )
107
+
108
+ execute { datastore.begin_transaction tx_req }
109
+ end
110
+
111
+ ##
112
+ # Commit a transaction, optionally creating, deleting or modifying
113
+ # some entities.
114
+ def commit mutations, transaction: nil
115
+ commit_req = Google::Datastore::V1::CommitRequest.new(
116
+ project_id: project,
117
+ mode: :NON_TRANSACTIONAL,
118
+ mutations: mutations
119
+ )
120
+ if transaction
121
+ commit_req.mode = :TRANSACTIONAL
122
+ commit_req.transaction = transaction
123
+ end
124
+
125
+ execute { datastore.commit commit_req }
126
+ end
127
+
128
+ ##
129
+ # Roll back a transaction.
130
+ def rollback transaction
131
+ rb_req = Google::Datastore::V1::RollbackRequest.new(
132
+ project_id: project,
133
+ transaction: transaction
134
+ )
135
+
136
+ execute { datastore.rollback rb_req }
137
+ end
138
+
139
+ def inspect
140
+ "#{self.class}(#{@project})"
141
+ end
142
+
143
+ ##
144
+ # Performs backoff and error handling
145
+ def execute
146
+ Google::Cloud::Core::GrpcBackoff.new(retries: retries).execute do
147
+ yield
148
+ end
149
+ rescue GRPC::BadStatus => e
150
+ raise Google::Cloud::Error.from_error(e)
151
+ end
152
+
153
+ protected
154
+
155
+ def generate_read_options consistency, transaction
156
+ if consistency == :eventual
157
+ return Google::Datastore::V1::ReadOptions.new(
158
+ read_consistency: :EVENTUAL)
159
+ elsif consistency == :strong
160
+ return Google::Datastore::V1::ReadOptions.new(
161
+ read_consistency: :STRONG)
162
+ elsif transaction
163
+ return Google::Datastore::V1::ReadOptions.new(
164
+ transaction: transaction)
165
+ end
166
+ nil
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,365 @@
1
+ # Copyright 2014 Google Inc. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ module Google
17
+ module Cloud
18
+ module Datastore
19
+ ##
20
+ # # Transaction
21
+ #
22
+ # Special Connection instance for running transactions.
23
+ #
24
+ # See {Google::Cloud::Datastore::Dataset#transaction}
25
+ #
26
+ # @see https://cloud.google.com/datastore/docs/concepts/transactions
27
+ # Transactions
28
+ #
29
+ # @example Transactional update:
30
+ # def transfer_funds from_key, to_key, amount
31
+ # datastore.transaction do |tx|
32
+ # from = tx.find from_key
33
+ # from["balance"] -= amount
34
+ # to = tx.find to_key
35
+ # to["balance"] += amount
36
+ # tx.save from, to
37
+ # end
38
+ # end
39
+ #
40
+ # @example Retry logic using the transactional update example above:
41
+ # (1..5).each do |i|
42
+ # begin
43
+ # return transfer_funds from_key, to_key, amount
44
+ # rescue Google::Cloud::Error => e
45
+ # raise e if i == 5
46
+ # end
47
+ # end
48
+ #
49
+ # @example Transactional read:
50
+ # task_list_key = datastore.key "TaskList", "default"
51
+ # datastore.transaction do |tx|
52
+ # task_list = tx.find task_list_key
53
+ # query = tx.query("Task").ancestor(task_list)
54
+ # tasks_in_list = tx.run query
55
+ # end
56
+ #
57
+ class Transaction < Dataset
58
+ attr_reader :id
59
+
60
+ ##
61
+ # @private Creates a new Transaction instance.
62
+ # Takes a Connection and Service instead of project and Credentials.
63
+ def initialize service
64
+ @service = service
65
+ reset!
66
+ start
67
+ end
68
+
69
+ ##
70
+ # Persist entities in a transaction.
71
+ #
72
+ # @example Transactional get or create:
73
+ # task_key = datastore.key "Task", "sampleTask"
74
+ #
75
+ # task = nil
76
+ # datastore.transaction do |tx|
77
+ # task = tx.find task_key
78
+ # if task.nil?
79
+ # task = datastore.entity task_key do |t|
80
+ # t["type"] = "Personal"
81
+ # t["done"] = false
82
+ # t["priority"] = 4
83
+ # t["description"] = "Learn Cloud Datastore"
84
+ # end
85
+ # tx.save task
86
+ # end
87
+ # end
88
+ #
89
+ def save *entities
90
+ @commit.save(*entities)
91
+ # Do not save yet
92
+ entities
93
+ end
94
+ alias_method :upsert, :save
95
+
96
+ ##
97
+ # Insert entities in a transaction. An InvalidArgumentError will raised
98
+ # if the entities cannot be inserted.
99
+ #
100
+ # @example Transactional insert:
101
+ # task_key = datastore.key "Task", "sampleTask"
102
+ #
103
+ # task = nil
104
+ # datastore.transaction do |tx|
105
+ # task = tx.find task_key
106
+ # if task.nil?
107
+ # task = datastore.entity task_key do |t|
108
+ # t["type"] = "Personal"
109
+ # t["done"] = false
110
+ # t["priority"] = 4
111
+ # t["description"] = "Learn Cloud Datastore"
112
+ # end
113
+ # tx.insert task
114
+ # end
115
+ # end
116
+ #
117
+ def insert *entities
118
+ @commit.insert(*entities)
119
+ # Do not insert yet
120
+ entities
121
+ end
122
+
123
+ ##
124
+ # Update entities in a transaction. An InvalidArgumentError will raised
125
+ # if the entities cannot be updated.
126
+ #
127
+ # @example Transactional update:
128
+ # task_key = datastore.key "Task", "sampleTask"
129
+ #
130
+ # task = nil
131
+ # datastore.transaction do |tx|
132
+ # task = tx.find task_key
133
+ # if task
134
+ # task["done"] = true
135
+ # tx.update task
136
+ # end
137
+ # end
138
+ #
139
+ def update *entities
140
+ @commit.update(*entities)
141
+ # Do not update yet
142
+ entities
143
+ end
144
+
145
+ ##
146
+ # Remove entities in a transaction.
147
+ #
148
+ # @example
149
+ # datastore.transaction do |tx|
150
+ # if tx.find(task_list.key).nil?
151
+ # tx.delete task1, task2
152
+ # end
153
+ # end
154
+ #
155
+ def delete *entities_or_keys
156
+ @commit.delete(*entities_or_keys)
157
+ # Do not delete yet
158
+ true
159
+ end
160
+
161
+ ##
162
+ # Retrieve an entity by providing key information. The lookup is run
163
+ # within the transaction.
164
+ #
165
+ # @param [Key, String] key_or_kind A Key object or `kind` string value.
166
+ #
167
+ # @return [Google::Cloud::Datastore::Entity, nil]
168
+ #
169
+ # @example Finding an entity with a key:
170
+ # task_key = datastore.key "Task", "sampleTask"
171
+ # task = datastore.find task_key
172
+ #
173
+ # @example Finding an entity with a `kind` and `id`/`name`:
174
+ # task = datastore.find "Task", "sampleTask"
175
+ #
176
+ def find key_or_kind, id_or_name = nil
177
+ key = key_or_kind
178
+ unless key.is_a? Google::Cloud::Datastore::Key
179
+ key = Key.new key_or_kind, id_or_name
180
+ end
181
+ find_all(key).first
182
+ end
183
+ alias_method :get, :find
184
+
185
+ ##
186
+ # Retrieve the entities for the provided keys. The lookup is run within
187
+ # the transaction.
188
+ #
189
+ # @param [Key] keys One or more Key objects to find records for.
190
+ #
191
+ # @return [Google::Cloud::Datastore::Dataset::LookupResults]
192
+ #
193
+ # @example
194
+ # gcloud = Google::Cloud.new
195
+ # datastore = gcloud.datastore
196
+ # task_key1 = datastore.key "Task", 123456
197
+ # task_key2 = datastore.key "Task", 987654
198
+ # tasks = datastore.find_all task_key1, task_key2
199
+ #
200
+ def find_all *keys
201
+ ensure_service!
202
+ lookup_res = service.lookup(*Array(keys).flatten.map(&:to_grpc),
203
+ transaction: @id)
204
+ LookupResults.from_grpc lookup_res, service, nil, @id
205
+ end
206
+ alias_method :lookup, :find_all
207
+
208
+ ##
209
+ # Retrieve entities specified by a Query. The query is run within the
210
+ # transaction.
211
+ #
212
+ # @param [Query] query The Query object with the search criteria.
213
+ # @param [String] namespace The namespace the query is to run within.
214
+ #
215
+ # @return [Google::Cloud::Datastore::Dataset::QueryResults]
216
+ #
217
+ # @example
218
+ # query = datastore.query("Task").
219
+ # where("done", "=", false)
220
+ # datastore.transaction do |tx|
221
+ # tasks = tx.run query
222
+ # end
223
+ #
224
+ # @example Run the query within a namespace with the `namespace` option:
225
+ # query = Google::Cloud::Datastore::Query.new.kind("Task").
226
+ # where("done", "=", false)
227
+ # datastore.transaction do |tx|
228
+ # tasks = tx.run query, namespace: "ns~todo-project"
229
+ # end
230
+ #
231
+ def run query, namespace: nil
232
+ ensure_service!
233
+ unless query.is_a?(Query) || query.is_a?(GqlQuery)
234
+ fail ArgumentError, "Cannot run a #{query.class} object."
235
+ end
236
+ query_res = service.run_query query.to_grpc, namespace,
237
+ transaction: @id
238
+ QueryResults.from_grpc query_res, service, namespace,
239
+ query.to_grpc.dup
240
+ end
241
+ alias_method :run_query, :run
242
+
243
+ ##
244
+ # Begins a transaction.
245
+ # This method is run when a new Transaction is created.
246
+ def start
247
+ fail TransactionError, "Transaction already opened." unless @id.nil?
248
+
249
+ ensure_service!
250
+ tx_res = service.begin_transaction
251
+ @id = tx_res.transaction
252
+ end
253
+ alias_method :begin_transaction, :start
254
+
255
+ ##
256
+ # Commits a transaction.
257
+ #
258
+ # @yield [commit] an optional block for making changes
259
+ # @yieldparam [Commit] commit The object that changes are made on
260
+ #
261
+ # @example
262
+ # require "google/cloud"
263
+ #
264
+ # gcloud = Google::Cloud.new
265
+ # datastore = gcloud.datastore
266
+ #
267
+ # task = datastore.entity "Task" do |t|
268
+ # t["type"] = "Personal"
269
+ # t["done"] = false
270
+ # t["priority"] = 4
271
+ # t["description"] = "Learn Cloud Datastore"
272
+ # end
273
+ #
274
+ # tx = datastore.transaction
275
+ # begin
276
+ # if tx.find(task.key).nil?
277
+ # tx.save task
278
+ # end
279
+ # tx.commit
280
+ # rescue
281
+ # tx.rollback
282
+ # end
283
+ #
284
+ # @example Commit can be passed a block, same as {Dataset#commit}:
285
+ # require "google/cloud"
286
+ #
287
+ # gcloud = Google::Cloud.new
288
+ # datastore = gcloud.datastore
289
+ #
290
+ # tx = datastore.transaction
291
+ # begin
292
+ # tx.commit do |c|
293
+ # c.save task1, task2
294
+ # c.delete entity1, entity2
295
+ # end
296
+ # rescue
297
+ # tx.rollback
298
+ # end
299
+ #
300
+ def commit
301
+ fail TransactionError,
302
+ "Cannot commit when not in a transaction." if @id.nil?
303
+
304
+ yield @commit if block_given?
305
+
306
+ ensure_service!
307
+
308
+ commit_res = service.commit @commit.mutations, transaction: @id
309
+ entities = @commit.entities
310
+ returned_keys = commit_res.mutation_results.map(&:key)
311
+ returned_keys.each_with_index do |key, index|
312
+ next if entities[index].nil?
313
+ entities[index].key = Key.from_grpc(key) unless key.nil?
314
+ end
315
+ # Make sure all entity keys are frozen so all show as persisted
316
+ entities.each { |e| e.key.freeze unless e.persisted? }
317
+ true
318
+ end
319
+
320
+ ##
321
+ # Rolls a transaction back.
322
+ #
323
+ # @example
324
+ # require "google/cloud"
325
+ #
326
+ # gcloud = Google::Cloud.new
327
+ # datastore = gcloud.datastore
328
+ #
329
+ # task = datastore.entity "Task" do |t|
330
+ # t["type"] = "Personal"
331
+ # t["done"] = false
332
+ # t["priority"] = 4
333
+ # t["description"] = "Learn Cloud Datastore"
334
+ # end
335
+ #
336
+ # tx = datastore.transaction
337
+ # begin
338
+ # if tx.find(task.key).nil?
339
+ # tx.save task
340
+ # end
341
+ # tx.commit
342
+ # rescue
343
+ # tx.rollback
344
+ # end
345
+ def rollback
346
+ if @id.nil?
347
+ fail TransactionError, "Cannot rollback when not in a transaction."
348
+ end
349
+
350
+ ensure_service!
351
+ service.rollback @id
352
+ true
353
+ end
354
+
355
+ ##
356
+ # Reset the transaction.
357
+ # {Transaction#start} must be called afterwards.
358
+ def reset!
359
+ @id = nil
360
+ @commit = Commit.new
361
+ end
362
+ end
363
+ end
364
+ end
365
+ end