• Stars
    star
    445
  • Rank 98,073 (Top 2 %)
  • Language
    Ruby
  • License
    MIT License
  • Created over 10 years ago
  • Updated 4 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Ruby persistence framework with entities and repositories

Hanami::Model

A persistence framework for Hanami.

It delivers a convenient public API to execute queries and commands against a database. The architecture eases keeping the business logic (entities) separated from details such as persistence or validations.

It implements the following concepts:

  • Entity - A model domain object defined by its identity.
  • Repository - An object that mediates between the entities and the persistence layer.

Like all the other Hanami components, it can be used as a standalone framework or within a full Hanami application.

Version

This branch contains the code for hanami-model 2.x.

Status

Gem Version CI Test Coverage Depfu Inline Docs

Contact

Rubies

Hanami::Model supports Ruby (MRI) 2.6+

Installation

Add this line to your application's Gemfile:

gem 'hanami-model'

And then execute:

$ bundle

Or install it yourself as:

$ gem install hanami-model

Usage

This class provides a DSL to configure the connection.

require 'hanami/model'
require 'hanami/model/sql'

class User < Hanami::Entity
end

class UserRepository < Hanami::Repository
end

Hanami::Model.configure do
  adapter :sql, 'postgres://username:password@localhost/bookshelf'
end.load!

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

puts user.id # => 1

found = repository.find(user.id)
found == user # => true

updated = repository.update(user.id, age: 34)
updated.age # => 34

repository.delete(user.id)

Concepts

Entities

A model domain object that is defined by its identity. See "Domain Driven Design" by Eric Evans.

An entity is the core of an application, where the part of the domain logic is implemented. It's a small, cohesive object that expresses coherent and meaningful behaviors.

It deals with one and only one responsibility that is pertinent to the domain of the application, without caring about details such as persistence or validations.

This simplicity of design allows developers to focus on behaviors, or message passing if you will, which is the quintessence of Object Oriented Programming.

require 'hanami/model'

class Person < Hanami::Entity
end

Repositories

An object that mediates between entities and the persistence layer. It offers a standardized API to query and execute commands on a database.

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

This architecture has several advantages:

  • Applications depend on a standard 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

  • Confines persistence logic at a low level

  • Multiple data sources can easily coexist in an application

When a class inherits from Hanami::Repository, it will receive the following interface:

  • #create(data) โ€“ Create a record for the given data (or entity)
  • #update(id, data) โ€“ Update the record corresponding to the given id by setting the given data (or entity)
  • #delete(id) โ€“ Delete the record corresponding to the given id
  • #all - Fetch all the entities from the relation
  • #find - Fetch an entity from the relation by primary key
  • #first - Fetch the first entity from the relation
  • #last - Fetch the last entity from the relation
  • #clear - Delete all the records from the relation

A relation is a homogenous set of records. It corresponds to a table for a SQL database or to a MongoDB collection.

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

Look at the following code:

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

This is bad for a variety of 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).

There is a better way:

require 'hanami/model'

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

This is a huge improvement, because:

  • 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.

Mapping

Hanami::Model can automap columns from relations and entities attributes.

When using a sql adapter, you must require hanami/model/sql before Hanami::Model.load! is called so the relations are loaded correctly.

However, there are cases where columns and attribute names do not match (mainly legacy databases).

require 'hanami/model'

class UserRepository < Hanami::Repository
  self.relation = :t_user_archive

  mapping do
    attribute :id,   from: :i_user_id
    attribute :name, from: :s_name
    attribute :age,  from: :i_age
  end
end

NOTE: This feature should be used only when automapping fails because of the naming mismatch.

Conventions

  • A repository must be named after an entity, by appending "Repository" to the entity class name (eg. Article => ArticleRepository).

Thread safety

Hanami::Model's is thread safe during the runtime, but it isn't during the loading process. The mapper compiles some code internally, so be sure to safely load it before your application starts.

