Class: Hanami::Repository

Inherits:
ROM::Repository::Root
  • Object
show all
Defined in:
lib/hanami/repository.rb

Overview

Mediates between the entities and the persistence layer, by offering an API to query and execute commands on a database.

By default, a repository is named after an entity, by appending the `Repository` suffix to the entity class name.

A repository is storage independent. All the queries and commands are delegated to the current adapter.

This architecture has several advantages:

* Applications depend on an abstract API, instead of low level details
  (Dependency Inversion principle)

* Applications depend on a stable API, that doesn't change if the
  storage changes

* Developers can postpone storage decisions

* Isolates the persistence logic at a low level

Hanami::Model is shipped with one adapter:

* SqlAdapter

All the queries and commands are private. This decision forces developers to define intention revealing API, instead of leaking storage API details outside of a repository.

Examples:

require 'hanami/model'

class Article < Hanami::Entity
end

# valid
class ArticleRepository < Hanami::Repository
end

# not valid for Article
class PostRepository < Hanami::Repository
end
require 'hanami/model'

# This is bad for several reasons:
#
#  * The caller has an intimate knowledge of the internal mechanisms
#      of the Repository.
#
#  * The caller works on several levels of abstraction.
#
#  * It doesn't express a clear intent, it's just a chain of methods.
#
#  * The caller can't be easily tested in isolation.
#
#  * If we change the storage, we are forced to change the code of the
#    caller(s).

ArticleRepository.new.where(author_id: 23).order(:published_at).limit(8)

# This is a huge improvement:
#
#  * The caller doesn't know how the repository fetches the entities.
#
#  * The caller works on a single level of abstraction.
#    It doesn't even know about records, only works with entities.
#
#  * It expresses a clear intent.
#
#  * The caller can be easily tested in isolation.
#    It's just a matter of stubbing this method.
#
#  * If we change the storage, the callers aren't affected.

ArticleRepository.new.most_recent_by_author(author)

class ArticleRepository < Hanami::Repository
  def most_recent_by_author(author, limit = 8)
    articles.
      where(author_id: author.id).
        order(:published_at).
        limit(limit)
  end
end

See Also:

Since:

  • 0.1.0

Defined Under Namespace

Modules: Commands

Constant Summary collapse

COMMAND_PLUGINS =

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

Plugins for database commands

See Also:

Since:

  • 0.7.0

%i[schema mapping timestamps].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeHanami::Repository

Initialize a new instance

Since:

  • 0.7.0


419
420
421
# File 'lib/hanami/repository.rb', line 419

def initialize
  super(self.class.container)
end

Class Method Details

.associations(&blk) ⇒ 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.

Declare associations for the repository

NOTE: This is an experimental feature

Examples:

class BookRepository < Hanami::Repository
  associations do
    has_many :books
  end
end

Since:

  • 0.7.0


248
249
250
# File 'lib/hanami/repository.rb', line 248

def self.associations(&blk)
  @associations = blk
end

.configurationObject

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.

Configuration

Since:

  • 0.7.0


123
124
125
# File 'lib/hanami/repository.rb', line 123

def self.configuration
  Hanami::Model.configuration
end

.containerObject

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.

Container

Since:

  • 0.7.0


131
132
133
# File 'lib/hanami/repository.rb', line 131

def self.container
  Hanami::Model.container
end

.define_associationsObject

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.

It defines associations, by adding relations to the repository

See Also:

Since:

  • 0.7.0


231
232
233
# File 'lib/hanami/repository.rb', line 231

def self.define_associations
  Model::Associations::Dsl.new(self, &@associations) unless @associations.nil?
end

.define_mappingObject

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.

Defines the mapping between a database table and an entity.

It's also responsible to associate table columns to entity attributes.

rubocop:disable Metrics/MethodLength rubocop:disable Metrics/AbcSize

Since:

  • 0.7.0


206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/hanami/repository.rb', line 206

def self.define_mapping
  self.entity = Utils::Class.load!(entity_name)
  e = entity
  m = @mapping

  blk = lambda do |_|
    model       e
    register_as Model::MappedRelation.mapper_name
    instance_exec(&m) unless m.nil?
  end

  root = self.root
  configuration.mappers { define(root, &blk) }
  configuration.define_mappings(root, &blk)
  configuration.register_entity(relation, entity_name.underscore, e)
end

.define_relationObject

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.

Define a database relation, which describes how data is fetched from the database.

It auto-infers the underlying database table.

rubocop:disable Metrics/MethodLength rubocop:disable Metrics/AbcSize

Since:

  • 0.7.0


172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/hanami/repository.rb', line 172

