Category: Default

Ruby on Rails, Mysql2::Error: Incorrect string value: ‘\xF0\x9F\x99\x82’ and Specified key was too long; max key length is 767 bytes

If you get error like this one:

Mysql2::Error: Incorrect string value: '\xF0\x9F\x99\x82'

it means that you use verion of Mysql with settings that does not support full Unicode. Changing that in Ruby on Rails should be fairly simple:

class ChangeEncoding < ActiveRecord::Migration[5.0]
  def change
    config = Rails.configuration.database_configuration
    db_name = config[Rails.env]["database"]
    collate = 'utf8mb4_polish_ci'
    char_set = 'utf8mb4'

    execute("ALTER DATABASE #{db_name} CHARACTER SET #{char_set} COLLATE #{collate};")

    ActiveRecord::Base.connection.tables.each do |table|
      execute("ALTER TABLE #{table} CONVERT TO CHARACTER SET #{char_set} COLLATE #{collate};")
    end
  end
end

However you might encounter following error:

Specified key was too long; max key length is 767 bytes

767 bytes is the stated prefix limitation for InnoDB tables.

To fix that you need to:

  • Update your database.yml file
  • Upgrade to MySQL 5.7 or edit your my.cnf  to enable innodb_large_prefix
  • Change row format to DYNAMIC
  • Alter the database and all the tables using Ruby on Rails migration

Database.yml

You need to change your encoding and collation for ActiveRecord:

production:
  encoding: utf8mb4
  collation: utf8mb4_polish_ci

MySQL my.cnf

Edit my.cnf and add following lines:

innodb_large_prefix=on
innodb_file_format=barracuda
innodb_file_per_table=true

DYNAMIC row format

If you don't do this, you will probably (if you have enough indexes) end up with this error:

767 bytes is the stated prefix limitation for InnoDB tables

To change row format, you need to run following query for each table:

ALTER TABLE table_name ROW_FORMAT=DYNAMIC;

However, you don't need to do this manually - below you will find a Ruby on Rails migration that will do everything that is required to make things going.

Complex migration to set everything as it should be in the MySQL database

class ChangeEncoding < ActiveRecord::Migration[5.0]
  def change
    config = Rails.configuration.database_configuration
    db_name = config[Rails.env]["database"]
    collate = 'utf8mb4_polish_ci'
    char_set = 'utf8mb4'
    row_format = 'DYNAMIC'

    execute("ALTER DATABASE #{db_name} CHARACTER SET #{char_set} COLLATE #{collate};")

    ActiveRecord::Base.connection.tables.each do |table|
      execute("ALTER TABLE #{table} ROW_FORMAT=#{row_format};")
      execute("ALTER TABLE #{table} CONVERT TO CHARACTER SET #{char_set} COLLATE #{collate};")
    end
  end
end

After you run this migration, everything should work fine.

Trailblazer + Devise: Integrating Devise validatable model with Trailblazer operation + error propagation

Devise is one of those gems that are tightly bound to the Rails stack. It means that as long as you follow the "Rails way" and you do things the recommended way, you should not have any problems.

However, Trailblazer is not the recommended way and the way it works, not always makes it painless to integrate with external gems (note: it's not a Trailblazer fault, more often poorly designed external gems). Unfortunately Devise is one of those libraries. Models have validations, things happen magically in the controllers and so on.

Most of the time, you can leave it that way, as the scope of what Devise does is pretty isolated (authentication + authorization). But what if you want to integrate it with Trailblazer, for example to provide a custom built password change page? You can do this by following those steps

  • Provide a contract with similar (or the same) validation rules as Devise (this will make it easier to migrate if you decide to drop model validations)
  • Create an operation that will save the contract and propagate changes to the model
  • Copy model errors (if any) into contract errors

Here's the code you need (I removed more complex validation rules to simplify things):

# Contract object
class Contracts::Update < Reform::Form
  include Reform::Form::ActiveRecord
  # Devise validatable model
  model User

  property :password
  property :password_confirmation

  validates :password,
    presence: true,
    confirmation: true
  validates :password_confirmation,
    presence: true
end

Operation is fairly simple as well:

class Operations::Update < Trailblazer::Operation
  include Trailblazer::Operation::Model
  contract Contracts::Update
  # Devise validatable model
  model User

  def process(params)
    validate(params[:user]) do
      # When our validations passes, we can try to save contract
      contract.save do |hash|
        # update our user instance
        model.update(hash)
        # and propagate any model based errors to our contract and operation
        model.errors.each { |name, desc| errors.add(name, desc) }
      end
    end
  end

And the best part - controller:

class PasswordsController < BaseController
  def edit
    respond_with(form Operations::Update)
  end

  def update
    respond_with(run Operations::Update)
  end

Copyright © 2024 Closer to Code

Theme by Anders NorenUp ↑