Giter Club home page Giter Club logo

model's Introduction

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

model's People

Contributors

aderyabin avatar alfonsouceda avatar arthurgeek avatar artofhuman avatar bbonislawski avatar beauby avatar benlovell avatar brennovich avatar chongfun avatar cllns avatar davydovanton avatar dsnipe avatar erol avatar felipesere avatar flash-gordon avatar g3d avatar gotjosh avatar guilhermefranco avatar jeremyf avatar jodosha avatar linuus avatar mereghost avatar mjbellantoni avatar partyschaum avatar rhynix avatar rogeriozambon avatar sidonath avatar taylorfinnell avatar vyper avatar ziggurat avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

model's Issues

Feature comparison of Lotus::Model and Virtus

I really like what I'm seeing in Lotus::Model. Its creating further separation of concerns.

I'm wondering how Lotus::Entity relates to the very feature complete Virtus https://github.com/solnic/virtus. I've used Virtus for modeling and found it extremely robust. I then use a repository to persist the entities to their constituent tables.

I'm also curious how Lotus::Models cleaves to the Ruby Object Mapper project (http://rom-rb.org/).

Thanks for your time and publishing your thought experiment.

Better exception handling for Repository w/o an adapter

When we have the following repository:

class AccountRepository
  include Lotus::Repository
end

And we forgot to assign an adapter, but we use like this:

AccountRepository.create(account)

It raises an hard to debug error:

NoMethodError:
       undefined method `create' for nil:NilClass

It would be nice to have a more debug friendly exception.

Add method count to repository

I think it would be a nice idea add the method count to the repository, an example:

PostRespository.count # =>  10

what do you think?

Saving an entity to repository will make "false" field to nil

Saving an entity where a field is false to Repository will turn the field to nil.

Pseudo code that demonstrate the issue.

entity = Entity.new({some_boolean_field: false})
EntityRepository.create(entity)
EntityRepository.first.some_boolean_field # => nil

I also created gist that reproduces the issue, so please have a look.

SQL Joins

Add support to SQL joins to Sql::Adapter and Sql::Query.

duplicate declarations of attributes in: Migration, Entity and Mapping's

Migration has fields

db.create_table! :art do
  String  :title

Entity has fields

module One
  class Art
    include Lotus::Entity
    attributes :some,

mapping has too

collection :art do
  entity One::Art
  repository One::ArtRepository
  attribute :id, Integer

Recently watched http://datamapper.org, serialization from one place, it looking nice, but maybe has own limitations. Auto-migrate, auto-update will reduce configurations but can alter table or drop it out.

May, inherit attributes from configuration, such as migration? Include / exclude necessary for Entities and Mappers. The Entity is the pinnacle of the application, maybe here should be a declaration of attributes, this is similar to dm-core.rb

Unfortunately, now I don't have proposals or a good ideas for enterprise level solution, let's discuss

maybe I'm wrong or don't know a prehistory, anyway many thanks

Add migration mechanism

UPDATE: Please see proposed changes in my PR #144

What: Schema Migration for SQL adapter
How:

  • Wrap Sequel::Migrator vs Writing from scratch?
  • Migrator class has up, down method
  • Schema file is the source of truths, and it should not reflect local changes of DB schema.
  • There is db/schema.rb for /lib and apps/<app_name>/db/schema.rb for app
  • Stop users from abusing migration to do data migration (I know - the wording 'migration' is confusing eh?)
  • Allow migrations to be stored in 1 level deep or many level deep folder under (as like Rails)
  • Follow Sequel Migrator DSL vs Rails DSL
  • Generator is to be done in lotusrb gem, and the core classes is to be done in model gem
  • The UX for the generator:
    • lotus generate migration oh_my_god will generate migration for /lib
    • LOTUS_APP=web lotus generate migration oh_my_lord will generate migration for /apps/web
      OR
    • lotus generate migration web:oh_my_lord

Wrong inherited configuration

Reported by: @mwallasch

In Lotus Container applications we use Lotus::Model directly in lib/bookshelf.rb to setup connection params and mapping.

Imagine that we have two applications: web and admin. When we load those apps, because Lotus::Model is activated, we duplicate the framework and create Web::Model and Admin::Model, respectively.

Those duplicated frameworks inherit the wrong configuration. At this point Lotus::Model becomes a master to copy settings from. If Lotus::Model.configure connects to a Postgres database, also Web::Model and Admin::Model connect to.

This causes unwanted connections against the database.

/cc @lotus/core-team

Trouble using expressions in `where`

I have the following in my repository based on #46

class OrderRepository
  def since(date)
    query do
      where{ created_at > date }
    end
  end
end

But I'm receive the following error:

     Failure/Error: recent_orders = OrderRepository.since(yesterday)                                                                                                                                       │
     ArgumentError:                                                                                                                                                                                        │
       wrong number of arguments (0 for 1)                                                                                                                                                                 │
     # ./repositories/order_repository.rb:15:in `block in since'                                                                                                                                           │
     # ./repositories/order_repository.rb:14:in `since'                                                                                                                                                    │
     # ./spec/repositories/order_repository_spec.rb:23:in `block (3 levels) in <top (required)>'

