BigBing 技术博客

Add a new database migration tips in Rails

When you are using MySQL, if a migration fails the DDL will not be rolled back with the transaction, potentially leaving the database in an invalid state, which could theoretically bring the whole application down.

Referencing model classes in a migration

Generally you would not need to reference model classes in a migration unless you were modifying existing data in some way, or adding new data. Sometimes we need to do this, but referencing application model classes raises some problems.

  • If a model class is referenced in one migration, then the model’s table is altered in a subsequent migration, the annotation written to the model’s file will not reflect the subsequent alteration.
  • Old migrations referencing the model might fail if the model code has evolved significantly since the time the migration was written.

To avoid these problems, you should:

  1. Avoid using Active Record models in your migrations by using SQL
  2. If you must use Active Record models, create a migration-specific class for the purpose.

Let’s look at an example where we want to strip leading and trailing whitespace from a column. A naive implementation might look something like this:

class MyMigration < ActiveRecord::Migration
  def change
    User.where.not(name: nil).each do |user|
      user.update name: user.name.strip
    end
  end
end

Using SQL

Of course, doing this in SQL is quite simple and far more efficient than using Active Record to load and modify each record individually:

class MyMigration < ActiveRecord::Migration
  def change
    execute <<-SQL.squish
      UPDATE users
         SET name = LTRIM(RTRIM(name))
       WHERE name IS NOT NULL
    SQL
  end
end

Note: that squish-ing your SQL is recommended because it makes the migration output a lot easier to read.

Using a migration-specific model class

Some data migrations can’t be easilly expressed in SQL, so let’s say we want to stick with using Active Record. In such cases, we can create a migration specific class for the purpose:

class MyMigration < ActiveRecord::Migration
  class User < ActiveRecord::Base
  end

  def change
    User.where.not(name: nil).each do |user|
      user.update name: user.name.strip
    end
  end
end

Here the migration will use the MyMigration::User class instead of ::User

为 VS Code定制 Rspec 的 formatter

最近我已经完全从 Sublime 转到了 VS CodeVS Code 的各种功能做的还是非常不错的. 在这里也向大家推荐。

其中我最喜欢的就是集成了 terminal,然后在做 Rails 相关的项目开发时,通过 Rspec 的插件可以快速的在集成的终端中运行测试和调试代码(pry debug)。

这样在开发的过程中就不需要离开编辑器去调试代码,极大的缩短了BDD和 TDD 的循环时间。

由于我还是喜欢把 terminal 放到下面,然后也不能给 terminal 很大的空间,当测试失败的时候,会打印出很长的 backtrace,这时候经常就需要滚动鼠标才能查看失败的信息或者异常信息. 效果如下图所示: Rspec default formatter

这时想到了,Rspec 支持自定义的formatter,这样我就可以把输出的 backtrace 简化,保留我可能感兴趣的出错文件和测试信息.看上去的效果如下。 Rspec custom formatter

详细的custom_formatter.rb代码请见: gist link

使用方法:custom_formatter.rb 下载到本地,然后配置 Rspec 插件,设置如下:

"ruby.specCommand": "spring rspec --require ~/custom_formatter.rb --format CustomFormatter"

Tips

  • 在 VS Code 的 terminal 中可以,按住 Command 键然后点击带有行号的出错文件信息,以快速的打开文件
  • VS Code的 Markdown 插件也很棒,让我也不再需要单独的 markdown 编辑器

使用 Rails Migration转换 MySQL数据库和表的字符集总结

MySQL Character Set基础知识

对于 MySQL 数据库你可以在不同的 Level 设置Character Set 和 Collation,包括:Server Level,Database Level,Table Level,Column Level 还有 Application Level.

Server Level:

可以通过命令行设置,也可以通过配置文件设置

默认: --character-set-server=latin1

latin1_swedish_ci is the default collation for latin1

还可以通过重新编译时指定参数实现:use the DEFAULT_CHARSET and DEFAULT_COLLATION

作用范围:如果创建数据库时不指定,那么就使用 Server Level 的设置

查看当前的设定,可以查看系统变量:character_set_server and collation_server

Database Level:

可以在创建数据库时设置:

CREATE DATABASE db_name CHARACTER SET latin1 COLLATE latin1_swedish_ci;

默认值:可以由 character_set_database and collation_database 系统变量决定.

可以通过下面的命令查看当前的设置:

USE db_name;

SELECT @@character_set_database, @@collation_database;

或者

SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME
FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = 'db_name';

作用:如果建表时没有指定,那么会作为表的默认值,同时也是作为 LOAD DATA 的默认值

修改数据库level 的 character set 和 collation

ALTER DATABASE db_name CHARACTER SET utf8 COLLATE utf8_unicode_ci;

或者单独修改

ALTER DATABASE my_database DEFAULT COLLATE utf8_unicode_ci;

ALTER DATABASE my_database DEFAULT CHARACTER SET utf8;

Table Level:

可以在建表的语句中进行设置

作用:如果字段没有具体制定,那么会作为字段的默认值 note:该功能是 mysql 的一个扩展,不是标准的SQL

使用下面语句可以同时修改 table 和 table 中字段的设置

ALTER TABLE tbl_name CONVERT TO CHARACTER SET charset_name [COLLATE collation_name];

查看某个数据库中的所有表的一些设置信息的语句

SHOW TABLE STATUS FROM db_name;

Column Level:

N/A

Application Connection Level:

对于 Rails 应用,在 database.yml的数据库连接设置中加上 ?reconnect=true&encoding=utf8&collation=utf8_unicode_ci

查看数据库不同级别的元数据的设置的语句,比如:character set

SELECT DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA S WHERE schema_name = 'db_name' AND DEFAULT_COLLATION_NAME != 'utf8_unicode_ci';

SELECT TABLE_NAME, TABLE_COLLATION FROM information_schema.TABLES WHERE table_schema = 'db_name' AND table_collation != 'utf8_unicode_ci';

SELECT * FROM information_schema.COLUMNS WHERE table_schema = 'db_name' AND collation_name != 'utf8_unicode_ci';

ConvertDatabaseCharacterSetAndCollationToUtf8 Migration

class ConvertDatabaseCharacterSetAndCollationToUtf8 < ActiveRecord::Migration
  def up
    execute <<~SQL
      ALTER DATABASE #{ActiveRecord::Base.connection.current_database} CHARACTER SET utf8 COLLATE utf8_unicode_ci;
    SQL
  end

  def down
    execute <<~SQL
      ALTER DATABASE #{ActiveRecord::Base.connection.current_database} CHARACTER SET latin1 COLLATE latin1_swedish_ci;
    SQL
  end
end

ConvertTablesCharacterSetAndCollationToUtf8 Migration

class ConvertTablesCharacterSetAndCollationToUtf8 < ActiveRecord::Migration
  def up
    execute("SET foreign_key_checks = 0")

    latin_tables_sql = <<~SQL
      SELECT TABLE_NAME, TABLE_COLLATION
      FROM information_schema.TABLES
      WHERE table_schema = '#{ActiveRecord::Base.connection.current_database}'
      AND table_collation != 'utf8_unicode_ci';
    SQL

    results = ActiveRecord::Base.connection.execute(latin_tables_sql)
    say "Total: #{results.count}"

    results.each do |result|
      alter_table_sql = "ALTER TABLE #{result[0]} CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci;"
      execute alter_table_sql
    end

    execute("SET foreign_key_checks = 1")
  end
end

Rails Migration tips

在调试该功能的时候,学到的一些 tips:

rake db:migrate:status 查看当前migration 的状态,包括版本信息等

rake db:migrate VERSION=33333333 migrate指定 version

rake db:rollback STEP=n 通过 STEP 参数指定回滚的范围

User.connection 可以用来检查当前数据库设置和连接的信息

ActiveRecord::Base.connection.current_database 获取当前连接的数据库

ActiveRecord::Migrator.current_version 查看当前的版本

在 Rails Console 或者 Runner 中执行 SQL语句,可以使用 ActiveRecord::Migration.execute("SQL")


参考文章: How to change all columns’ and tables’ collation to ‘utf8_bin’ in MySQL

ActiveRecord Enum实战总结

基本使用方法

