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



408
409
410
# File 'lib/hanami/repository.rb', line 408

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



241
242
243
# File 'lib/hanami/repository.rb', line 241

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



125
126
127
# File 'lib/hanami/repository.rb', line 125

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



133
134
135
# File 'lib/hanami/repository.rb', line 133

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



224
225
226
# File 'lib/hanami/repository.rb', line 224

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.

Since:

  • 0.7.0



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/hanami/repository.rb', line 201

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.

Since:

  • 0.7.0



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

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.

Since:

  • 0.7.0



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

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



289
290
291
292
293
# File 'lib/hanami/repository.rb', line 289

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



281
282
283
# File 'lib/hanami/repository.rb', line 281

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



262
263
264
# File 'lib/hanami/repository.rb', line 262

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



440
441
442
# File 'lib/hanami/repository.rb', line 440

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

#clearObject

Deletes all the records from the relation

Examples:

UserRepository.new.clear

Since:

  • 0.7.0



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

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



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

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



426
427
428
429
430
# File 'lib/hanami/repository.rb', line 426

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

#firstHanami::Entity, NilClass

Returns the first record for the relation

Examples:

UserRepository.new.first

Returns:

Since:

  • 0.7.0



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

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



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

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