Memory adapter does not support repository query chaining

Hi,

Due to the different implementation of the sql and memory adapters, the memory one does not support query chaining like this.

In fact, the memoy adapter's Query#method_missing does not even work properly as it was copy-pasted from the sql adapter - in the memory adapter there is no @context variable, though as demonstrated below, we could simply get the context (repository) from the collection:

diff --git a/lib/lotus/model/adapters/memory/query.rb b/lib/lotus/model/adapters/memory/query.rb
index 8c4c4ea..1141ed7 100644
--- a/lib/lotus/model/adapters/memory/query.rb
+++ b/lib/lotus/model/adapters/memory/query.rb
@@ -442,14 +442,20 @@ module Lotus

           protected
           def method_missing(m, *args, &blk)
-            if @context.respond_to?(m)
-              apply @context.public_send(m, *args, &blk)
+            if @collection.repository.respond_to?(m)
+              apply @collection.repository.public_send(m, *args, &blk)
             else
               super
             end
           end

           private
+          def apply(query)
+            dup.tap do |result|
+              result.conditions.push(*query.conditions)
+            end
+          end
+
           # Apply all the conditions and returns a filtered collection.
           #
           # This operation is idempotent, but the records are actually fetched

This solves the problem but it's been a few months since I started looking at Lotus again, I'm not sure whether this is a proper fix for it. Please discuss. :)

The assigned adapter is being obliterated by the `Mapper.load!` method.

Given the following mapping (see below)
When I call `Curus::Repository::DepositRepository.persist(deposit)`
Then I get an the following exception:
   NoMethodError: undefined method `persist' for nil:NilClass
     BUNDLE_HOME/gems/model-f8d624f65c63/lib/lotus/repository.rb:253:in `persist'

The assigned adapter is being obliterated by the Mapper.load! method.

Mapping

require 'lotus/model/mapper'
require 'curus/deposit'
require 'curus/repositories/deposit_repository'
require 'lotus/model/adapters/memory_adapter'

module Curus
  @@mapping = Lotus::Model::Mapper.new do
    collection :deposits do
      entity     Curus::Deposit
      repository Curus::Repositories::DepositRepository

      attribute :id, String
      attribute :created_at, Time
      attribute :state, String
    end
  end

  def self.mapping
    @@mapping
  end

  def self.load!
    adapter = Lotus::Model::Adapters::MemoryAdapter.new(mapping)

    # First invocation of `.adapter=` with specified adapter
    Curus::Repositories::DepositRepository.adapter = adapter

    # Second invocation of `.adapter=` with nil
    mapping.load!
  end

end

Call Trace

First invocation

BUNDLER_HOME/gems/model-f8d624f65c63/lib/lotus/repository.rb:211:in `adapter='
APP_HOME/lib/curus/mapping.rb:24:in `load!'

Second invocation