案例说明: 给已经存在的 Company 增加一个 size 属性, 属性包括 large, medium, small 三个选项

从 Rails4.1 开始,可以通过 ActiveRecord::Enum 来快速实现这样的功能

class Company < ActiveRecord::Base
  enum size: [:large, :medium, :small]
end

定义了 enum 后,Rails会自动产生下面这些方法

Company.sizes
# => {"large"=>0, "medium"=>1, "small"=>2}

# Scope methods
Company.large
Company.medium
Company.small

# Query methods
company.large?
company.medium?

# Action methods
company.large!
company.small!

Migration

enum 的实现是基于该字段是一个 integer 类型的。所以添加这个新的字段我们需要下面的 migration

class AddSizeToCompanies < ActiveRecord::Migration
  def change
    add_column :companies, :size, :integer, null: false, default: 0
    add_index  :companies, :size
  end
end

赋值操作

通过上面 Company.sizes 方法,我们看到 Rails 默认是从0开始对应的。所以上面的 migration 中默认值是0。当新建一个Company时,默认 company 的 size 是 large

company = Company.new
company.size # => "large"

# 多种赋值操作
company.size = :medium
company.medium? # => true

company.size = 2
company.small? # => true

company.large = 'large'
company.large? # => true

enum 会自动添加一些验证,如果给 size 属性赋错误的值,Rails会抛出异常

company.size = 5
# => ArgumentError: 5 is not a valid billing_category
company.size = :bala
# => ArgumentError: 'bala' is not a valid billing_category

Form下拉菜单的填充

当在前端的Form中填充到下拉列表中,可以这样做

f.input :size, collection: Company.sizes.keys.map {|s| [s.titleize, s]}, prompt: "Select a size"

需要注意的地方:

由于存到数据库的仍然是 number,所以如果有别的应用也使用同样的数据库,那么该应用需要知道对应关系。

Rails4.1 及之后的版本中,由于 enum 会自动产生一些方法,所以要特别注意选项的命名问题,尽量用明确的命名。 另外如果你还需要不同的属性,拥有相同的选项,那么你可以考虑这个 gem activerecord-enum-without-methods

同时在 Rails5 中,就可以使用_prefix_postfix选项来避免相应的问题。·

更好的Migration方法

上面的migration方法对于产品环境已经有数据的情况下,可能会产生问题。

对于Mysql 数据库,新加字段的时候对于已有的记录,mysql 会自动设置 NOT NULL 的已有记录为 default 但是对于 postgreSQL 就会出现下面的错误:

PG::NotNullViolation: ERROR: column “size” contains null values

这时可以先add_column添加字段,不要加 NOT NULL 的限制,然后更新已有数据,然后再通过change_column_null来添加 NOT NULL 限制。

用CSS隐藏元素的五种方法

1. opacity: 0

.hide {
  opacity: 0;
}

相当于设置元素为透明 屏幕阅读器可读, 可以继续交互

2. visibility: hidden

.hide {
  visibility: hidden;
}

屏幕阅读器不可读.

重新成visible后就可以重新进行交互了 后续的子节点可以设置为visible而变得可见

3. display: none

.hide {
  display: none;
}

该元素不会被渲染, 所有后续的子节点也都是不会显示的 只可以通过dom api进行操作 屏幕阅读器不可读.

4. move out the viewport

.hide {
   position: absolute;
   top: -9999px;
   left: -9999px;
}

该方法的目的是想保留交互的功能,同时不要影响布局. 屏幕阅读器可读.

5. clip-path

.hide {
  clip-path: polygon(0px 0px,0px 0px,0px 0px,0px 0px);
}

通过裁剪的方法来隐藏.之前大家会用clip属性,现在clip属性已经废弃了,可以使用新的clip-path属性 想深入了解的可以看: 介绍clip-path属性

不过现在IE对clip-path还不支持

总结

用一张表来总结各种方法的区别:

方法 Occupy its position User interaction Screen Reader Transition Animation IE Support
opacity: 0 yes yes yes yes yes
visibility: hidden yes no no no yes
display: none no no no no yes
move out the viewport no yes yes yes yes
clip-path yes no yes yes no