Giter Site home page Giter Site logo

hanami / model Goto Github PK

View Code? Open in Web Editor NEW
445.0 30.0 152.0 1.76 MB

Ruby persistence framework with entities and repositories

Home Page: http://hanamirb.org

License: MIT License

Ruby 99.88% Shell 0.12%
hanami ruby persistence persistence-framework persistence-layer entity database repository repository-pattern entity-framework

model's Introduction

Hanami 🌸

The web, with simplicity.

Version

This branch contains the code for hanami 2.0.x.

Frameworks

Hanami is a full-stack Ruby web framework. It's made up of smaller, single-purpose libraries.

This repository is for the full-stack framework, which provides the glue that ties all the parts together:

These components are designed to be used independently or together in a Hanami application.

Status

Gem Version CI Depfu

Installation

Hanami supports Ruby (MRI) 3.0+

gem install hanami

Usage

hanami new bookshelf
cd bookshelf && bundle
bundle exec hanami server # visit http://localhost:2300

Please follow along with the Getting Started guide.

Donations

You can give back to Open Source, by supporting Hanami development via GitHub Sponsors.

Supporters

Contact

Community

We strive for an inclusive and helpful community. We have a Code of Conduct to handle controversial cases. In general, we expect you to be nice with other people. Our hope is for a great software and a great Community.

Contributing Open Source Helpers

  1. Fork it ( https://github.com/hanami/hanami/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 a new Pull Request

In addition to contributing code, you can help to triage issues. This can include reproducing bug reports, or asking for vital information such as version numbers or reproduction instructions. If you would like to start triaging issues, one easy way to get started is to subscribe to hanami on CodeTriage.

Tests

To run all test suite:

$ bundle exec rake

To run all the unit tests:

$ bundle exec rspec spec/unit

To run all the integration tests:

$ bundle exec rspec spec/integration

To run a single test:

$ bundle exec rspec path/to/spec.rb

Development Requirements

  • Ruby >= 3.0
  • Bundler
  • Node.js (MacOS)

Versioning

Hanami uses Semantic Versioning 2.0.0

Copyright

Copyright © 2014 Hanami Team – Released under MIT License.

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

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

[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]

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

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

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?

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.

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?

[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

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

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?

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

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

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.

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"

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.

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.

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

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>]

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

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?

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.

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

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.

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!'

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.

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.

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\"}"

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.

SQL Joins

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

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.

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>

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.

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

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.

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/

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

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?

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

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.

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

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>]

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.

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.

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.