BUNDLER_HOME/gems/model-f8d624f65c63/lib/lotus/repository.rb:211:in `adapter='
BUNDLER_HOME/gems/model-f8d624f65c63/lib/lotus/model/mapping/collection.rb:364:in `configure_repository!'
BUNDLER_HOME/gems/model-f8d624f65c63/lib/lotus/model/mapping/collection.rb:352:in `load!'
BUNDLER_HOME/gems/model-f8d624f65c63/lib/lotus/model/mapper.rb:105:in `block in load!'
BUNDLER_HOME/gems/model-f8d624f65c63/lib/lotus/model/mapper.rb:103:in `each_value'
BUNDLER_HOME/gems/model-f8d624f65c63/lib/lotus/model/mapper.rb:103:in `load!'
APP_HOME/lib/curus/mapping.rb:25:in `load!'

[Deprecation] Raise Lotus::Model::EntityNotFound when an entity is not found

I have to confess that this was introduced by me because "devs will expect this behavior". But this isn't the way I'm building Lotus. First create things that are correct, and then meet expectations. So, mea culpa and let's move on.

We are using exceptions for control flow, which is a poor design.

Exceptions shouldn’t be expected

Use exceptions only for exceptional situations. [...] Exceptions are often overused. Because they distort the flow of control, they can lead to convoluted constructions that are prone to bugs. It is hardly exceptional to fail to open a file; generating an exception in this case strikes us as over-engineering.
– Brian Kernighan and Rob Pike, The Practice of Programming [1][2]

When we query the database, we expect that a record can't be found, so instead of raising an exception, I suggest to return nil.


[1] Grimm A. - Exceptional Ruby (2011) - ShipRise
[2] Kernighan B., Pike R. - The Practice of Programming (1999) - Addison-Wesley

Entity doesn't allow strings as keys in initializer attributes hash

Currently entities only allow { name: "myname" } and not { "name" => "myname" } and former.
For example an entity which looks like https://github.com/bennyklotz/timetracker-ruby/blob/master/backend/models/entities/user.rb#L24 works because an explicit initalizer is set and super called which therefore calls https://github.com/lotus/model/blob/master/lib/lotus/entity.rb#L142 where hash keys as Strings are allowed.

This leads to an inconsitency in the behaviour of Entities.
Therefore the intializer which is defined via class_eval in https://github.com/lotus/model/blob/master/lib/lotus/entity.rb#L117 should also allow Strings as hash keys.

Custom coercions

Status: In Progress
Taker: @midas

Allow the mapper to accept custom coercers.

Given the following SQL table

CREATE TABLE products (
  id      serial primary key,
  price   integer NOT NULL,
);
Money = Struct.new(:amount) do
  def to_int
    amount
  end
end

class Product
  include Lotus::Entity
  self.attributes = :price
end

Lotus::Model::Mapper do
  collection :products do
    entity Product
    attribute :id, Integer
    attribute :price, Money # this coerces the integer value to a Money instance
  end
end

class ProductRepository
  include Lotus::Repository
end

product = Product.new(money: Money.new(100))
ProductRepository.persist(product) # => price must be persisted as an integer (see the schema) 

product = ProductRepository.find(product.id)
product.price # => #<struct Money amount=100>

Should SQL Adapter and Memory Adapter methods have similar signature?

From Memory Adapter

class Lotus::Model::Adapters::Memory::Query
  def where(condition)
    column, value = _expand_condition(condition)
    conditions.push(Proc.new{ find_all{|r| r.fetch(column) == value} })
    self
  end
end

From SQL Adapter

class Lotus::Model::Adapters::Sql::Query
  def where(condition=nil, &blk)
    condition = (condition or blk or raise ArgumentError.new('You need to specify an condition.'))
    conditions.push([:where, condition])
    self
  end
end

Working with default values

Currently lotus-model doesn't respect default values in the database:

CREATE TABLE deployments (
    id integer NOT NULL,
    project_id integer NOT NULL,
    environment character varying NOT NULL,
    branch character varying NOT NULL,
    deployed_at timestamp with time zone DEFAULT now() NOT NULL
);
class Deployment
  include Lotus::Entity
  attributes :id, :project_id, :environment, :branch, :deployed_at

  attr_accessor :project
end

deployment = Deployment.new(project_id: 1, environment: 'production', branch: 'master')
DeploymentRepository.persist(deployment)
# Sequel::NotNullConstraintViolation: PG::NotNullViolation: ERROR:  null value in column "deployed_at" violates not-null constraint
# DETAIL:  Failing row contains (4, 1, production, master, null).

Seems like the empty deployed_at value is being written to the database. I see 2 possible ways to solve this issue: to read the default value into the entity upon it's creation (but I believe it breaks incapsulation and is not very helpful with a default value of NOW()), or to save only attributes that were changed.

Ruby 1.9.3 - Lotus::Model::Mapping::UnmappedCollectionError

I have a build on Travis which is using Ruby 1.9.3, 2.0.0 and 2.1.2. The code is at radar/spree_poro and is easy enough to set up. While the 2.x builds are passing, the 1.9.3 build does not. I would expect it would, given it's the same code. I am able to reproduce this issue on my local machine using Ruby 1.9.3-p545.

I would think this is due to the Mutex.new.synchronize call not operating the same way in 1.9.3 as it does in 2.x.

I do not know how to solve this.

Memory adapter accepts any condition when querying, which differs from SQL adapter

See below for an example:

require 'lotus/model'
require 'lotus/model/adapters/memory_adapter'

class User
  include Lotus::Entity
  self.attributes = :id, :name, :age
end

mapper = Lotus::Model::Mapper.new do
  collection :users do
    entity User

    attribute :id,   Integer
    attribute :name, String
    attribute :age,  Integer
  end
end.load!

adapter = Lotus::Model::Adapters::MemoryAdapter.new(mapper)

user_1 = User.new({name: 'Foo', age: 32})
user_2 = User.new({name: 'Bar', age: 32})
adapter.create(:users, user_1)
adapter.create(:users, user_2)

adapter.all(:users) #=> [#<User:0x007f6ea8436d90 @id=1, @name="Foo", @age=32>, #<User:0x007f6ea8436d18 @id=2, @name="Bar", @age=32>]
query = adapter.query(:users) { where(age: 32).where(name: 'Foo') }
query.all #=> [#<User:0x007f6ea83f9c10 @id=1, @name="Foo", @age=32>, #<User:0x007f6ea83f9b48 @id=2, @name="Bar", @age=32>]

Question about frequent use of `instance_eval` in initialize methods

In looking through the code of various Lotus repositories, I'm noticing the following pattern:

class Lotus::SomeClass
  def initialize(&blk)
    instance_eval(&blk) if block_given?
  end
end

Is there a reason to prefer instance_eval vs. yield(self)? I understand that the resulting blocks will look a little different. Are there other advantages? I am cautious about instance_eval.

A detailed dive (though a bit old) that may be applicable. http://merbist.com/2011/09/05/ruby-optimization-example-and-explaination/

Joins?

Does Lotus::Model include any support for join queries? I didn't see any evidence of joins in the SQL adapter, but I thought I'd ask to be sure.

DRY initialisation

The initialization of the framework is verbose and requires a lot of manual setup.

We can enhance those mechanisms with the following changes:

  • Make Lotus::Model to register adapters instances, and to specify a default one (see example 1).
  • Because of the previous requirement, consider to introduce a configuration and duplication mechanisms, like we do with Lotus::Controller and Lotus::View.
  • Ditch the convention of naming repositories after entities. Allow this association in mapper (see example 3).
  • Provide a single entry point to make the setup automatic.

Mapper should allow to specify adapters for a collection (see example 2).

Example 1

Lotus::Model.configure do
  adapter :sql, 'postgres://localhost/database', default: true
  adapter :redis, 'redis://localhost:6379/0'
end

The code above will try to load Lotus::Model::Adapters::SqlAdapter and Lotus::Model::Adapters::RedisAdapter.

Example 2 (out of scope for now)

Lotus::Model::Mapper.new do
  collection :articles do # use the default adapter
    # ...
  end

  adapter :redis do # use the redis adapter
    collection :comments do
      # ...
    end
  end
end

Example 3

Lotus::Model::Mapper.new do
  collection :articles do
    entity Article
    repository ArticlesRepository # talks to a local SQL database
  end

  adapter :remote_api do
    collection :articles do
      entity Article
      repository RemoteArticlesRepository # loads articles from a remote API
    end
  end
end

Memory adapter does not coerce values when matching conditions

See below for an example:

require 'lotus/model'
require 'lotus/model/adapters/memory_adapter'

class User
  include Lotus::Entity
  self.attributes = :id, :name, :age
end

mapper = Lotus::Model::Mapper.new do
  collection :users do
    entity User

    attribute :id,   Integer
    attribute :name, String
    attribute :age,  Integer
  end
end.load!

adapter = Lotus::Model::Adapters::MemoryAdapter.new(mapper)

user_1 = User.new({name: 'Foo', age: '32'})
user_2 = User.new({name: 'Bar', age: 32})
adapter.create(:users, user_1)
adapter.create(:users, user_2)

adapter.all(:users) #=> [#<User:0x007fea9f4bf320 @id=1, @name="Foo", @age=32>, #<User:0x007fea9f4bf2d0 @id=2, @name="Bar", @age=32>]
query = adapter.query(:users) { where(age: 32) }
query.all #=> [#<User:0x007fea9f486430 @id=2, @name="Bar", @age=32>]

Entity is not updated based on Repository state

I've noticed that if I persist an entity with attributes that have not been added to the schema yet and no mapper has been created for them, that the entity is not modified. I would expect it to remove those fields from the object returned from persist.

Associations

Support associations between collections.

Lotus::Model::Mapper.new do
  collection :articles do
    # ...
    association :comments, [Comment]
  end

  collection :comments do
    # ...
    association :article, Article, foreign_key: :article_id 
  end
end

The first association is 1-n, where an article has many comments. The [] syntax specifies this.

The second association is n-1, where a comment belongs to an article (lack of []). The foreign_key should be optional and only specified when the following convention isn't respected: association name + _id. In the example above: article + _id.

Associations MUST NOT be loaded by default, but they require an explicit intervention via the preload method (which should be implemented as part of this feature).

class ArticleRepository
  include Lotus::Repository

  def self.by_author(author)
    query do
      where(author_id: author.id)
    end.preload(:comments)
  end
end

IMPORTANT: Let the preload mechanisms to not work with SQL joins, but with repositories instead. This isn't efficient, but we have a gain in terms of flexibility. Imagine the the scenario where the comments above aren't stored in the local database, but fetched from a remote JSON API. Thanks to the mapper we can easily know where to fetch those comments.

Raise meaningful exception when `mapping` is missing in configuration

When we omit mapping in configuration block, at the time we invoke Lotus::Model.load! it raises a cryptic exception undefined methodload!' for nil:NilClass (NoMethodError)`.

