如何制作通用的 SQL heredocs?

How to make generic SQL heredocs?

我正在 Ruby 中制作模拟 ORM,存储 'forum' 数据。

我有以下 tables:

users (id, fname, lname)questions (id, title, body, author_id).

a_user.createa_question.create大致相同:

    # aUser#create

    def create
        # adds a user to the database
        raise "user already in db" if self.id

        begin
            QuestionsDB.instance.execute(<<-SQL, self.fname, self.lname)
                insert into users (fname, lname)
                values (?, ?);
            SQL
            self.id = QuestionsDB.instance.last_insert_row_id

            return self.id
        rescue => exception
            raise "failed to create user: #{exception}"
        end
    end
    # aQuestion#create

    def create
        # add question to db
        raise "already asked" if self.id
        raise "title already exists" unless Question.find_by_title(self.title).empty?
        
        begin
            QuestionsDB.instance.execute(<<-SQL, self.title, self.body, self.author_id)
                insert into questions (title, body, author_id)
                values (?, ?, ?);
            SQL
            self.id = QuestionsDB.instance.last_insert_row_id

            return self.id
        rescue => exception
            raise "failed to create question: #{exception}"
        end
    end

我正在写另一个 class,ModelBase,我希望能够说出类似

的内容
ModelBase.create(aUser)

aMB = ModelBase.new(~characteristics_from_user_or_question_object~)
aMB.create

。用户和问题在添加到数据库之前没有 ID。

两个创建的 SQL 基本上采用所有预创建属性并将它们放入相应 table 的所有列中,不包括第一个 'id' 列。有没有一种方法可以概括指令“将此对象的所有属性插入指定的 table 值 ('all but the first')”?

我们可以通过一些 Ruby 元编程实现非常基本的 ORM,但请记住,出于安全原因,您不应该在生产中使用它。实现一个完全可用的 ORM 需要大量工作。

我们可以用instance_variables.

得到一个class的实例变量
class User
  attr_reader :id, :first_name, :last_name

  def initialize(first_name:, last_name:)
    @first_name = first_name
    @last_name = last_name
  end
end

puts User.new(first_name: "Elvis", last_name: "Presley").instance_variables
# @first_name @last_name

请注意开头的 @,我们可以使用简单的 gsub 将其删除。我们还需要拒绝 @id 属性。最后使用 instance_variable_get 我们可以获得实际值。

根据这些信息,我们现在可以实施 columnsvalues 方法。

class OrmBase
  def columns
    instance_variables_without_id.map do |var_name|
      var_name.to_s.gsub /^@/, ''
    end
  end

  def values
    instance_variables_without_id.map do |var_name|
      instance_variable_get var_name
    end
  end

  def instance_variables_without_id
    instance_variables.reject { |var_name| var_name == "@id" }
  end
end

现在我们需要计算 table 名称,我们可以从具有 self.class.name 的对象的 class 名称派生该名称。如果我们把所有东西放在一起,它看起来像这样:

class OrmBase
  def create
    return if @id

    QuestionsDB.instance.execute(to_sql)
    @id = QuestionsDB.instance.last_insert_row_id
    self
  end

  def to_sql
    "INSERT INTO #{table_name} (#{columns.join(', ')}) values (#{values.join(', ')});" 
  end

  private

  def table_name
    "#{self.class.name}s".downcase  
  end

  def columns
    instance_variables_without_id.map do |var_name|
      var_name.to_s.gsub(/^@/, '')
    end
  end

  def values
    instance_variables_without_id.map do |var_name|
      instance_variable_get(var_name)
    end
  end

  def instance_variables_without_id
    instance_variables.reject { |var_name| var_name == "@id" }
  end
end

class User < OrmBase
  attr_reader :id, :first_name, :last_name

  def initialize(first_name:, last_name:)
    @first_name = first_name
    @last_name = last_name
  end
end

class Question < OrmBase
  attr_reader :id, :title

  def initialize(title:)
    @title = title
  end
end

puts User.new(first_name: "Elvis", last_name: "Presley").to_sql
# INSERT INTO users (first_name, last_name) values (Elvis, Presley);
puts Question.new(title: "What is your favorite song?").to_sql
# INSERT INTO questions (title) values (What is your favorite song?);

如开头所述,请只将其用作练习的一部分,因为它不安全(SQL 注入),有错误(例如 table 名称的复数化和转义值)等

另一种实现方法是首先查询数据库 table 具有哪些列名称,然后从中派生 class 属性。例如 ActiveRecord 就是这样做的。