Mutex.new.synchronize do
  Hanami::Model.load!
end

This is not necessary when Hanami::Model is used within a Hanami application.

Features

Timestamps

If an entity has the following accessors: :created_at and :updated_at, they will be automatically updated when the entity is persisted.

require 'hanami/model'
require 'hanami/model/sql'

class User < Hanami::Entity
end

class UserRepository < Hanami::Repository
end

Hanami::Model.configure do
  adapter :sql, 'postgresql://localhost/bookshelf'
end.load!

repository = UserRepository.new

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

puts user.created_at.to_s # => "2016-09-19 13:40:13 UTC"
puts user.updated_at.to_s # => "2016-09-19 13:40:13 UTC"

sleep 3
user = repository.update(user.id, age: 34)
puts user.created_at.to_s # => "2016-09-19 13:40:13 UTC"
puts user.updated_at.to_s # => "2016-09-19 13:40:16 UTC"

Configuration

Logging

In order to log database operations, you can configure a logger:

Hanami::Model.configure do
  # ...
  logger "log/development.log", level: :debug
end

It accepts the following arguments:

  • stream: a Ruby StringIO object - it can be $stdout or a path to file (eg. "log/development.log") - Defaults to $stdout
  • :level: logging level - it can be: :debug, :info, :warn, :error, :fatal, :unknown - Defaults to :debug
  • :formatter: logging formatter - it can be: :default or :json - Defaults to :default

Versioning

Hanami::Model uses Semantic Versioning 2.0.0

Contributing

  1. Fork it ( https://github.com/hanami/model/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Copyright

Copyright ยฉ 2014-2021 Luca Guidi โ€“ Released under MIT License

This project was formerly known as Lotus (lotus-model).

More Repositories

1

hanami

The web, with simplicity.
Ruby
6,147
star
2

router

Ruby/Rack HTTP router
Ruby
360
star
3

api

Minimal, lightweight, fastest Ruby framework for HTTP APIs.
Ruby
342
star
4

controller

Complete, fast and testable actions for Rack and Hanami
Ruby
245
star
5

validations

Validation mixin for Ruby objects
Ruby
213
star
6

utils

Ruby core extentions and class utilities for Hanami
Ruby
173
star
7

view

Views, templates and presenters for Ruby web applications
Ruby
171
star
8

hanami-2-application-template

Hanami 2 application starter template
Ruby
107
star
9

hanami.github.io

Hanami website
HTML
88
star
10

helpers

View helpers for Ruby applications
Ruby
79
star
11

guides

Hanami Guides
CSS
51
star
12

mailer

Mail for Ruby applications
Ruby
48
star
13

assets

Assets management for Ruby web applications
Ruby
47
star
14

events

[Experimental] Events framework for Hanami
Ruby
43
star
15

cli

Hanami command line
Ruby
25
star
16

bookshelf

Hanami "Getting Started" project
Ruby
21
star
17

contributors

All hanami contributors in one place
Ruby
14
star
18

reloader

Code reloading for Hanami 2
Ruby
14
star
19

ecosystem

Hanami Ecosystem
12
star
20

snippets

Learn Hanami by reading, short, curated, manually crafted code snippets.
CSS
10
star
21

webconsole

Hanami web console for development
Ruby
7
star
22

community

7
star
23

ujs

Hanami UJS
Ruby
4
star
24

devtools

Hanami development tools [internal usage]
Ruby
4
star
25

docs

Hanami API Docs
CSS
4
star
26

rspec

Hanami RSpec extensions
Ruby
3
star
27

assets-js

esbuild plugin for Hanami Assets
TypeScript
3
star
28

guides-v2

Working area for new Hanami 2.0 guides; these will be merge into the main guides repo before 2.0 release.
2
star
29

model-sql

Hanami Model SQL
Ruby
1
star
30

playbook

Ruby
1
star
31

docker-ci-images

Docker images to be used with our CI
Dockerfile
1
star