Lotus::Model.configure do
  adapter type: :memory, uri: 'memory://localhost'

  # missing `mapping` block
end.load!
/Users/luca/.rubies/ruby-2.1.5/bin/ruby -I/Users/luca/.gem/ruby/2.1.5/gems/rspec-support-3.1.2/lib:/Users/luca/.gem/ruby/2.1.5/gems/rspec-core-3.1.7/lib /Users/luca/.gem/ruby/2.1.5/gems/rspec-core-3.1.7/exe/rspec --pattern spec/\*\*\{,/\*/\*\*\}/\*_spec.rb
/Users/luca/.gem/ruby/2.1.5/bundler/gems/model-8314fd2632b3/lib/lotus/model/configuration.rb:57:in `load!': undefined method `load!' for nil:NilClass (NoMethodError)
    from /Users/luca/.gem/ruby/2.1.5/bundler/gems/model-8314fd2632b3/lib/lotus/model.rb:76:in `load!'
    from /Users/luca/Code/chirp/lib/chirp.rb:11:in `<top (required)>'
    from /Users/luca/Code/chirp/config/environment.rb:3:in `require_relative'
    from /Users/luca/Code/chirp/config/environment.rb:3:in `<top (required)>'
    from /Users/luca/Code/chirp/spec/spec_helper.rb:7:in `require_relative'
    from /Users/luca/Code/chirp/spec/spec_helper.rb:7:in `<top (required)>'
    from /Users/luca/.rubies/ruby-2.1.5/lib/ruby/2.1.0/rubygems/core_ext/kernel_require.rb:55:in `require'
    from /Users/luca/.rubies/ruby-2.1.5/lib/ruby/2.1.0/rubygems/core_ext/kernel_require.rb:55:in `require'
    from /Users/luca/Code/chirp/spec/backend/features/visit_home_spec.rb:1:in `<top (required)>'
    from /Users/luca/.gem/ruby/2.1.5/gems/rspec-core-3.1.7/lib/rspec/core/configuration.rb:1105:in `load'
    from /Users/luca/.gem/ruby/2.1.5/gems/rspec-core-3.1.7/lib/rspec/core/configuration.rb:1105:in `block in load_spec_files'
    from /Users/luca/.gem/ruby/2.1.5/gems/rspec-core-3.1.7/lib/rspec/core/configuration.rb:1105:in `each'
    from /Users/luca/.gem/ruby/2.1.5/gems/rspec-core-3.1.7/lib/rspec/core/configuration.rb:1105:in `load_spec_files'
    from /Users/luca/.gem/ruby/2.1.5/gems/rspec-core-3.1.7/lib/rspec/core/runner.rb:96:in `setup'
    from /Users/luca/.gem/ruby/2.1.5/gems/rspec-core-3.1.7/lib/rspec/core/runner.rb:84:in `run'
    from /Users/luca/.gem/ruby/2.1.5/gems/rspec-core-3.1.7/lib/rspec/core/runner.rb:69:in `run'
    from /Users/luca/.gem/ruby/2.1.5/gems/rspec-core-3.1.7/lib/rspec/core/runner.rb:37:in `invoke'
    from /Users/luca/.gem/ruby/2.1.5/gems/rspec-core-3.1.7/exe/rspec:4:in `<main>'
