hanami / model Goto Github PK
View Code? Open in Web Editor NEWRuby persistence framework with entities and repositories
Home Page: http://hanamirb.org
License: MIT License
Ruby persistence framework with entities and repositories
Home Page: http://hanamirb.org
License: MIT License
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
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.
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
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!'
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.
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.
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.
I think it would be a nice idea add the method count to the repository, an example:
PostRespository.count # => 10
what do you think?
Hi,
I'm struggling with getting the lotus_model example running. Am I just holding it wrong or needs it to be updated?
Maybe you can have a look at my comment.
UPDATE: Please see proposed changes in my PR #144
What: Schema Migration for SQL adapter
How:
up
, down
methoddb/schema.rb
for /lib
and apps/<app_name>/db/schema.rb
for applotus 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
lotus generate migration web:oh_my_lord
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
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"
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.
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>]
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.
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\"}"
When a new Lotus process is started (new console or server process fork), the persisted data isn't loaded in memory.
A subsequent write (form submission), erases the old and keeps only the new one.
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)>'
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.
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.
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]
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
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?
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.
The initialization of the framework is verbose and requires a lot of manual setup.
We can enhance those mechanisms with the following changes:
Lotus::Model
to register adapters instances, and to specify a default one (see example 1).Lotus::Controller
and Lotus::View
.Mapper should allow to specify adapters for a collection (see example 2).
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
.
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
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
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?
Configuration#mapping
should store the block from blk
or path
, and evaluate it when #load!
is invoked.
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>
I understand that associations are under discussion, but how is the best way to implement many-to-many relationships currently?
I can understand how to model 1-many (by calling the corresponding Repository
on the many side), but what about many-to-many?
Add support to SQL joins to Sql::Adapter
and Sql::Query
.
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/
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.
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.
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.
Taker: LeoTeam
Repository#create
updates the #created_at
if exists#created_at
does not existI 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
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)?
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. :)
class Article
include Lotus::Entity
end
article = Article.new
article.id # => NoMethodError: undefined method `id'
I believe we have to set attr_accessor :id
explicitly in Lotus::Entity. Thoughts?
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?
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
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.
When we omit mapping
in configuration block, at the time we invoke Lotus::Model.load!
it raises a cryptic exception undefined method
load!' 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
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.
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>]
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.
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
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
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
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:
Book
instances can't be fetched, because the mapping will try to invoke a setter for price
, which isn't defined.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.We can proceed with two alternatives:
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?
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.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.