activemodel-datastore 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4d8d2d055a0934bc97f36c9c46a0ef375d30aa4d
4
- data.tar.gz: 9640f7bb31c42d4a9c0f7e5d981cd75a20c847a7
3
+ metadata.gz: b11457bc388803b1f429027dd976ac9ed10250c9
4
+ data.tar.gz: 52247ef1a1e9b49ee738d756374c441b00bd9488
5
5
  SHA512:
6
- metadata.gz: fcf58339c5a1e7afc910fc5c3db2b7334af33d194c38966f0b3b875c9b7cec36e6ce83f5a2514b4e0673b92bc50892a981658e912db6bff5a2ab0891cc0753c4
7
- data.tar.gz: 13fb3b0621cd3b906156c0ef4e027c54edd20dc6b1cf98a9d34250961e45fa60ea4e89e34a5892f6892e4f5c8cdeb92138a3d1ec9caffb948a6f1afbc6179844
6
+ metadata.gz: 8677a69f2faafa802c634829f01a86cc438295f157b71da0d431fe7bc3175f22154ecd03696a6d93444df42db86bd9d459d62271f5722b0b091c10d83e2bffa8
7
+ data.tar.gz: 4caa657d528f99eb06963e028224a489ee3c9c3d44460b53336383422db86e8d3733f18c940854b75307cdf57ccb2a343a09d93d67084007332c02b5656fae27
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ### 0.2.0 / 2017-04-19
2
+
3
+ * many documentation improvements
4
+ * adding support for creating entity groups through either a parent or parent_key_id
5
+ * example Rails 5 app
6
+
1
7
  ### 0.1.0 / 2017-03-27
2
8
 
3
9
  Initial release.
data/README.md CHANGED
@@ -10,6 +10,8 @@ with Rails?
10
10
  When you want a Rails app backed by a managed, massively-scalable datastore solution. Cloud Datastore
11
11
  automatically handles sharding and replication, providing you with a highly available and durable
12
12
  database that scales automatically to handle your applications' load.
