Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Engine (ROM) #334

Merged
merged 82 commits into from
Nov 10, 2016
Merged

New Engine (ROM) #334

merged 82 commits into from
Nov 10, 2016

Conversation

jodosha
Copy link
Member

@jodosha jodosha commented Sep 19, 2016

New Engine

Use rom as low-level engine for hanami-model. Introduce new features (automapping for SQL databases, experimental associations), remove old (dirty tracking). Fix old bugs.

Example

Before to dive into the details here's how it looks the basic example:

require 'hanami/model'

class User
  include Hanami::Entity
  # no more attributes here
end

class UserRepository < Hanami::Repository # inherit instead of include
end

Hanami::Model.configure do
  adapter :sql, ENV['DATABASE_URL']
  # No more mapping
end.load!

repository = UserRepository.new
user       = repository.create(name: 'Luca') # It works with data as primary input type
user.id # => 1

user       = repository.update(user.id, age: 34) # It accepts the primary key and the data
user.age # => 34

repository.delete(user.id) # It accepts the primary key

This reduces drastically the boilerplate to setup an entity and a repository pair.

Repositories

Keeping the good work from #299, repositories now expose their CRUD operations at the instance level, not at the class level.

Data (hashes) is the preferred input for CRUD operations. In past we used to pass entities, while this is still possible, it’s suggested to use just data.

Repositories can access all the relations with instance methods. For instance, from the BookRepository we can access users relation (database table) with #users.

Repositories are able to auto infer the database schema and use the correct Ruby types. Advanced PostgreSQL types like UUID, Array, JSON(B) and Money are handled natively.

Repositories can also auto map database columns with entities attributes. In case there is a mismatch between the names, we can define a custom mapping. NOTE: manual mapping is optional and should be used only with legacy databases.

# Basic case
class UserRepository < Hanami::Repository
end

# Advanced case for legacy databases
class OperatorRepository < Hanami::Repository
  # By convention we expect it to work with :operators,
  # but instead we manually define the name
  self.relation = :t_operator

  # Define a mapping between each entity attribute to the corresponding database column
  mapping do
    attribute :id,   from: :i_operator_id
    attribute :name, from: :s_name
    # …
  end
end

Custom queries can be defined with an intuitive Ruby syntax:

class UserRepository < Hanami::Repository
  def by_name(name)
    users.where(name: name).as(:entity)
  end
end

Entities

Entities are now immutable.

Automatic Schema

With SQL databases, entities get attributes from table schema.

As example, image to have a users table with id (int), name (string), email (string), age (int), created_at (time), and updated_at (time).

class User < Hanami::Entity
end

class UserRepository < Hanami::Repository
end

user = User.new(name: "Luca", foo: "bar")
user.name # => "Luca"
user.age # => nil
user.foo # => NoMethodError

It ignores foo because it isn't part of the schema. age instead is nil because we haven't initialized it.

There is also a new feature for entities which enforces schema types. When an entity is initialized with one or more wrong attributes it raises an error.

class User < Hanami::Entity
end

class UserRepository < Hanami::Repository
end

User.new(age: "foo") # => ArgumentError

Custom Schema

If developers want to customize the set of attributes of an entity or specify the level of strictness of the type coercion, they can build a custom schema.

class User < Hanami::Entity
  attributes do
    attribute :id, Types::Int
    # ...
  end
end

class UserRepository < Hanami::Repository
end

For a complete list of the options, please check: http://dry-rb.org/gems/dry-types/built-in-types/

Please note that .attributes changed its public API.

Associations (experimental)

We introduced experimental support for associations (only has_many at this time).

When we define an association, it does NOT add any method to the public interface. We can access the association internally via #assoc and define only the explicit operations that we need for our model domain.

