How to mix database and non database attributes on an entity?


#1

Hi everyone, I am new to Hanami and I couldn’t find a definitive answer on what is the best way to have “virtual” or “not database persisted” attribute on an entity?

The example I have is I think very common, it’s the password attribute on a User. Since what gets stored is the encrypted_password, password itself is “non persisted”.

What I ended up doing is just going with plain ruby approach by changing the initializer on User entity:

def initialize(attributes = nil)
  if attributes && attributes[:password]
    attributes = attributes.merge(encrypted_password: encrypt(attributes[:password]))
  end

  super(attributes)
end

This works but only if User.new is called so I can’t use UserRepository#create directly and I have to do (in an action):

UserRepository.new.create(User.new(params[:user]))

Is there a better way?

P.S. Somewhat unrelated, I’m wondering why Repository#create doesn’t call Entity.new? Being new to Hanami I assumed Repository.new.create(params) is just a convenience for Repository.new.create(Entity.new(params)) and was very surprised to find they are not equivalent.


#2

@radanskoric Hi :wave:

You’ve got several questions. Let me answer one-by-one.

Adding virtual attributes

The automatic mapping of database columns with entities attributes saves you from manually maintaining the schema.

Let’s say your columns are:

CREATE TABLE users (
    id integer NOT NULL,
    encrypted_password text,
    created_at timestamp without time zone,
    updated_at timestamp without time zone
);

Automatically, User will have id, encrypted_password, created_at, and updated_at attributes.

Any change to the schema automatic schema must be added manually.

class User < Hanami::Entity
  attributes do
    attribute :id,                 Types::Int
    attribute :password,           Types::String
    attribute :encrypted_password, Types::String
    attribute :created_at,         Types::Time
    attribute :updated_at,         Types::Time
  end
end

Please note: when Entity’s manual schema is used, it won’t reflect the changes from the database schema. Any new column must be manually added here.

Dealing with passwords

The reason why User has both password and encrypted_password is due to an old solution in the Rails community, that all the other new libraries cargo culted.

You can name the database column :password and prepare the data before to pass it to the repository.

# apps/web/controllers/signups/create.rb
module Web::Controllers::Signups
  class Create
    include Web::Action

    def call(params)
      data = params[:signup].dup
      data[:password] = Password.generate(data[:password])

      user = UserRepository.new.create(data)

      # ...
    end
  end
end

What’s the implementation of Password? Here it is:

# lib/bookshelf/password.rb
require 'bcrypt'

class Password
  DEFAULT_COST = BCrypt::Engine.cost

  def self.generate(input, cost: DEFAULT_COST)
    BCrypt::Password.create(input, cost: cost)
  end

  def initialize(encrypted_password)
    @password = BCrypt::Password.new(encrypted_password)
  end

  def ==(other)
    @password == other
  end
end

Then User can override #password with a custom implementation:

# lib/bookshelf/entities/user.rb
class User < Hanami::Entity
  def password
    Password.new(super)
  end
end

If you want to compare stored password with a login attempt:

user.password
# => #<Password:0x007fa3e8d89d50 @password="$2a$10$.bbV0QtW7.5JJSdEef4gM.xoF4NilOGh5J.TVbq1.o5OgpcQeO0cq">

user.password == "123"
# => true

user.password == "foo"
# => false

This eliminates the need of dealing with two attributes.

Repository#create vs Entity#new

For a repository, an entity is the result of an operation. When you create a new user, it returns an entity. When you find an entity, it returns one.

On the other hand, the input is always data. When you receive input from a form, it’s data. Instantiating an entity would be unnecessary ceremony.

We encourage to manipulate data after validations and before to persist it. We’re working on a way to standardize this process.


#3

Regarding the placement of encryption into the action:

There is an actual reason to have separate attributes and to handle the encryption on the model layer. It’s to eliminate any chance that an actual password will ever hit the database.

The solution you proposed makes complete sense for most use cases because the best place to prepare the data is of course at the entry into the system and it makes sense to do it per case basis since it can vary based on context. However, I don’t want to do that for passwords.
I think we can agree that the requirement for the password to be encrypted is system wide and will never change. Because of that and the fact that if a mistake in code caused a password to be actually stored would be a disaster I want to place the actual encryption as low as possible to make it extremely unlikely it will ever be missed. That is why I am placing it on the entity itself.

Regarding the Repository#create comment.

I’m going to have to disagree again. :slight_smile: It is only needless ceremony if the entity doesn’t do any modification to its attributes. Therefore the assumption that is hidden in this design decision is that the entity doesn’t do any modification to its attributes.

If the entity does that then after repository returns the entity it will be different from an entity that was created directly in memory via Entity#new. Since I consider the repository to be void of application logic and merely a storage mechanism implementation detail that is something that surprises me. I would expect that the only difference between those two entities would be that one is persisted in permanent storage and one isn’t. Can you please point out the flaw in my logic?

P.S. I want to apologize if I sound critical, I consider Hanami a great project. I’ve been working with Rails for the past 9 years and it’s great to see another ruby full featured framework starting to become a very viable alternative. It’s just that talking about all the points in which I agree with Hanami approach is not nearly as productive or useful as talking about disagreements. :slight_smile:


#4

Yes we agree. It’s just to make sure that the right components are used to “create” an user. For instance, you implement an interactor CreateUser that will take care of encrypting the password. As long you use it (and not the repository directly), you’ll have safe data stored.


Anyway, I understand and respect your opposite opinion on the design of feature. So how can I help you? Another suggestion that I have is to do:

class UserRepository < Hanami::Repository
  def create_from_entity(data)
    create(User.new(data))
  end
end

Of course you need to keep the encryption logic in User#initialize.


Please let me clarify again this point. :slight_smile: We want to formalize data transformations in Hanami to make sure that the repository will blindly store the data that receives. In our opinion this should not involve the entity, but a new component that we’re going to implement.

The idea is to have: coercions & validations => data transformations (missing) => repository.

Again, the entity is only the output of this process. It can provide a different representation of some of its attributes. One example is my code above that overrides User#password. But it’s not entity’s resposibility to prepare data before to be stored.


No worries at all. You’re very welcome. :wink:


#5

Ah, got it. This makes sense. Actually, in my job I work on an extremely large Rails applications and in it we have a layer of business actions (something along the lines of Trailblazer’s operation, to give you an idea). The key decision in the design is that ANY mutation of the system state must go through them. Our (Rails) controllers are very thin, delegating all business logic to business action objects.

It sounds like you are aiming for something similar but a key feature for us is that business actions can call other business actions which is not possible with hanami actions since they have http handling logic:

[6] pry(main)> Web::Controllers::Users::Create.new.call(user: {name: "Tester", email: "tester@example.com", password: "password"})
Hanami::Action::InvalidCSRFTokenError: Hanami::Action::InvalidCSRFTokenError

And now I realise that this is why I did’t want to place this logic on the action. If I will have any other code path which needs to also create a user I can’t reuse these validations and data transformation logic.

There are hundredths of such cases in the app I work on but let me come up with a synthetic example to better ilustrate the issue. Let’s say we are building an ad posting site and we want to provide a form when people register and post an ad in one go, through one form. Ideally I’d want to have something like Web::Controllers::Users::CreateWithAd that would internally call both Web::Controllers::Users::Create and Web::Controllers::Ads::Create to ensure that they are both created correctly.

Since Actions have HTTP related logic it might have to be a new type of object that would have validations and data transformations (something like Operation or Interaction if you’re doing DCI or something like that). The action would then end up being a thin wrapper for it. But that’s out of scope of this discussion.

Anyway, I now have a much clearer idea of your motivations so thank you for that. I think I’ll revisit this topic in the future when I have a bit more experience with Hanami.


#6

We have Hanami::Interactor which is a good candidate for that. But it still misses validations and data transformations.