Rails before_create按字母顺序递增

时间:2018-09-27 09:52:11

标签: ruby-on-rails ruby

所以我的表中有一个字段-exposure。该字段的名称称为setup_code, 每次创建exposure时,我想从A-Z递增setup_code怎么实现?

到目前为止,这是我所做的,根据this other answer,我看到可以使用next

进行递增
class Exposure < ApplicationRecord
  before_create :create_setup_code

  def create_setup_code
    last_setup_code = self.last.setup_code
    last_setup_code.next
  end
end

如果我已经在数据库中至少有一个setup_code,例如setup_code ='A',则此方法将起作用,然后对于下一个setup_code,当调用before_create时它将为'B'。

如何使用第一个setup_code = 'A'进行初始化?

3 个答案:

答案 0 :(得分:2)

为避免出现竞争情况,您可能需要使用自动递增列。

首先创建一个自动递增的列:

class AddSetupCodeToExposures < ActiveRecord::Migration[5.2]
  def change
    add_column :exposures, :setup_code, :serial
  end
end

此示例适用于PostgreSQL。在MySQL上,仅当您使用MyISAM而不是INNODB时,才可以创建辅助自动递增列(为什么?),因此您需要找到其他解决方案。

由于ID列会自动递增,因此您可以使用它。

在Ruby中将整数映射到字母是非常简单的:

irb(main):008:0> ALPHABET = ('A'..'Z').to_a.unshift(nil)
=> [nil, "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]
irb(main):009:0> ALPHABET[1]
=> "A"
irb(main):010:0> ALPHABET[26]
=> "Z"

在模型中,我们可以使用自定义的setter和getter处理值的铸造:

class Exposure < ApplicationRecord
  # This creates a array of the 24 ASCII letters A..Z
  # adding null to the beginning lets us treat it as a 1 indexed array
  ALPHABET = ('A'..'Z').to_a.unshift(nil)

  def setup_code
    # the default value here handles out of range values
    self.class.integer_to_letter(super || 0) || "default_value"
  end

  def setup_code=(value)
    super self.class.integer_to_letter(value)
  end

  def self.integer_to_letter(integer)
    ALPHABET[integer]
  end

  def self.letter_to_integer(letter)
    ALPHABET.index(letter)
  end
end

具有自动递增的列和数据库默认值的陷阱是插入记录时未填充该列:

irb(main):005:0> e = Exposure.create
   (0.2ms)  BEGIN
  Exposure Create (0.7ms)  INSERT INTO "exposures" ("created_at", "updated_at") VALUES ($1, $2) RETURNING "id"  [["created_at", "2018-09-27 12:26:24.672016"], ["updated_at", "2018-09-27 12:26:24.672016"]]
   (0.6ms)  COMMIT
=> #<Exposure id: 3, created_at: "2018-09-27 12:26:24", updated_at: "2018-09-27 12:26:24", setup_code: nil>
irb(main):006:0> e.setup_code
=> "default_value"
irb(main):007:0> e.reload
  Exposure Load (0.7ms)  SELECT  "exposures".* FROM "exposures" WHERE "exposures"."id" = $1 LIMIT $2  [["id", 3], ["LIMIT", 1]]
=> #<Exposure id: 3, created_at: "2018-09-27 12:26:24", updated_at: "2018-09-27 12:26:24", setup_code: 3>
irb(main):008:0> e.setup_code
=> "C"

由于ActiveRecord在插入时仅返回id列。

将现有列更改为序列

这并没有真正改变列-而是使用旧的重命名技巧来创建新列并将旧值移动到新列。

# rename the old column
class RenameExposuresSequenceCode < ActiveRecord::Migration[5.2]
  def change
    rename_column :exposures, :setup_code, :old_setup_code
  end
end

# add the new column
class AddSequencedSetupCodeToExposures < ActiveRecord::Migration[5.2]
  def change
    add_column :exposures, :setup_code, :serial
  end
end

# convert the existing values 
class ConvertOldSetupCodes < ActiveRecord::Migration[5.2]
  def up
    Exposure.find_each do |e|
      converted_code = Exposure.letter_to_integer(e.old_setup_code)
      e.update_attribute(setup_code: converted_code) if converted_code
    end
  end

  def down
    Exposure.find_each do |e|
      converted_code = Exposure.integer_to_letter(e.setup_code)
      e.update_attribute(old_setup_code: converted_code) if converted_code
    end
  end
end

# remove the old column
class RemoveOldSetupCodeFromExposures < ActiveRecord::Migration[5.2]
  def change
    remove_column :exposures, :old_setup_code, :string
  end
end

答案 1 :(得分:2)

在Postgres中,实际上有一种很酷的方法,可以将计算出的默认值应用于列。

首先,我们要定义一个Postgres函数,该函数为您提供一个整数(1 = A,26​​ = Z)的字母,以便我们可以得到一个给定ID的字母。

class CreateIdToLetterFunction < ActiveRecord::Migration[5.2]
  def up
    execute <<-SQL
    CREATE OR REPLACE FUNCTION id_to_letter(integer) RETURNS varchar
      AS 'select chr(64 + $1)' 
      LANGUAGE SQL
      IMMUTABLE
      RETURNS NULL ON NULL INPUT;
    SQL
  end

  def down
    execute <<-SQL
      DROP FUNCTION id_to_letter(integer);
    SQL
  end
end

chr(int)返回具有给定ASCII码的字符。在ASCII中,A的值为65,因此我们需要将该值填充为64。

然后我们添加对曝光量进行计数的功能,以便我们可以使用默认值:

class AddExposureCountFunction < ActiveRecord::Migration[5.2]
  def up
    execute <<-SQL
      CREATE OR REPLACE FUNCTION count_exposures() RETURNS bigint
        AS 'select count(*) from exposures'
        LANGUAGE SQL
        VOLATILE;
    SQL
  end

  def down
    execute <<-SQL
      DROP FUNCTION count_exposures();
    SQL
  end
end

然后,我们要更改Exposures.setup_code列以添加默认值。

class AddDefaultSetupCodeToExposures < ActiveRecord::Migration[5.2]
  def change
    change_column_default(
      :exposures,
      :setup_code,
      from: nil,
      to: -> {"id_to_letter(CAST(count_exposures() AS integer) + 1)"}
    )
  end
end

我们将新的默认值包装在lambda(->{})中,因为这告诉ActiveRecord该值不是文字值,应作为SQL添加到语句中。

由于这是在数据库级别处理的,因此不需要其他模型代码。请注意,有关数据库设置的默认值的警告适用-在从数据库重新加载实例之前,该值将为nil。

已添加

但是,如果您希望从注释中获得功能上等同于不良代码的功能,则只需:

class Exposure < ApplicationRecord
  SETUP_CODES = %w{ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z A2 B2 C2 D2 E2 F2 G2 H2 }
  def self.next_setup_code(current_code)
    if !SETUP_CODES.index(current_code) || current_code == SETUP_CODES.last
      "error"
    else
      SETUP_CODES[ SETUP_CODES.index(current_code) + 1 ]
    end 
  end
end

答案 2 :(得分:0)

class Exposure < ApplicationRecord
  NO_FORMAT = "A"
  before_create :create_setup_code

  def next_setup_code
    (Exposure.count > 0 ? Exposure.last.setup_code : Exposure::NO_FORMAT).succ
  end

  def create_setup_code
    self.setup_code = next_setup_code
  end
end

您可以创建任意数量的