リポジトリ

エンティティとパーシスタンス層を仲介するオブジェクト。これは、データベース上でコマンドを照会および実行するための標準化されたAPIを提供します。

リポジトリはストレージに依存しないため、すべてのクエリとコマンドは現在のアダプタに委譲されます。

このアーキテクチャにはいくつかの利点があります。

  • アプリケーションは、低レベルの詳細ではなく標準APIに依存しています(Dependency Inversion原則)
  • アプリケーションは、ストレージが変更されても変更されない安定したAPIに依存します
  • 開発者はストレージの決定を延期できます
  • 永続性ロジックを低レベルで閉じ込める
  • 複数のデータソースをアプリケーション内に簡単に共存させることができます。

現在のバージョンでは、HanamiはSQLデータベースのみをサポートしています。

 

インタフェース

クラスが継承Hanami::Repositoryすると、次のインタフェースを受け取ります。

  • #create(data) – 与えられたデータのレコードを作成し、エンティティを返す
  • #update(id, data) – idに対応するレコードを更新し、更新されたエンティティを返す
  • #delete(id) – 指定されたエンティティに対応するレコードを削除する
  • #all コレクションからすべてのエンティティを取得する
  • #find(id) – コレクションIDからエンティティを取得します。
  • #first – コレクションから最初のエンティティを取得する
  • #last – コレクションから最後のエンティティをフェッチする
  • #clear – コレクションからすべてのレコードを削除する

コレクションは同種のレコードセットです。
これは、SQLデータベースまたはMongoDBコレクションのテーブルに対応します。

repository = BookRepository.new

book = repository.create(title: "Hanami")
  # => #<Book:0x007f95cbd8b7c0 @attributes={:id=>1, :title=>"Hanami", :created_at=>2016-11-13 16:02:37 UTC, :updated_at=>2016-11-13 16:02:37 UTC}>

book = repository.find(book.id)
  # => #<Book:0x007f95cbd5a030 @attributes={:id=>1, :title=>"Hanami", :created_at=>2016-11-13 16:02:37 UTC, :updated_at=>2016-11-13 16:02:37 UTC}>

book = repository.update(book.id, title: "Hanami Book")
  # => #<Book:0x007f95cb243408 @attributes={:id=>1, :title=>"Hanami Book", :created_at=>2016-11-13 16:02:37 UTC, :updated_at=>2016-11-13 16:03:34 UTC}>

repository.delete(book.id)

repository.find(book.id)
  # => nil

 

プライベートクエリ

すべてのクエリはプライベートです。この決定により、開発者はストレージAPIの詳細をリポジトリの外部に漏らすのではなく、意図を明らかにするAPIを定義するよう強制されます。

次のコードを見てください:

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

これはさまざまな理由で悪いことです

  • 呼び出し元は、リポジトリの内部メカニズムの詳細な知識を持っています。
  • 呼び出し元は、抽象化のいくつかのレベルで動作します。
  • それは明確な意図を表しているわけではなく、単なる一連の方法です。
  • 呼び出し元を単独で簡単にテストすることはできません。
  • ストレージを変更すると、発信者のコードを変更することが強制されます。

より良い方法があります:

# lib/bookshelf/repositories/book_repository.rb
class BookRepository < Hanami::Repository
  def most_recent_by_author(author, limit: 8)
    books
      .where(author_id: author.id)
      .order(:published_at)
      .limit(limit)
  end
end

これは大きく改善されています。理由は次のとおりです。

  • 呼び出し元は、リポジトリがどのようにエンティティをフェッチするかを知らない。
  • 呼び出し元は、抽象化の単一レベルで動作します。レコードについても知らず、エンティティでしか動作しません。
  • それは明確な意図を表しています。
  • 呼び出し元は単独で簡単にテストできます。それはちょうどこのメソッドをスタブの問題です。
  • ストレージを変更した場合、発信者は影響を受けません。

 

タイムスタンプ

プロダクションでプロジェクトを実行するときに、レコードが作成または更新された日時を記録することが重要です。

新しい表を作成するときに、次の列を追加すると、リポジトリは値を更新したままに保ちます。

Hanami::Model.migration do
  up do
    create_table :books do
      # ...
      column :created_at, DateTime
      column :updated_at, DateTime
    end
  end
end
repository = BookRepository.new

book = repository.create(title: "Hanami")

book.created_at # => 2016-11-14 08:20:44 UTC
book.updated_at # => 2016-11-14 08:20:44 UTC

book = repository.update(book.id, title: "Hanami Book")

book.created_at # => 2016-11-14 08:20:44 UTC
book.updated_at # => 2016-11-14 08:22:40 UTC

データベーステーブルがある場合created_atupdated_at、タイムスタンプ、リポジトリは自動的にそれらの値を更新します。

タイムスタンプはUTCのタイムゾーンです。

 

レガシーデータベース

デフォルトでは、リポジトリは対応するデータベーステーブルの自動マッピングを実行し、関連するエンティティの自動スキーマを作成します。

レガシーデータベースを操作する場合、リポジトリのdefaults属性とentities属性を使用して、表名、列間の命名の不一致を解決できます。

次のようなデータベーステーブルがあるとします。

CREATE TABLE t_operator (
    operator_id integer NOT NULL,
    s_name text
);

リポジトリは次のコードで設定できます:

# lib/bookshelf/repositories/operator_repository.rb
class OperatorRepository < Hanami::Repository
  self.relation = :t_operator

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

エンティティは基本設定にとどまることができますが、

# lib/bookshelf/entities/operator.rb
class Operator < Hanami::Entity
end

エンティティは、リポジトリで定義したマッピングを取得します。

operator = Operator.new(name: "Jane")
operator.name # => "Jane"

リポジトリは、同じマップされた属性を使用できます。

operator = OperatorRepository.new.create(name: "Jane")
  # => #<Operator:0x007f8e43cbcea0 @attributes={:id=>1, :name=>"Jane"}>

 

カウント

カウントは、一般にすべてのデータベースで利用可能ではない概念です。SQLデータベースはそれを持っていますが、他のデータベースはありません。

SQLデータベースを使用している場合、メソッドを定義できます。

class BookRepository < Hanami::Repository
  def count
    books.count
  end
end

特定の条件を公開することもできます。

class BookRepository < Hanami::Repository
  # ...

  def on_sale_count
    books.where(on_sale: true).count
  end
end

未処理のSQLを使用する場合は、次のようにします。

class BookRepository < Hanami::Repository
  # ...

  def old_books_count
    books.read("SELECT id FROM books WHERE created_at < (NOW() - 1 * interval '1 year')").count
  end
end