/Users/luca/.rubies/ruby-2.1.5/bin/ruby -I/Users/luca/.gem/ruby/2.1.5/gems/rspec-support-3.1.2/lib:/Users/luca/.gem/ruby/2.1.5/gems/rspec-core-3.1.7/lib /Users/luca/.gem/ruby/2.1.5/gems/rspec-core-3.1.7/exe/rspec --pattern spec/\*\*\{,/\*/\*\*\}/\*_spec.rb failed

shell returned 1

two locations for mapping

Is it potentially confusing to have two places to define mappings (in a freshly generated application)?

There is one under: lib/example_app.rb and one under apps/web/config/mapping.rb

Few API questions

Hi there,

I was playing today with extracting the mapper and adapter configuration from application.rb and entitiy files to the "framework" in the room reserevation app.

So I've got few questions to help me better understand the Lotus::Model design and the Lotus direction in general.

It seems that there's no way to get to mappers' collections if you don't know the names of all collections. Please see line 91 of lotus.rb. Maybe a Mapper should have a public accessor for all collections? Or at least an accessor for all collection names defined in the mapper.

Would it make sense for a Collection to expose a repository it's tied to? Since it's already assigning itself to the repository it doesn't seem wrong for a collection to expose the repository class as well. Code to assign the adapter to the repository would get simplified a bit.

