Module: Guacamole::Collection::ClassMethods

Extended by:
Forwardable
Defined in:
lib/guacamole/collection.rb

Overview

The class methods added to the class via the mixin

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#connectionAshikawa::Core::VertexCollection

Note:

We're well aware that we return a Ashikawa::Core::VertecCollection here but naming it a connection. We think the name connection still fits better in this context.

The raw Collection object for this collection

You can use this method for low level communication with the collection. Details can be found in the Ashikawa::Core documentation.

Returns:

  • (Ashikawa::Core::VertexCollection)

See Also:



73
74
75
# File 'lib/guacamole/collection.rb', line 73

def connection
  @connection
end

#databaseAshikawa::Core::Database

The raw Database object that was configured

You can use this method for low level communication with the database. Details can be found in the Ashikawa::Core documentation.

Returns:

  • (Ashikawa::Core::Database)

See Also:



47
48
49
# File 'lib/guacamole/collection.rb', line 47

def database
  @database
end

#graphAshikawa::Core::Graph

The application graph to be used

This is quite important since we're using the Graph module to realize relations between models. To guarantee consistency of your data all requests must be routed through the graph module.

Returns:

  • (Ashikawa::Core::Graph)

See Also:



59
60
61
# File 'lib/guacamole/collection.rb', line 59

def graph
  @graph
end

#mapperDocumentModelMapper

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

The DocumentModelMapper for this collection

Returns:



84
85
86
# File 'lib/guacamole/collection.rb', line 84

def mapper
  @mapper
end

Instance Method Details

#allQuery

Get all Models stored in the collection

The result can be limited (and should be for most datasets) This can be done one the returned Query object. All methods of the Enumerable module and .to_a will lead to the execution of the query.

Examples:

Get all podcasts

podcasts = PodcastsCollection.all.to_a

Get the first 50 podcasts

podcasts = PodcastsCollection.all.limit(50).to_a

Returns:



281
282
283
# File 'lib/guacamole/collection.rb', line 281

def all
  Query.new(connection.query, mapper)
end

#by_aql(aql_fragment, bind_parameters = {}, options = {}) ⇒ Query

Note:

Please use always bind parameters since they provide at least some form of protection from AQL injection.

Find models with simple AQL queries

Since Simple Queries are quite limited in their possibilities you will need to use AQL for more advanced data retrieval. Currently there is only a very basic support for AQL. Nevertheless it will allow to use any arbitrary query you want.

Parameters:

  • aql_fragment (String)

    An AQL string that will will be put between the FOR x IN coll and the RETURN x part.

  • bind_parameters (Hash<Symbol, String>) (defaults to: {})

    The parameters to be passed into the query

  • options (Hash) (defaults to: {})

    Additional options for the query execution

Options Hash (options):

  • :return_as (String) — default: 'RETURN #{model_name}'

    A custom RETURN statement

  • :mapping (Boolean) — default: true

    Should the mapping be performed?

Returns:

See Also:



262
263
264
265
266
267
# File 'lib/guacamole/collection.rb', line 262

def by_aql(aql_fragment, bind_parameters = {}, options = {})
  query                 = AqlQuery.new(self, mapper, options)
  query.aql_fragment    = aql_fragment
  query.bind_parameters = bind_parameters
  query
end

#by_example(example) ⇒ Query

Find models by the provided attributes

Search for models in the collection where the attributes are equal to those that you provided. This returns a Query object, where you can provide additional information like limiting the results. See the documentation of Query or the examples for more information. All methods of the Enumerable module and .to_a will lead to the execution of the query.

Examples:

Get all podcasts with the title 'Best Podcast'

podcasts = PodcastsCollection.by_example(title: 'Best Podcast').to_a

Get the second batch of podcasts for batches of 10 with the title 'Best Podcast'

podcasts = PodcastsCollection.by_example(title: 'Best Podcast').skip(10).limit(10).to_a

Iterate over all podcasts with the title 'Best Podcasts'

PodcastsCollection.by_example(title: 'Best Podcast').each do |podcast|
  p podcast
end

Parameters:

  • example (Hash)

    The attributes and their values

Returns:



240
241
242
243
244
# File 'lib/guacamole/collection.rb', line 240

def by_example(example)
  query = all
  query.example = example
  query
end

#by_key(key) ⇒ Model

Find a model by its key

The key is the unique identifier of a document within a collection, this concept is similar to the concept of IDs in most databases.

Examples:

Find a podcast by its key

podcast = PodcastsCollection.by_key('27214247')

Parameters:

  • key (String)

Returns:

  • (Model)

    The model with the given key

Raises:

  • (Ashikawa::Core::DocumentNotFoundException)


113
114
115
116
117
# File 'lib/guacamole/collection.rb', line 113

def by_key(key)
  raise Ashikawa::Core::DocumentNotFoundException unless key

  mapper.document_to_model fetch_document(key)
end

#callbacks(model) ⇒ Callbacks

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Gets the callback class for the given model class

Parameters:

  • model (Model)

    The model to look up callbacks for

Returns:

  • (Callbacks)

    An instance of the registered callback class



333
334
335
# File 'lib/guacamole/collection.rb', line 333

def callbacks(model)
  Callbacks.callbacks_for(model)
end

#collection_nameString

The name of the collection in ArangoDB

Use this method in your hand crafted AQL queries, for debugging etc.

Returns:

  • (String)

    The name



93
94
95
# File 'lib/guacamole/collection.rb', line 93

def collection_name
  @collection_name ||= name.gsub(/Collection\z/, '').underscore
end

#consistently_get_document_and_model(model_or_key) ⇒ Array<Ashikawa::Core::Document, Model>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Gets the document and model instance for either a given model or a key.

Parameters:

  • model_or_key (String, Model)

    The key of the model or a model

Returns:

  • (Array<Ashikawa::Core::Document, Model>)

    Both the document and model for the given input



190
191
192
193
194
195
196
# File 'lib/guacamole/collection.rb', line 190

def consistently_get_document_and_model(model_or_key)
  if model_or_key.respond_to?(:key)
    [fetch_document(model_or_key.key), model_or_key]
  else
    [document = fetch_document(model_or_key), mapper.document_to_model(document)]
  end
end

#create(model) ⇒ Model

Persist a model in the collection

The model will be saved in the collection. Timestamps, revision and key will be set on the model.

Examples:

Save a podcast to the database

podcast = Podcast.new(title: 'Best Show', guest: 'Dirk Breuer')
PodcastsCollection.save(podcast)
podcast.key #=> '27214247'

Parameters:

  • model (Model)

    The model to be saved

Returns:

  • (Model)

    The provided model



155
156
157
158
159
160
161
162
# File 'lib/guacamole/collection.rb', line 155

def create(model)
  return false unless model.valid?

  callbacks(model).run_callbacks :save, :create do
    create_document_from(model)
  end
  model
end

#create_document_from(model) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

TODO:

Currently we only save the associated models if those never have been persisted. In future versions we should add something like :autosave to always save associated models.

Create a document from a model



303
304
305
306
307
308
309
310
# File 'lib/guacamole/collection.rb', line 303

def create_document_from(model)
  result = with_transaction(model)

  model.key = result[model.object_id.to_s]['_key']
  model.rev = result[model.object_id.to_s]['_rev']

  model
end

#delete(model_or_key) ⇒ String

Delete a model from the database

If you provide a key, we will fetch the model first to run the :delete callbacks for that model.

Examples:

Delete a podcast by key

PodcastsCollection.delete(podcast.key)

Delete a podcast by model

PodcastsCollection.delete(podcast)

Parameters:

  • model_or_key (String, Model)

    The key of the model or a model

Returns:

  • (String)

    The key



175
176
177
178
179
180
181
182
183
# File 'lib/guacamole/collection.rb', line 175

def delete(model_or_key)
  document, model = consistently_get_document_and_model(model_or_key)

  callbacks(model).run_callbacks :delete do
    document.delete
  end

  model.key
end

#map(&block) ⇒ Object

Specify details on the mapping

The method is called with a block where you can specify details about the way that the data from the database is mapped to models.

See DocumentModelMapper for details on how to configure the mapper.



293
294
295
# File 'lib/guacamole/collection.rb', line 293

def map(&block)
  mapper.instance_eval(&block)
end

#model_classClass

The class of the resulting models

Returns:

  • (Class)

    The model class



100
101
102
# File 'lib/guacamole/collection.rb', line 100

def model_class
  @model_class ||= collection_name.singularize.camelcase.constantize
end

#model_to_document(model) ⇒ Ashikawa::Core::Document

Convert a model to a document to save it to the database

You can use this method for your hand made storage or update methods. Most of the time it makes more sense to call save or update though, they do the conversion and handle the communication with the database

Parameters:

  • model (Model)

    The model to be converted

Returns:

  • (Ashikawa::Core::Document)

    The converted document



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
# File 'lib/guacamole/collection.rb', line 33

module ClassMethods
  extend Forwardable
  def_delegators :mapper, :model_to_document
  def_delegator :connection, :fetch, :fetch_document

  attr_accessor :connection, :mapper, :database, :graph

  # The raw `Database` object that was configured
  #
  # You can use this method for low level communication with the database.
  # Details can be found in the Ashikawa::Core documentation.
  #
  # @see http://rubydoc.info/gems/ashikawa-core/Ashikawa/Core/Database
  # @return [Ashikawa::Core::Database]
  def database
    @database ||= Guacamole.configuration.database
  end

  # The application graph to be used
  #
  # This is quite important since we're using the Graph module to realize relations
  # between models. To guarantee consistency of your data all requests must be routed
  # through the graph module.
  #
  # @see http://rubydoc.info/gems/ashikawa-core/Ashikawa/Core/Graph
  # @return [Ashikawa::Core::Graph]
  def graph
    @graph ||= Guacamole.configuration.graph
  end

  # The raw `Collection` object for this collection
  #
  # You can use this method for low level communication with the collection.
  # Details can be found in the Ashikawa::Core documentation.
  #
  # @note We're well aware that we return a Ashikawa::Core::VertecCollection here
  #       but naming it a connection. We think the name `connection` still
  #       fits better in this context.
  # @see http://rubydoc.info/gems/ashikawa-core/Ashikawa/Core/VertexCollection
  # @return [Ashikawa::Core::VertexCollection]
  def connection
    # FIXME: This is a workaround for a bug in Ashikawa::Core (https://github.com/triAGENS/ashikawa-core/issues/139)
    @connection ||= graph.add_vertex_collection(collection_name)
  rescue
    @connection ||= graph.vertex_collection(collection_name)
  end

  # The DocumentModelMapper for this collection
  #
  # @api private
  # @return [DocumentModelMapper]
  def mapper
    @mapper ||= Guacamole.configuration.default_mapper.new(model_class)
  end

  # The name of the collection in ArangoDB
  #
  # Use this method in your hand crafted AQL queries, for debugging etc.
  #
  # @return [String] The name
  def collection_name
    @collection_name ||= name.gsub(/Collection\z/, '').underscore
  end

  # The class of the resulting models
  #
  # @return [Class] The model class
  def model_class
    @model_class ||= collection_name.singularize.camelcase.constantize
  end

  # Find a model by its key
  #
  # The key is the unique identifier of a document within a collection,
  # this concept is similar to the concept of IDs in most databases.
  #
  # @param [String] key
  # @return [Model] The model with the given key
  # @example Find a podcast by its key
  #   podcast = PodcastsCollection.by_key('27214247')
  def by_key(key)
    raise Ashikawa::Core::DocumentNotFoundException unless key

    mapper.document_to_model fetch_document(key)
  end

  # Persist a model in the collection or update it in the database, depending if it is already persisted
  #
  # * If {Model#persisted? model#persisted?} is `false`, the model will be saved in the collection.
  #   Timestamps, revision and key will be set on the model.
  # * If {Model#persisted? model#persisted?} is `true`, it replaces the currently saved version of the model with
  #   its new version. It searches for the entry in the database
  #   by key. This will change the updated_at timestamp and revision
  #   of the provided model.
  #
  # See also {#create create} and {#update update} for explicit usage.
  #
  # @param [Model] model The model to be saved
  # @return [Model] The provided model
  # @example Save a podcast to the database
  #   podcast = Podcast.new(title: 'Best Show', guest: 'Dirk Breuer')
  #   PodcastsCollection.save(podcast)
  #   podcast.key #=> '27214247'
  # @example Get a podcast, update its title, update it
  #   podcast = PodcastsCollection.by_key('27214247')
  #   podcast.title = 'Even better'
  #   PodcastsCollection.save(podcast)
  def save(model)
    model.persisted? ? update(model) : create(model)
  end

  # Persist a model in the collection
  #
  # The model will be saved in the collection. Timestamps, revision
  # and key will be set on the model.
  #
  # @param [Model] model The model to be saved
  # @return [Model] The provided model
  # @example Save a podcast to the database
  #   podcast = Podcast.new(title: 'Best Show', guest: 'Dirk Breuer')
  #   PodcastsCollection.save(podcast)
  #   podcast.key #=> '27214247'
  def create(model)
    return false unless model.valid?

    callbacks(model).run_callbacks :save, :create do
      create_document_from(model)
    end
    model
  end

  # Delete a model from the database
  #
  # If you provide a key, we will fetch the model first to run the `:delete`
  # callbacks for that model.
  #
  # @param [String, Model] model_or_key The key of the model or a model
  # @return [String] The key
  # @example Delete a podcast by key
  #   PodcastsCollection.delete(podcast.key)
  # @example Delete a podcast by model
  #   PodcastsCollection.delete(podcast)
  def delete(model_or_key)
    document, model = consistently_get_document_and_model(model_or_key)

    callbacks(model).run_callbacks :delete do
      document.delete
    end

    model.key
  end

  # Gets the document **and** model instance for either a given model or a key.
  #
  # @api private
  # @param [String, Model] model_or_key The key of the model or a model
  # @return [Array<Ashikawa::Core::Document, Model>] Both the document and model for the given input
  def consistently_get_document_and_model(model_or_key)
    if model_or_key.respond_to?(:key)
      [fetch_document(model_or_key.key), model_or_key]
    else
      [document = fetch_document(model_or_key), mapper.document_to_model(document)]
    end
  end

  # Update a model in the database with its new version
  #
  # Updates the currently saved version of the model with
  # its new version. It searches for the entry in the database
  # by key. This will change the updated_at timestamp and revision
  # of the provided model.
  #
  # @param [Model] model The model to be updated
  # @return [Model] The model
  # @example Get a podcast, update its title, update it
  #   podcast = PodcastsCollection.by_key('27214247')
  #   podcast.title = 'Even better'
  #   PodcastsCollection.update(podcast)
  def update(model)
    return false unless model.valid?

    callbacks(model).run_callbacks :save, :update do
      replace_document_from(model)
    end
    model
  end

  # Find models by the provided attributes
  #
  # Search for models in the collection where the attributes are equal
  # to those that you provided.
  # This returns a Query object, where you can provide additional information
  # like limiting the results. See the documentation of Query or the examples
  # for more information.
  # All methods of the Enumerable module and `.to_a` will lead to the execution
  # of the query.
  #
  # @param [Hash] example The attributes and their values
  # @return [Query]
  # @example Get all podcasts with the title 'Best Podcast'
  #   podcasts = PodcastsCollection.by_example(title: 'Best Podcast').to_a
  # @example Get the second batch of podcasts for batches of 10 with the title 'Best Podcast'
  #   podcasts = PodcastsCollection.by_example(title: 'Best Podcast').skip(10).limit(10).to_a
  # @example Iterate over all podcasts with the title 'Best Podcasts'
  #   PodcastsCollection.by_example(title: 'Best Podcast').each do |podcast|
  #     p podcast
  #   end
  def by_example(example)
    query = all
    query.example = example
    query
  end

  # Find models with simple AQL queries
  #
  # Since Simple Queries are quite limited in their possibilities you will need to
  # use AQL for more advanced data retrieval. Currently there is only a very basic
  # support for AQL. Nevertheless it will allow to use any arbitrary query you want.
  #
  # @param [String] aql_fragment An AQL string that will will be put between the
  #                 `FOR x IN coll` and the `RETURN x` part.
  # @param [Hash<Symbol, String>] bind_parameters The parameters to be passed into the query
  # @param [Hash] options Additional options for the query execution
  # @option options [String] :return_as ('RETURN #{model_name}') A custom `RETURN` statement
  # @option options [Boolean] :mapping (true) Should the mapping be performed?
  # @return [Query]
  # @note Please use always bind parameters since they provide at least some form
  #       of protection from AQL injection.
  # @see https://docs.arangodb.com/Aql/README.html AQL Documentation
  def by_aql(aql_fragment, bind_parameters = {}, options = {})
    query                 = AqlQuery.new(self, mapper, options)
    query.aql_fragment    = aql_fragment
    query.bind_parameters = bind_parameters
    query
  end

  # Get all Models stored in the collection
  #
  # The result can be limited (and should be for most datasets)
  # This can be done one the returned Query object.
  # All methods of the Enumerable module and `.to_a` will lead to the execution
  # of the query.
  #
  # @return [Query]
  # @example Get all podcasts
  #   podcasts = PodcastsCollection.all.to_a
  # @example Get the first 50 podcasts
  #   podcasts = PodcastsCollection.all.limit(50).to_a
  def all
    Query.new(connection.query, mapper)
  end

  # Specify details on the mapping
  #
  # The method is called with a block where you can specify
  # details about the way that the data from the database
  # is mapped to models.
  #
  # See `DocumentModelMapper` for details on how to configure
  # the mapper.
  def map(&block)
    mapper.instance_eval(&block)
  end

  # Create a document from a model
  #
  # @api private
  # @todo Currently we only save the associated models if those never have been
  #       persisted. In future versions we should add something like `:autosave`
  #       to always save associated models.
  def create_document_from(model)
    result = with_transaction(model)

    model.key = result[model.object_id.to_s]['_key']
    model.rev = result[model.object_id.to_s]['_rev']

    model
  end

  # Replace a document in the database with this model
  #
  # @api private
  # @note This will **not** update associated models (see {#create})
  def replace_document_from(model)
    result = with_transaction(model)

    model.rev = result['_rev']

    model
  end

  def with_transaction(model)
    Transaction.run(collection: self, model: model)
  end

  # Gets the callback class for the given model class
  #
  # @api private
  # @param [Model] model The model to look up callbacks for
  # @return [Callbacks] An instance of the registered callback class
  def callbacks(model)
    Callbacks.callbacks_for(model)
  end
end

#replace_document_from(model) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Note:

This will not update associated models (see #create)

Replace a document in the database with this model



316
317
318
319
320
321
322
# File 'lib/guacamole/collection.rb', line 316

def replace_document_from(model)
  result = with_transaction(model)

  model.rev = result['_rev']

  model
end

#save(model) ⇒ Model

Persist a model in the collection or update it in the database, depending if it is already persisted

  • If model#persisted? is false, the model will be saved in the collection. Timestamps, revision and key will be set on the model.
  • If model#persisted? is true, it replaces the currently saved version of the model with its new version. It searches for the entry in the database by key. This will change the updated_at timestamp and revision of the provided model.

See also create and update for explicit usage.

Examples:

Save a podcast to the database

podcast = Podcast.new(title: 'Best Show', guest: 'Dirk Breuer')
PodcastsCollection.save(podcast)
podcast.key #=> '27214247'

Get a podcast, update its title, update it

podcast = PodcastsCollection.by_key('27214247')
podcast.title = 'Even better'
PodcastsCollection.save(podcast)

Parameters:

  • model (Model)

    The model to be saved

Returns:

  • (Model)

    The provided model



140
141
142
# File 'lib/guacamole/collection.rb', line 140

def save(model)
  model.persisted? ? update(model) : create(model)
end

#update(model) ⇒ Model

Update a model in the database with its new version

Updates the currently saved version of the model with its new version. It searches for the entry in the database by key. This will change the updated_at timestamp and revision of the provided model.

Examples:

Get a podcast, update its title, update it

podcast = PodcastsCollection.by_key('27214247')
podcast.title = 'Even better'
PodcastsCollection.update(podcast)

Parameters:

  • model (Model)

    The model to be updated

Returns:



211
212
213
214
215
216
217
218
# File 'lib/guacamole/collection.rb', line 211

def update(model)
  return false unless model.valid?

  callbacks(model).run_callbacks :save, :update do
    replace_document_from(model)
  end
  model
end

#with_transaction(model) ⇒ Object



324
325
326
# File 'lib/guacamole/collection.rb', line 324

def with_transaction(model)
  Transaction.run(collection: self, model: model)
end