def self.define_relation
  a = @associations
  s = @schema

  configuration.relation(relation) do
    if s.nil?
      schema(infer: true) do
        associations(&a) unless a.nil?
      end
    else
      schema(&s)
    end
  end

  relations(relation)
  root(relation)
  class_eval %{
    def #{relation}
      Hanami::Model::MappedRelation.new(@#{relation})
    end
  }, __FILE__, __LINE__ - 4
end

.inherited(klass) ⇒ 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.

rubocop:disable Metrics/MethodLength rubocop:disable Metrics/AbcSize

Since:

  • 0.7.0


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
# File 'lib/hanami/repository.rb', line 307

def self.inherited(klass)
  klass.class_eval do
    include Utils::ClassAttribute
    auto_struct true

    @associations = nil
    @mapping      = nil
    @schema       = nil

    class_attribute :entity
    class_attribute :entity_name
    class_attribute :relation

    Hanami::Utils::IO.silence_warnings do
      def self.relation=(name)
        @relation = name.to_sym
      end
    end

    self.entity_name = Model::EntityName.new(name)
    self.relation    = Model::RelationName.new(name)

    commands :create, update: :by_pk, delete: :by_pk, mapper: Model::MappedRelation.mapper_name, use: COMMAND_PLUGINS
    prepend Commands
  end

  Hanami::Model.repositories << klass
end

.load!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.

Define relations, mapping and associations

Since:

  • 0.7.0


296
297
298
299
300
# File 'lib/hanami/repository.rb', line 296

def self.load!
  define_relation
  define_mapping
  define_associations
end

.mapping(&blk) ⇒ Object

Declare mapping between database columns and entity's attributes

NOTE: This should be used *only* when there is a name mismatch (eg. in legacy databases).

Examples:

class BookRepository < Hanami::Repository
  self.relation = :t_operator

  mapping do
    attribute :id,   from: :operator_id
    attribute :name, from: :s_name
  end
end

Since:

  • 0.7.0


288
289
290
# File 'lib/hanami/repository.rb', line 288

def self.mapping(&blk)
  @mapping = blk
end

.schema(&blk) ⇒ Object

Declare database schema

NOTE: This should be used *only* when Hanami can't find a corresponding Ruby type for your column.

Examples:

# In this example `name` is a PostgreSQL Enum type that we want to treat like a string.

class ColorRepository < Hanami::Repository
  schema do
    attribute :id,         Hanami::Model::Sql::Types::Int
    attribute :name,       Hanami::Model::Sql::Types::String
    attribute :created_at, Hanami::Model::Sql::Types::DateTime
    attribute :updated_at, Hanami::Model::Sql::Types::DateTime
  end
end

Since:

  • 1.0.0


269
270
271
# File 'lib/hanami/repository.rb', line 269

def self.schema(&blk)
  @schema = blk
end

Instance Method Details

#allArray<Hanami::Entity>

Return all the records for the relation

Examples:

UserRepository.new.all

Returns:

Since:

  • 0.7.0


451
452
453
# File 'lib/hanami/repository.rb', line 451

def all
  root.as(:entity).to_a
end

#clearObject

Deletes all the records from the relation

Examples:

UserRepository.new.clear

Since:

  • 0.7.0


485
486
487
# File 'lib/hanami/repository.rb', line 485

def clear
  root.delete
end

#command(*args, **opts, &block) ⇒ ROM::Command

Define a new ROM::Command while preserving the defaults used by Hanami itself.

It allows the user to define a new command to, for example, create many records at the same time and still get entities back.

The first argument is the command and relation it will operate on.

Examples:

# In this example, calling the create_many method with and array of data,
# would result in the creation of records and return an Array of Task entities.

class TaskRepository < Hanami::Repository
  def create_many(data)
    command(create: :tasks, result: :many).call(data)
  end
end

Returns:

  • (ROM::Command)

    the created command

Since:

  • 1.2.0


155
156
157
158
159
160
# File 'lib/hanami/repository.rb', line 155

def command(*args, **opts, &block)
  opts[:use] = COMMAND_PLUGINS | Array(opts[:use])
  opts[:mapper] = opts.fetch(:mapper, Model::MappedRelation.mapper_name)

  super(*args, **opts, &block)
end

#find(id) ⇒ Hanami::Entity, NilClass

Find by primary key

Examples:

repository = UserRepository.new
user       = repository.create(name: 'Luca')

user       = repository.find(user.id)

Returns:

Raises:

Since:

  • 0.7.0


437
438
439
440
441
# File 'lib/hanami/repository.rb', line 437

def find(id)
  root.by_pk(id).as(:entity).one
rescue => e
  raise Hanami::Model::Error.for(e)
end

#firstHanami::Entity, NilClass

Returns the first record for the relation

Examples:

UserRepository.new.first

Returns:

Since:

  • 0.7.0


463
464
465
# File 'lib/hanami/repository.rb', line 463

def first
  root.as(:entity).limit(1).one
end

#lastHanami::Entity, NilClass

Returns the last record for the relation

Examples:

UserRepository.new.last

Returns:

Since:

  • 0.7.0


475
476
477
# File 'lib/hanami/repository.rb', line 475

def last
  root.as(:entity).limit(1).reverse.one
end