Is there a good reason why Collection#load! doesn't assign the attributes to the entity through the Entity.attributes=? I guess it's because including Lotus::Entity is optional, but the presence of the method could be checked.

[Breaking] Change Entity.attributes= to Entity.attributes

With the recent introduction of Entity's attributes inheritance, the current API looks counter-intuitive. The semantic of assignment is: "add the following set by erasing the previous".

class Book
  include Lotus::Entity
  self.attributes = :title
end

class PricedBook < Book
  include Lotus::Entity
  self.attributes = :price
end

Book.attributes # => [:title]
PricedBook.attributes # => [:title, :price]

For some developers it may be unexpected to still see :title.
Look at the following Ruby code:

attributes = [:title]
puts attributes # => [:title]

attributes = [:price]
puts attributes # => [:price]

It erases the previous assigment, and replace it with the new value.
What I'm suggesting is to add something different.

class Book
  include Lotus::Entity
  attributes :title
end

class PricedBook < Book
  include Lotus::Entity
  attributes :price
end

Book.attributes # => [:title]
PricedBook.attributes # => [:title, :price]

Which should recall the following semantic:

attributes = [:title]
puts attributes # => [:title]

attributes.push :price
puts attributes # => [:title, :price]

Adapter interchangeability

Currently, the memory adapter and SQL adapter behave in slightly different ways. The memory adapter will accept any condition specified by where (see https://github.com/lotus/model/blob/56eb7afb9e0129cf38cde909ab4a125442f0330a/lib/lotus/model/adapters/memory/query.rb#L435), whereas the SQL adapter requires that all conditions are met. Also, the memory adapter cannot be used to chain methods like the SQL adapter can.

It seems that the adapters might be more useful if they were interchangeable. If that were the case, I could use the fast memory adapter in my tests instead of the SQL adapter, for example. However, making the memory adapter behave the same way as the SQL adapter will make the memory adapter more complicated.

What are your thoughts?

Make sequel dependency optional

Since Lotus::Model is designed to have as many as possible pluggable adapters, it doesn't seem right having 'sequel' as a runtime dependency. Some people definitely won't using it 😃

Thoughts?

Implement Entity#to_h

This will make serializations easier.

require 'json'

class User
  include Lotus::Entity
  self.attributes = :name
end

user = User.new(id: 23, name: 'Luca')
user.to_h # => { :id => 23, :name => "Luca" }

JSON.generate(user.to_h) # => "{\"id\":\"23\", \"name\":\"Luca\"}"

Remove Entity attributes inheritance

This is just an exploratory discussion.

I'm thinking if we want to allow Entity's inheritance.
Entities are Ruby objects that can be persisted by a Repository.

Imagine the following mapping:

collection :books do
  entity Book
  repository BookRepository

  attribute :id, Integer
  attribute :title, String
  attribute :price, Integer
end

Also imagine to have the following entities:

class Book
  include Lotus::Entity
  self.attributes = :title
end

class PricedBook < Book
  include Lotus::Entity
  self.attributes = :price
end

Now, this scenario may lead to some problems:

  1. Book instances can't be fetched, because the mapping will try to invoke a setter for price, which isn't defined.
  2. If I persist a PricedBook instance, when I do BookRepository.find(23) it will return a Book, because that is the entity mapped for that collection. This makes sense, but it's counter intuitive at the first glance.
  3. This resembles like ActiveRecord Single Table Inheritance, which has been source of pain in my career.

We can proceed with two alternatives:

  1. Prevent inheritance
  2. Document that inheritance should be managed by mapping multiple times the same database table. To achieve this, we should introduce an aliasing for Mapper#collection. Right now it accepts a name that MUST match the table, and it also acts as unique identifier for the Mapper. We could to collection :priced_books, as: :books. Where the first will be the identifier and the second one the real table name.

What do you think?

Add default option to #attribute in collection

Suppose we have this mapper

mapper = Lotus::Model::Mapper.new do
   collection :users do
     entity User

     attribute :id,    Integer
     attribute :name,  String
     attribute :admin, Boolean, default: -> { false }
   end
 end

 u = User.new(name: 'Luca', admin: true)
 UserRepository.persist(u)
 u.admin #=> true

 u = User.new(name: 'Luca')
 UserRepository.persist(u)
 u.admin #=> should be false instead nil

Question: Memory::Command vs. Sql::Command

As I've been working on two separate adapters for Lotus::Model, I've been using the MemoryAdapter as my control adapter; That is the test should pass against the MemoryAdapter and the other Adapter.

One thing I've noticed is that the Lotus::Model::SqlAdapter and Lotus::Model::MemoryAdapter have what appear to be varying responsibilities. In the case of the Sql adapter, the Collection is performing the serialization and deserialization. In the case of the MemoryAdapter, the Query and Command are performing the serialization and deserialization.

I'm wondering about normalizing this behavior; that is having serialization and deserialization of an object happen in the analogous class for each adapter. It looks easiest to move the deserialization/serialization responsibility into the Adapter::Collection object; Is that the most logical place to put this?

In having a responsibilities separated, I can provide general guidance on crafting other adapters.

lotus model version on rubygems outdated

When I try to do all steps from EXAMPLE.md, I get the following error message:

application.rb:28:in `<top (required)>': undefined method `configure' for Lotus::Model:Module (NoMethodError)

My application.rb looks like this:

require 'lotus'
require 'lotus/model'

module ToDoApp
  class Application < Lotus::Application
    configure do
      routes do
        get '/', to: 'home#index'
        post '/', to: 'home#index'
        get '/impressum', to: 'imprint#page'
      end

      load_paths << [
        'controllers',
        'models',
        'views',
        'repositories'
      ]
      layout :application
    end

    load!
  end

  CONNECTION_URI = "sqlite://#{ __dir__ }/test.db"

  Lotus::Model.configure do
    adapter type: :sql, uri: CONNECTION_URI

    mapping do
      collection :tasks do
        entity     ToDoApp::Models::Task
        repository ToDoApp::Repositories::TaskRepository

        attribute :id,   Integer
        attribute :name, String
      end
    end
  end

  Lotus::Model.load!
end

This works only when I use lotus-model from github:

source "https://rubygems.org"

gem 'sqlite3'
gem 'lotus-model', github: "lotus/model"
gem "lotusrb", github: "lotus/lotus"

SQL Comparison Operators

I have the following method in a repository:

class WidgetRepository
  include Lotus::Repository

  def self.active
    query do
      where("publish_at >= #{Date.today}")
    end
  end
end

Calling the class method active via WidgetRepository.active results in the following error:

KeyError:
  key not found: "publish_at >= 2014-07-21"

I do not see any examples or documentation for this functionality within Lotus. Any help would be appreciated.

Forcing query execution

What's the preferred approach to forcing a query to execute? Following the docs/EXAMPLE.md, you'd think the following method best_article_ever would return an Article:

class ArticleRepository
  include Lotus::Repository

  def self.best_article_ever
    query do
      where(published: true)
      .desc(:comments_count)
      .first
    end
  end
end

But this method actually returns an unexecuted Lotus::Model::Adapters::Sql::Query object. Calling .to_a works, but it feels awkward to call .to_a.first when the .first call is present in the original query.

Attributes not coerced when persisted

class User
  include Lotus::Entity
  attributes :id, :name, :age
end

mapper = Lotus::Model::Mapper.new do
  collection :users do
    entity User

    attribute :id,   String
    attribute :name, String
    attribute :age,  Integer
  end
end.load!

user1 = User.new(name: 'Luca', age: '32')
UserRepository.persist(user1)

When user1 is persisted, the age is not coerced to an Integer before persisting and remains as a string. For dynamic datastores (e.g. RethinkDB), the type is kept intact.

When retrieved from storage, user1.age is coerced to an Integer from a String.

Is this behavior intended? I can see how input should be type-validated before it reaches the persisting method, but defining the type in the mapper should guarantee the type is saved properly.

Is it the job of the adapter to coerce the attributes?

Add created_at/updated_at timestamp

What: Having a conventional / implicit timestamp plugin
How:

  • Implement a special attribute helper for collection declaration, eg:

    collection :users do
      entity     User
      repository UserRepository
    
      attribute :id,   Integer
      attribute :name, String
      timestamps
    end
  • Enable Sequel::Plugins::Timestamps If the DB table has column created_at and updated_at

Pending for discussion

Add SQL transactions support

This should be implemented in Sql::Query.
Here's the related Sequel documentation: http://sequel.jeremyevans.net/rdoc/files/doc/transactions_rdoc.html

I'd say to not support/document Sequel::Rollback. Using exceptions for control flow is always a poor choice. If an exception is raised as side effect, the transaction should be rolled back.

transaction do
  raise `Sequel::Rollback` # this will rollback anyway because of Sequel, but it's a poor style that we shouldn't encourage
end
transaction do
  some_dangerous_method! # if this raises an exception, the transaction should be rolled back
end

The second style is the one that we should encourage/document.

/cc @stevehodgkiss @joneslee85

Allow `mapping` to take a path as argument

When we configure Lotus::Model, it would be great to make mapping to accept a path where to find the mapping definitions.

Lotus::Model.configure do
  adapter type: :memory, uri: 'memory://localhost'
  mapping 'config/mapping'
end.load!
# config/mapping.rb
collection :users do
  # ...
end

We already support this syntax in lotusrb for routes definitions.

Postgresql - Default values and Nil

So I'm having the issues have setting a default value and not allowing null. It seems that if an attribute is nil, Lotus is explicitly sending NULL.

Naming confusion

Throughout the code there are two Collection concepts. A Mapping::Collection and an Adapter::Collection. Are there perhaps better names for the variables?

In the Memory adapter, the dataset variable is an instance of Lotus::Model::Adapters::Memory::Collection. Is that a better name?

The reason I'm asking is that I've been working on a custom adapter for Fedora Commons and now Solr, and juggling the context of what is meant by collection is taxing my brain.

In the case of Mapper::Collection, would the name make more sense as Map? Or Legend (as in a map's legend)?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.