class AuthorRepository < Hanami::Repository
  associations do
    has_many :books
  end

  def find_with_books(id)
    aggregate(:books).where(authors__id: id).as(Author).one
  end

  def books_for(author)
    assoc(:books, author)
  end

  def add_book(author, data)
    assoc(:books, author).add(data)
  end

  def remove_book(author, id)
    assoc(:books, author).remove(id)
  end

  def delete_books(author)
    assoc(:books, author).delete
  end

  def delete_on_sales_books(author)
    assoc(:books, author).where(on_sale: true).delete
  end

  def books_count(author)
    assoc(:books, author).count
  end

  def on_sales_books_count(author)
    assoc(:books, author).where(on_sale: true).count
  end

  def find_book(author, id)
    book_for(author, id).one
  end

  def book_exists?(author, id)
    book_for(author, id).exists?
  end

  private

  def book_for(author, id)
    assoc(:books, author).where(id: id)
  end
end

A special thanks goes to @solnic, @flash-gordon, @AMHOL and @timriley for ROM and their tireless support provided during these months of development. You rock! 💚


Closes #217
Closes #237
Closes #245
Closes #272
Closes #273
Closes #278
Closes #279
Closes #285
Closes #291
Closes #294


/cc @hanami/core-team @hanami/contributors

jodosha added 30 commits May 17, 2016 19:39
@beauby
Copy link
Contributor

beauby commented Nov 8, 2016

Is this change in the configuration API accidental? Before, the adapter method took a hash (adapter type: :sql, uri: 'foo://bar') – or at least this is what the comments in lib/bookshelf.rb in a Hanami 0.8 generated app implies – and now it expects the type and uri to be given directly as parameters (adapter :sql, 'foo://bar').

@beauby
Copy link
Contributor

beauby commented Nov 8, 2016

Also, when one generates a model before the backing sql table is created, the whole thing breaks because schema inferring fails.

@jodosha
Copy link
Member Author

jodosha commented Nov 8, 2016

Okay, I tried HoundCI and it doesn't respect our .rubocop.yml. My apologize for the the tons of emails you have received.

@beauby
Copy link
Contributor

beauby commented Nov 8, 2016

@jodosha Looks like you forgot to add .hound.yml with

ruby:
  config_file: .rubocop.yml

@jodosha
Copy link
Member Author

jodosha commented Nov 8, 2016

@beauby The first batch of comments was caused by the lack of .hound.yml, but then I added it: 4ad9db6 and the second wave of comments happened.

Am I missing something?

@beauby
Copy link
Contributor

beauby commented Nov 8, 2016

@jodosha You might want to add the following to the .rubocop.yml

Style/FrozenStringLiteralComment:
  Enabled: false

@jodosha
Copy link
Member Author

jodosha commented Nov 8, 2016

@beauby Sure, but why it doesn't report the issue locally and on Travis CI?

@beauby
Copy link
Contributor

beauby commented Nov 8, 2016

@jodosha According to github, this cop only exists since early 2016, so I'd put my money on HoundCI running a fresher version of rubocop.

@beauby
Copy link
Contributor

beauby commented Nov 8, 2016

Looking at rubocop's default config, it appears that by default, this cop is enabled or disabled depending on the TargetRubyVersion of rubocop which may be influenced by the environment in which it is ran, as it is not explicitly specified in the .rubocop.yml.
According to this discussion, the default value for this cop will be disabled anyways.

@jodosha
Copy link
Member Author

jodosha commented Nov 8, 2016

@beauby Thanks for the quick and detailed reply. I'm running rubocop 0.45.0 on MRI 2.3.1 and I didn't ran into that frozen string violation by running bundle exec rubocop.


My point is: local machine and TravisCI agree, while HoundCI doesn't. The first two environments are more transparent for us than the latter. What I want to avoid is to have headaches like this in the future.

@beauby
Copy link
Contributor

beauby commented Nov 8, 2016

@jodosha Agreed, but TBH, this seems like a shortcoming on rubocop's side, as the environment should not have any effect on static analysis of code.

@jodosha jodosha merged commit 419712c into master Nov 10, 2016
@jodosha jodosha mentioned this pull request Nov 10, 2016
@jodosha
Copy link
Member Author

jodosha commented Nov 10, 2016

This was merged in master. Thank you all! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants