A persistence framework for Lotus.
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 - An object defined by its identity.
- Repository - An object that mediates between the entities and the persistence layer.
- Data Mapper - A persistence mapper that keep entities independent from database details.
- Adapter – A database adapter.
- Query - An object that represents a database query.
Like all the other Lotus components, it can be used as a standalone framework or within a full Lotus application.
- Home page: http://lotusrb.org
- Mailing List: http://lotusrb.org/mailing-list
- API Doc: http://rdoc.info/gems/lotus-model
- Bugs/Issues: https://github.com/lotus/model/issues
- Support: http://stackoverflow.com/questions/tagged/lotus-ruby
- Chat: https://gitter.im/lotus/chat
Lotus::Model supports Ruby (MRI) 2+
Add this line to your application's Gemfile:
gem'lotus-model'And then execute:
$ bundle
Or install it yourself as:
$ gem install lotus-model
This class provides a DSL to configure adapter, mapping and collection.
require'lotus/model'classUserincludeLotus::Entityattributes:name,:ageendclassUserRepositoryincludeLotus::RepositoryendLotus::Model.configuredoadaptertype: :sql,uri: 'postgres://localhost/database'mappingdocollection:usersdoentityUserrepositoryUserRepositoryattribute:id,Integerattribute:name,Stringattribute:age,IntegerendendendLotus::Model.load!user=User.new(name: 'Luca',age: 32)user=UserRepository.create(user)putsuser.id# => 1u=UserRepository.find(user.id)u == user# => trueAn 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'lotus/model'classPersonincludeLotus::Entityattributes:name,:ageendWhen a class includes Lotus::Entity it receives the following interface:
#id#id=#initialize(attributes = {})
Lotus::Entity also provides the .attributes for defining attribute accessors for the given names.
If we expand the code above in pure Ruby, it would be:
classPersonattr_accessor:id,:name,:agedefinitialize(attributes={})@id,@name,@age=attributes.values_at(:id,:name,:age)endendLotus::Model ships Lotus::Entity for developers's convenience.
Lotus::Model depends on a narrow and well-defined interface for an Entity - #id, #id=, #initialize(attributes={}).
If your object implements that interface then that object can be used as an Entity in the Lotus::Model framework.
However, we suggest to implement this interface by including Lotus::Entity, in case that future versions of the framework will expand it.
See Dependency Inversion Principle for more on interfaces.
When a class extends a Lotus::Entity class, it will also inherit its mother's attributes.
require'lotus/model'classArticleincludeLotus::Entityattributes:nameendclassRareArticle < Articleattributes:priceendThat is, RareArticle's attributes carry over :name attribute from Article,
thus is :id, :name, :price.
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 includes Lotus::Repository, it will receive the following interface:
.persist(entity)– Create or update an entity.create(entity)– Create a record for the given entity.update(entity)– Update the record corresponding to the given entity.delete(entity)– Delete the record corresponding to the given entity.all- Fetch all the entities from the collection.find- Fetch an entity from the collection by its ID.first- Fetch the first entity from the collection.last- Fetch the last entity from the collection.clear- Delete all the records from the collection.query- Fabricates a query object
A collection 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.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'lotus/model'classArticleRepositoryincludeLotus::Repositorydefself.most_recent_by_author(author,limit=8)querydowhere(author_id: author.id).order(:published_at)end.limit(limit)endendThis 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.
Here is an extended example of a repository that uses the SQL adapter.
classArticleRepositoryincludeLotus::Repositorydefself.most_recent_by_author(author,limit=8)querydowhere(author_id: author.id).desc(:id).limit(limit)endenddefself.most_recent_published_by_author(author,limit=8)most_recent_by_author(author,limit).publishedenddefself.publishedquerydowhere(published: true)endenddefself.draftsexcludepublishedenddefself.rankpublished.desc(:comments_count)enddefself.best_article_everrank.limit(1)enddefself.comments_averagequery.average(:comments_count)endendYour models and repositories have to be in the same namespace. Otherwise Lotus::Model::Mapper#load!
will not initialize your repositories correctly.
classMyLotusApp::Model::UserincludeLotus::Entity# your code hereend# This repository will work...classMyLotusApp::Model::UserRepositoryincludeLotus::Repository# your code hereend# ...this will not!classMyLotusApp::Repository::UserRepositoryincludeLotus::Repository# your code hereendA persistence mapper that keeps entities independent from database details. It is database independent, it can work with SQL, document, and even with key/value stores.
The role of a data mapper is to translate database columns into the corresponding attribute of an entity.
require'lotus/model'mapper=Lotus::Model::Mapper.newdocollection:usersdoentityUserattribute:id,Integerattribute:name,Stringattribute:age,IntegerendendFor simplicity's sake, imagine that the mapper above is used with a SQL database.
We use #collection to indicate the name of the table that we want to map, #entity to indicate the class that we want to associate.
In the end, each call to #attribute associates the specified column with a corresponding Ruby type.
For advanced mapping and legacy databases, please have a look at the API doc.
Known limitations
Note there are limitations with inherited entities:
require'lotus/model'classArticleincludeLotus::Entityattributes:nameendclassRareArticle < Articleattributes:priceendmapper=Lotus::Model::Mapper.newdocollection:articlesdoentityArticleattribute:id,Integerattribute:name,Stringattribute:price,IntegerendendIn the example above, there are a few problems:
Articlecould not be fetched because mapping could not mapprice.- Finding a persisted
RareArticlerecord, for eg.ArticleRepository.find(123), the result is anArticlenotRareArticle.
An adapter is a concrete implementation of persistence logic for a specific database. Lotus::Model is shipped with three adapters:
- SqlAdapter
- MemoryAdapter
- FileSystemAdapter
An adapter can be associated with one or multiple repositories.
require'pg'require'lotus/model'require'lotus/model/adapters/sql_adapter'mapper=Lotus::Model::Mapper.newdo# ...endadapter=Lotus::Model::Adapters::SqlAdapter.new(mapper,'postgres://host:port/database')PersonRepository.adapter=adapterArticleRepository.adapter=adapterIn the example above, we reuse the adapter because the target tables (people and articles) are defined in the same database.
As rule of thumb, one adapter instance per database.
An object that implements an interface for querying the database. This interface may vary, according to the adapter's specifications. Think of an adapter for Redis, it will probably employ different strategies to filter records than an SQL query object.
- A repository must be named after an entity, by appending
"Repository"to the entity class name (eg.Article=>ArticleRepository).
- Non-standard repository can be configured for an entity, by setting
repositoryon the collection.
require'lotus/model'mapper=Lotus::Model::Mapper.newdocollection:usersdoentityUserrepositoryEmployeeRepositoryendendLotus::Model's is thread safe during the runtime, but it isn't during the loading process. The mapper compiles some code internally, be sure to safely load it before your application starts.
Mutex.new.synchronizedoLotus::Model.load!endThis is not necessary, when Lotus::Model is used within a Lotus application.
If an entity has the following accessors: :created_at and :updated_at, they will be automatically updated when the entity is persisted.
require'lotus/model'classUserincludeLotus::Entityattributes:name,:created_at,:updated_atendclassUserRepositoryincludeLotus::RepositoryendLotus::Model.configuredoadaptertype: :memory,uri: 'memory://localhost/timestamps'mappingdocollection:usersdoentityUserrepositoryUserRepositoryattribute:id,Integerattribute:name,Stringattribute:created_at,DateTimeattribute:updated_at,DateTimeendendend.load!user=User.new(name: 'L')putsuser.created_at# => nilputsuser.updated_at# => niluser=UserRepository.create(user)putsuser.created_at.to_s# => "2015-05-15T10:12:20+00:00"putsuser.updated_at.to_s# => "2015-05-15T10:12:20+00:00"sleep3user.name="Luca"user=UserRepository.update(user)putsuser.created_at.to_s# => "2015-05-15T10:12:20+00:00"putsuser.updated_at.to_s# => "2015-05-15T10:12:23+00:00"Entities are able to track changes of their data, if Lotus::Entity::DirtyTracking is included.
require'lotus/model'classUserincludeLotus::EntityincludeLotus::Entity::DirtyTrackingattributes:name,:ageendclassUserRepositoryincludeLotus::RepositoryendLotus::Model.configuredoadaptertype: :memory,uri: 'memory://localhost/dirty_tracking'mappingdocollection:usersdoentityUserrepositoryUserRepositoryattribute:id,Integerattribute:name,Stringattribute:age,Stringendendend.load!user=User.new(name: 'L')user.changed?# => falseuser.age=33user.changed?# => trueuser.changed_attributes# => {:age=>33}user=UserRepository.create(user)user.changed?# => falseuser.update(name: 'Luca')user.changed?# => trueuser.changed_attributes# => {:name=>"Luca"}user=UserRepository.update(user)user.changed?# => falseresult=UserRepository.find(user.id)result.changed?# => falseFor a full working example, have a look at EXAMPLE.md. Please remember that the setup code is only required for the standalone usage of Lotus::Model. A Lotus application will handle that configurations for you.
Lotus::Model uses Semantic Versioning 2.0.0
- Fork it ( https://github.com/lotus/model/fork )
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create new Pull Request
Copyright © 2014-2015 Luca Guidi – Released under MIT License