13
+
14
+ [![Gem Version](https://badge.fury.io/rb/activemodel-datastore.svg)](https://badge.fury.io/rb/activemodel-datastore)
13
15
 
14
16
  ## Table of contents
15
17
 
@@ -17,10 +19,13 @@ database that scales automatically to handle your applications' load.
17
19
  - [Model Example](#model)
18
20
  - [Controller Example](#controller)
19
21
  - [Retrieving Entities](#queries)
22
+ - [Datastore Consistency](#consistency)
23
+ - [Datastore Indexes](#indexes)
24
+ - [Datastore Emulator](#emulator)
20
25
  - [Example Rails App](#rails)
21
- - [Development and Test](#development)
26
+ - [Track Changes](#track_changes)
22
27
  - [Nested Forms](#nested)
23
- - [Work In Progress](#wip)
28
+ - [Datastore Gotchas](#gotchas)
24
29
 
25
30
  ## <a name="setup"></a>Setup
26
31
 
@@ -30,15 +35,22 @@ Generate your Rails app without ActiveRecord:
30
35
  rails new my_app -O
31
36
  ```
32
37
 
38
+ You can remove the db/ directory as it won't be needed.
39
+
33
40
  To install, add this line to your `Gemfile` and run `bundle install`:
34
41
 
35
42
  ```ruby
36
43
  gem 'activemodel-datastore'
37
44
  ```
38
45
 
39
- Google Cloud requires a Project ID and Service Account Credentials to connect to the Datastore API.
46
+ Create a Google Cloud account [here](https://cloud.google.com) and create a project.
47
+
48
+ Google Cloud requires the Project ID and Service Account Credentials to connect to the Datastore API.
40
49
 
41
- *Follow the [activation instructions](https://cloud.google.com/datastore/docs/activate) to use the Google Cloud Datastore API.*
50
+ *Follow the [activation instructions](https://cloud.google.com/datastore/docs/activate) to enable the
51
+ Google Cloud Datastore API. When running on Google Cloud Platform environments the Service Account
52
+ credentials will be discovered automatically. When running on other environments (such as AWS or Heroku)
53
+ you need to create a service account with the role of editor and generate json credentials.*
42
54
 
43
55
  Set your project id in an `ENV` variable named `GCLOUD_PROJECT`.
44
56
 
@@ -48,9 +60,10 @@ To locate your project ID:
48
60
  2. From the projects list, select the name of your project.
49
61
  3. On the left, click Dashboard. The project name and ID are displayed in the Dashboard.
50
62
 
51
- When running on Google Cloud Platform environments the Service Account credentials will be discovered automatically.
52
- When running on other environments (such as AWS or Heroku), the Service Account credentials need to be
53
- specified in two additional `ENV` variables named `SERVICE_ACCOUNT_CLIENT_EMAIL` and `SERVICE_ACCOUNT_PRIVATE_KEY`.
63
+ If you have an external application running on a platform outside of Google Cloud you also need to
64
+ provide the Service Account credentials. They are specified in two additional `ENV` variables named
65
+ `SERVICE_ACCOUNT_CLIENT_EMAIL` and `SERVICE_ACCOUNT_PRIVATE_KEY`. The values for these two `ENV`
66
+ variables will be in the downloaded service account json credentials file.
54
67
 
55
68
  ```bash
56
69
  SERVICE_ACCOUNT_PRIVATE_KEY = -----BEGIN PRIVATE KEY-----\nMIIFfb3...5dmFtABy\n-----END PRIVATE KEY-----\n
@@ -58,6 +71,11 @@ SERVICE_ACCOUNT_CLIENT_EMAIL = web-app@app-name.iam.gserviceaccount.com
58
71
  ```
59
72
 
60
73
  On Heroku the `ENV` variables can be set under 'Settings' -> 'Config Variables'.
74
+
75
+ Active Model Datastore will then handle the authentication for you, and the datastore instance can
76
+ be accessed with `CloudDatastore.dataset`.
77
+
78
+ There is an example Puma config file [here](https://github.com/Agrimatics/activemodel-datastore/blob/master/test/support/datastore_example_rails_app/config/puma.rb).
61
79
 
62
80
  ## <a name="model"></a>Model Example
63
81
 
@@ -67,21 +85,71 @@ Let's start by implementing the model:
67
85
  class User
68
86
  include ActiveModel::Datastore
69
87
 
70
- attr_accessor :email, :name, :enabled, :state
88
+ attr_accessor :email, :enabled, :name, :role, :state
89
+
90
+ def entity_properties
91
+ %w[email enabled name role]
92
+ end
93
+ end
94
+ ```
95
+
96
+ Data objects in Cloud Datastore are known as entities. Entities are of a kind. An entity has one
97
+ or more named properties, each of which can have one or more values. Think of them like this:
98
+ * 'Kind' (which is your table and the name of your Rails model)
99
+ * 'Entity' (which is the record from the table)
100
+ * 'Property' (which is the attribute of the record)
101
+
102
+ The `entity_properties` method defines an Array of properties that belong to the entity in cloud
103
+ datastore. Define the attributes of your model using `attr_accessor`. With this approach, Rails
104
+ deals solely with ActiveModel objects. The objects are converted to/from entities automatically
105
+ during save/query operations. You can still use virtual attributes on the model (such as the
106
+ `:state` attribute above) by simply excluding it from `entity_properties`. In this example state
107
+ is available to the model but won't be persisted with the entity in datastore.
108
+
109
+ Validations work as you would expect:
110
+
111
+ ```ruby
112
+ class User
113
+ include ActiveModel::Datastore
114
+
115
+ attr_accessor :email, :enabled, :name, :role, :state
116
+
117
+ validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i }
118
+ validates :name, presence: true, length: { maximum: 30 }
119
+
120
+ def entity_properties
121
+ %w[email enabled name role]
122
+ end
123
+ end
124
+ ```
125
+
126
+ Callbacks work as you would expect. We have also added the ability to set default values through
127
+ [`default_property_value`](http://www.rubydoc.info/gems/activemodel-datastore/ActiveModel%2FDatastore:default_property_value)
128
+ and type cast the format of values through [`format_property_value`](http://www.rubydoc.info/gems/activemodel-datastore/ActiveModel%2FDatastore:format_property_value):
129
+
130
+ ```ruby
131
+ class User
132
+ include ActiveModel::Datastore
133
+
134
+ attr_accessor :email, :enabled, :name, :role, :state
71
135
 
72
136
  before_validation :set_default_values
137
+ after_validation :format_values
138
+
73
139
  before_save { puts '** something can happen before save **'}
74
140
  after_save { puts '** something can happen after save **'}
75
141
 
76
142
  validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i }
77
143
  validates :name, presence: true, length: { maximum: 30 }
144
+ validates :role, presence: true
78
145
 
79
146
  def entity_properties
80
- %w[email name enabled]
147
+ %w[email enabled name role]
81
148
  end
82
149
 
83
150
  def set_default_values
84
151
  default_property_value :enabled, true
152
+ default_property_value :role, 1
85
153
  end
86
154
 
87
155
  def format_values
@@ -90,21 +158,6 @@ class User
90
158
  end
91
159
  ```
92
160
 
93
- Using `attr_accessor` the attributes of the model are defined. Validations and Callbacks all work
94
- as you would expect. However, `entity_properties` is new. Data objects in Cloud Datastore
95
- are known as entities. Entities are of a kind. An entity has one or more named properties, each
96
- of which can have one or more values. Think of them like this:
97
- * 'Kind' (which is your table)
98
- * 'Entity' (which is the record from the table)
99
- * 'Property' (which is the attribute of the record)
100
-
101
- The `entity_properties` method defines an Array of the properties that belong to the entity in
102
- cloud datastore. With this approach, Rails deals solely with ActiveModel objects. The objects are
103
- converted to/from entities as needed during save/query operations.
104
-
105
- We have also added the ability to set default property values and type cast the format of values
106
- for entities.
107
-
108
161
  ## <a name="controller"></a>Controller Example
109
162
 
110
163
  Now on to the controller! A scaffold generated controller works out of the box:
@@ -169,6 +222,14 @@ end
169
222
 
170
223
  ## <a name="queries"></a>Retrieving Entities
171
224
 
225
+ Each entity in Cloud Datastore has a key that uniquely identifies it. The key consists of the
226
+ following components:
227
+
228
+ * the kind of the entity, which is User in these examples
229
+ * an identifier for the individual entity, which can be either a a key name string or an integer numeric ID
230
+ * an optional ancestor path locating the entity within the Cloud Datastore hierarchy
231
+
232
+ #### [all(options = {})](http://www.rubydoc.info/gems/activemodel-datastore/ActiveModel%2FDatastore%2FClassMethods:all)
172
233
  Queries entities using the provided options. When a limit option is provided queries up to the limit
173
234
  and returns results with a cursor.
174
235
  ```ruby
@@ -194,8 +255,9 @@ users, cursor = User.all(limit: 7)
194
255
  # @option options [Array] :where Adds a property filter of arrays in the format[name, operator, value].
195
256
  ```
196
257
 
258
+ #### [find(*ids, parent: nil)](http://www.rubydoc.info/gems/activemodel-datastore/ActiveModel%2FDatastore%2FClassMethods:find)
197
259
  Find entity by id - this can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
198
- The parent key is optional.
260
+ The parent key is optional. This method is a lookup by key and results will be strongly consistent.
199
261
  ```ruby
200
262
  user = User.find(1)
201
263
 
@@ -205,18 +267,161 @@ user = User.find(1, parent: parent)
205
267
  users = User.find(1, 2, 3)
206
268
  ```
207
269
 
208
- Finds the first entity matching the specified condition.
270
+ #### [find_by(args)](http://www.rubydoc.info/gems/activemodel-datastore/ActiveModel%2FDatastore%2FClassMethods:find_by)
271
+ Queries for the first entity matching the specified condition.
209
272
  ```ruby
210
273
  user = User.find_by(name: 'Joe')
211
274
 
212
275
  user = User.find_by(name: 'Bryce', ancestor: parent)
213
276
  ```
214
277
 
215
- ## <a name="rails"></a>Example Rails App
278
+ Cloud Datastore has documentation on how [Datastore Queries](https://cloud.google.com/datastore/docs/concepts/queries#datastore-basic-query-ruby)
279
+ work, and pay special attention to the the [restrictions](https://cloud.google.com/datastore/docs/concepts/queries#restrictions_on_queries).
280
+
281
+ ## <a name="consistency"></a>Datastore Consistency
282
+
283
+ Cloud Datastore is a non-relational databases, or NoSQL database. It distributes data over many
284
+ machines and uses synchronous replication over a wide geographic area. Because of this architecture
285
+ it offers a balance of strong and eventual consistency.
286
+
287
+ What is eventual consistency?
288
+
289
+ It means that an updated entity value may not be immediately visible when executing a query.
290
+ Eventual consistency is a theoretical guarantee that, provided no new updates to an entity are made,
291
+ all reads of the entity will eventually return the last updated value.
292
+
293
+ In the context of a Rails app, there are times that eventual consistency is not ideal. For example,
294
+ let's say you create a user entity with a key that looks like this:
295
+
296
+ `@key=#<Google::Cloud::Datastore::Key @kind="User", @id=1>`
297
+
298
+ and then immediately redirect to the index view of users. There is a good chance that your new user
299
+ is not yet visible in the list. If you perform a refresh on the index view a second or two later
300
+ the user will appear.
301
+
302
+ "Wait a minute!" you say. "This is crap!" you say. Fear not! We can make the query of users strongly
303
+ consistent. We just need to use entity groups and ancestor queries. An entity group is a hierarchy
304
+ formed by a root entity and its children. To create an entity group, you specify an ancestor path
305
+ for the entity which is a parent key as part of the child key.
216
306
 
217
- There is an example Rails 5 app in the test directory [here](https://github.com/Agrimatics/activemodel-datastore/tree/master/test/support/datastore_example_rails_app)
307
+ Before using the `save` method, assign the `parent_key_id` attribute an ID. Let's say that 12345
308
+ represents the ID of the company that the users belong to. The key of the user entity will now
309
+ look like this:
218
310
 
219
- ## <a name="development"></a>Development and Test
311
+ `@key=#<Google::Cloud::Datastore::Key @kind="User", @id=1, @parent=#<Google::Cloud::Datastore::Key @kind="ParentUser", @id=12345>>`
312
+
313
+ All of the User entities will now belong to an entity group named ParentUser and can be queried by the
314
+ Company ID. When we query for the users we will provide User.parent_key(12345) as the ancestor option.
315
+
316
+ *Ancestor queries are always strongly consistent.*
317
+
318
+ However, there is a small downside. Entities with the same ancestor are limited to 1 write per second.
319
+ Also, the entity group relationship cannot be changed after creating the entity (as you can't modify
320
+ an entity's key after it has been saved).
321
+
322
+ The Users controller would now look like this:
323
+
324
+ ```ruby
325
+ class UsersController < ApplicationController
326
+ before_action :set_user, only: [:show, :edit, :update, :destroy]
327
+
328
+ def index
329
+ @users = User.all(ancestor: User.parent_key(12345))
330
+ end
331
+
332
+ def show
333
+ end
334
+
335
+ def new
336
+ @user = User.new
337
+ end
338
+
339
+ def edit
340
+ end
341
+
342
+ def create
343
+ @user = User.new(user_params)
344
+ @user.parent_key_id = 12345
345
+ respond_to do |format|
346
+ if @user.save
347
+ format.html { redirect_to @user, notice: 'User was successfully created.' }
348
+ else
349
+ format.html { render :new }
350
+ end
351
+ end
352
+ end
353
+
354
+ def update
355
+ respond_to do |format|
356
+ if @user.update(user_params)
357
+ format.html { redirect_to @user, notice: 'User was successfully updated.' }
358
+ else
359
+ format.html { render :edit }
360
+ end
361
+ end
362
+ end
363
+
364
+ def destroy
365
+ @user.destroy
366
+ respond_to do |format|
367
+ format.html { redirect_to users_url, notice: 'User was successfully destroyed.' }
368
+ end
369
+ end
370
+
371
+ private
372
+
373
+ def set_user
374
+ @user = User.find(params[:id], parent: User.parent_key(12345))
375
+ end
376
+
377
+ def user_params
378
+ params.require(:user).permit(:email, :name)
379
+ end
380
+ end
381
+ ```
382
+
383
+ See here for the Cloud Datastore documentation on [Data Consistency](https://cloud.google.com/datastore/docs/concepts/structuring_for_strong_consistency).
384
+
385
+ ## <a name="indexes"></a>Datastore Indexes
386
+
387
+ Every cloud datastore query requires an index. Yes, you read that correctly. Every single one. The
388
+ indexes contain entity keys in a sequence specified by the index's properties and, optionally,
389
+ the entity's ancestors.
390
+
391
+ There are two types of indexes, *built-in* and *composite*.
392
+
393
+ #### Built-in
394
+ By default, Cloud Datastore automatically predefines an index for each property of each entity kind.
395
+ These single property indexes are suitable for simple types of queries. These indexes are free and
396
+ do not count against your index limit.
397
+
398
+ #### Composite
399
+ Composite index multiple property values per indexed entity. Composite indexes support complex
400
+ queries and are defined in an index.yaml file.
401
+
402
+ Composite indexes are required for queries of the following form:
403
+
404
+ * queries with ancestor and inequality filters
405
+ * queries with one or more inequality filters on a property and one or more equality filters on other properties
406
+ * queries with a sort order on keys in descending order
407
+ * queries with multiple sort orders
408
+ * queries with one or more filters and one or more sort orders
409
+
410
+ *NOTE*: Inequality filters are LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL.
411
+
412
+ Google has excellent doc regarding datastore indexes [here](https://cloud.google.com/datastore/docs/concepts/indexes).
413
+
414
+ The datastore emulator generates composite indexes in an index.yaml file automatically. The file
415
+ can be found in /tmp/local_datastore/WEB-INF/index.yaml. If your localhost Rails app exercises every
416
+ possible query the application will issue, using every combination of filter and sort order, the
417
+ generated entries will represent your complete set of indexes.
418
+
419
+ One thing to note is that the datastore emulator caches indexes. As you add and modify application
420
+ code you might find that the local datastore index.yaml contains indexes that are no longer needed.
421
+ In this scenario try deleting the index.yaml and restarting the emulator. Navigate through your Rails
422
+ app and the index.yaml will be built from scratch.
423
+
424
+ ## <a name="emulator"></a>Datastore Emulator
220
425
 
221
426
  Install the Google Cloud SDK.
222
427
 
@@ -250,8 +455,26 @@ To create the local test datastore execute the following from the root of the pr
250
455
 
251
456
  To start the local Cloud Datastore emulator:
252
457
 
253
- $ ./start-local-datastore.sh
458
+ $ cloud_datastore_emulator start --port=8180 tmp/local_datastore
254
459
 
460
+ ## <a name="rails"></a>Example Rails App
461
+
462
+ There is an example Rails 5 app in the test directory [here](https://github.com/Agrimatics/activemodel-datastore/tree/master/test/support/datastore_example_rails_app).
463
+
464
+ ```bash
465
+ $ bundle
466
+ $ cloud_datastore_emulator create tmp/local_datastore
467
+ $ cloud_datastore_emulator create tmp/test_datastore
468
+ $ ./start-local-datastore.sh
469
+ $ rails s
470
+ ```
471
+
472
+ Navigate to http://localhost:3000.
473
+
474
+ ## <a name="track_changes"></a>Track Changes
475
+
476
+ TODO: document the change tracking implementation.
477
+
255
478
  ## <a name="nested"></a>Nested Forms
256
479
 
257
480
  Adds support for nested attributes to ActiveModel. Heavily inspired by
@@ -260,7 +483,7 @@ Rails ActiveRecord::NestedAttributes.
260
483
  Nested attributes allow you to save attributes on associated records along with the parent.
261
484
  It's used in conjunction with fields_for to build the nested form elements.
262
485
 
263
- See Rails ActionView::Helpers::FormHelper::fields_for for more info.
486
+ See Rails [ActionView::Helpers::FormHelper::fields_for](http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-fields_for) for more info.
264
487
 
265
488
  *NOTE*: Unlike ActiveRecord, the way that the relationship is modeled between the parent and
266
489
  child is not enforced. With NoSQL the relationship could be defined by any attribute, or with
@@ -329,12 +552,9 @@ Within the parent model `valid?` will validate the parent and associated childre
329
552
  a truthy `_destroy` key, the appropriate nested_models will have `marked_for_destruction` set
330
553
  to True.
331
554
 
332
- ## <a name="wip"></a>Work In Progress
333
-
334
- TODO: document datastore eventual consistency and mitigation using ancestor queries and entity groups.
335
-
336
- TODO: document indexes.
337
-
338
- TODO: document using the datastore emulator to generate the index.yaml.
339
-
340
- TODO: document the change tracking implementation.
555
+ ## <a name="gotchas"></a>Datastore Gotchas
556
+ #### Ordering of query results is undefined when no sort order is specified.
557
+ When a query does not specify a sort order, the results are returned in the order they are retrieved.
558
+ As Cloud Datastore implementation evolves (or if a project's indexes change), this order may change.
559
+ Therefore, if your application requires its query results in a particular order, be sure to specify
560
+ that sort order explicitly in the query.
@@ -9,21 +9,25 @@
9
9
  # class User
10
10
  # include ActiveModel::Datastore
11
11
  #
12
- # attr_accessor :email, :name, :enabled, :state
12
+ # attr_accessor :email, :enabled, :name, :role, :state
13
13
  #
14
14
  # before_validation :set_default_values
15
- # before_save { puts '** something can happen before save **' }
16
- # after_save { puts '** something can happen after save **' }
15
+ # after_validation :format_values
16
+ #
17
+ # before_save { puts '** something can happen before save **'}
18
+ # after_save { puts '** something can happen after save **'}
17
19
  #
18
20
  # validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i }
19
21
  # validates :name, presence: true, length: { maximum: 30 }
22
+ # validates :role, presence: true
20
23
  #
21
24
  # def entity_properties
22
- # %w[email name enabled]
25
+ # %w[email enabled name role]
23
26
  # end
24
27
  #
25
28
  # def set_default_values
26
29
  # default_property_value :enabled, true
30
+ # default_property_value :role, 1
27
31
  # end
28
32
  #
29
33
  # def format_values
@@ -116,13 +120,20 @@ module ActiveModel::Datastore
116
120
  included do
117
121
  private_class_method :query_options, :query_sort, :query_property_filter, :find_all_entities
118
122
  define_model_callbacks :save, :update, :destroy
119
- attr_accessor :id
123
+ attr_accessor :id, :parent_key_id
120
124
  end
121
125
 
122
126
  def entity_properties
123
127
  []
124
128
  end
125
129
 
130
+ ##
131
+ # Used to determine if the ActiveModel object belongs to an entity group.
132
+ #
133
+ def parent?
134
+ parent_key_id.present?
135
+ end
136
+
126
137
  ##
127
138
  # Used by ActiveModel for determining polymorphic routing.
128
139
  #
@@ -177,11 +188,18 @@ module ActiveModel::Datastore
177
188
  ##
178
189
  # Builds the Cloud Datastore entity with attributes from the Model object.
179
190
  #
191
+ # @param [Google::Cloud::Datastore::Key] parent An optional parent Key of the entity.
192
+ #
180
193
  # @return [Entity] The updated Google::Cloud::Datastore::Entity.
181
194
  #
182
195
  def build_entity(parent = nil)
183
196
  entity = CloudDatastore.dataset.entity self.class.name, id
184
- entity.key.parent = parent if parent.present?
197
+ if parent.present?
198
+ raise ArgumentError, 'Must be a Key' unless parent.is_a? Google::Cloud::Datastore::Key
199
+ entity.key.parent = parent
200
+ elsif parent?
201
+ entity.key.parent = self.class.parent_key(parent_key_id)
202
+ end
185
203
  entity_properties.each do |attr|
186
204
  entity[attr] = instance_variable_get("@#{attr}")
187
205
  end
@@ -194,17 +212,6 @@ module ActiveModel::Datastore
194
212
 
195
213
  ##
196
214
  # For compatibility with libraries that require the bang method version (example, factory_girl).
197
- # If you require a save! method that supports parents (ancestor queries), override this method
198
- # in your own code with something like this:
199
- #
200
- # def save!
201
- # parent = nil
202
- # if account_id.present?
203
- # parent = CloudDatastore.dataset.key 'Parent' + self.class.name, account_id.to_i
204
- # end
205
- # msg = 'Failed to save the entity'
206
- # save_entity(parent) || raise(ActiveModel::Datastore::EntityNotSavedError, msg)
207
- # end
208
215
  #
209
216
  def save!
210
217
  save_entity || raise(EntityNotSavedError, 'Failed to save the entity')
@@ -222,6 +229,7 @@ module ActiveModel::Datastore
222
229
  def destroy
223
230
  run_callbacks :destroy do
224
231
  key = CloudDatastore.dataset.key self.class.name, id
232
+ key.parent = self.class.parent_key(parent_key_id) if parent?
225
233
  self.class.retry_on_exception? { CloudDatastore.dataset.delete key }
226
234
  end
227
235
  end
@@ -234,12 +242,20 @@ module ActiveModel::Datastore
234
242
  entity = build_entity(parent)
235
243
  success = self.class.retry_on_exception? { CloudDatastore.dataset.save entity }
236
244
  self.id = entity.key.id if success
245
+ self.parent_key_id = entity.key.parent.id if entity.key.parent.present?
237
246
  success
238
247
  end
239
248
  end
240
249
 
241
250
  # Methods defined here will be class methods when 'include ActiveModel::Datastore'.
242
251
  module ClassMethods
252
+ ##
253
+ # A default parent key for specifying an ancestor path and creating an entity group.
254
+ #
255
+ def parent_key(parent_id)
256
+ CloudDatastore.dataset.key('Parent' + name, parent_id.to_i)
257
+ end
258
+
243
259
  ##
244
260
  # Retrieves an entity by id or name and by an optional parent.
245
261
  #
@@ -356,7 +372,7 @@ module ActiveModel::Datastore
356
372
  #
357
373
  # @example
358
374
  # User.find_by(name: 'Joe')
359
- # User.find_by(name: 'Bryce', ancestor: parent)
375
+ # User.find_by(name: 'Bryce', ancestor: parent_key)
360
376
  #
361
377
  def find_by(args)
362
378
  query = CloudDatastore.dataset.query name
@@ -391,6 +407,7 @@ module ActiveModel::Datastore
391
407
  model_entity = new
392
408
  model_entity.id = entity.key.id unless entity.key.id.nil?
393
409
  model_entity.id = entity.key.name unless entity.key.name.nil?
410
+ model_entity.parent_key_id = entity.key.parent.id if entity.key.parent.present?
394
411
  entity.properties.to_hash.each do |name, value|
395
412
  model_entity.send "#{name}=", value if model_entity.respond_to? "#{name}="
396
413
  end
@@ -1,5 +1,5 @@
1
1
  module ActiveModel
2
2
  module Datastore
3
- VERSION = '0.1.0'
3
+ VERSION = '0.2.0'
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activemodel-datastore
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bryce McLean
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-04-10 00:00:00.000000000 Z
11
+ date: 2017-04-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel