MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Ruby on Rails 模型与数据库交互

2021-12-066.7k 阅读

Ruby on Rails 模型与数据库交互基础

Rails 模型简介

在 Ruby on Rails 框架中,模型(Model)处于 MVC(Model-View-Controller)架构的核心位置。模型主要负责与数据库进行交互,处理业务逻辑以及维护数据的完整性。每个模型通常对应数据库中的一张表,模型类的实例则代表表中的一行记录。

例如,假设我们正在开发一个博客应用,我们可能会创建一个 Post 模型,它对应数据库中的 posts 表。这个 Post 模型将包含与博客文章相关的属性和方法,比如文章标题、内容、发布日期等,并且负责执行诸如保存新文章、更新现有文章、删除文章等操作。

数据库连接配置

在 Rails 应用能够与数据库交互之前,需要先配置好数据库连接。Rails 支持多种数据库,如 MySQL、PostgreSQL、SQLite 等。配置文件位于 config/database.yml

以 SQLite 为例,其默认配置如下:

default: &default
  adapter: sqlite3
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  <<: *default
  database: db/development.sqlite3

test:
  <<: *default
  database: db/test.sqlite3

production:
  <<: *default
  database: db/production.sqlite3

上述配置中,adapter 指定了使用的数据库类型为 SQLite3,pool 设置了数据库连接池的大小,timeout 表示数据库操作的超时时间。不同环境(开发、测试、生产)可以有不同的数据库配置,例如在生产环境中可能会使用 PostgreSQL 数据库,配置如下:

production:
  adapter: postgresql
  encoding: unicode
  database: your_production_database
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: your_username
  password: your_password
  host: your_host
  port: your_port

配置好数据库连接后,Rails 应用就可以根据环境加载相应的配置,从而与数据库建立连接。

创建模型与数据库迁移

  1. 生成模型 使用 Rails 命令行工具可以方便地生成模型。例如,要生成前面提到的 Post 模型,可以在终端执行以下命令:
rails generate model Post title:string content:text published_at:datetime

这条命令会在 app/models 目录下生成一个 post.rb 文件,同时在 db/migrate 目录下生成一个迁移文件。迁移文件用于在数据库中创建对应的 posts 表。 2. 数据库迁移 迁移文件定义了如何修改数据库结构。打开生成的迁移文件(文件名类似于 20230801123456_create_posts.rb),内容如下:

class CreatePosts < ActiveRecord::Migration[7.0]
  def change
    create_table :posts do |t|
      t.string :title
      t.text :content
      t.datetime :published_at

      t.timestamps
    end
  end
end

create_table 方法用于创建 posts 表,t.stringt.textt.datetime 等方法定义了表中的列及其数据类型。t.timestamps 会自动添加 created_atupdated_at 两个时间戳列,记录记录的创建和更新时间。

要将迁移应用到数据库中,在终端执行:

rails db:migrate

如果需要撤销迁移,可以执行:

rails db:rollback

模型与数据库的基本交互操作

创建记录

在 Rails 模型中,可以通过多种方式创建数据库记录。

  1. 使用 newsave 方法
post = Post.new(title: "First Post", content: "This is the content of the first post.", published_at: Time.now)
if post.save
  puts "Post created successfully!"
else
  puts "Error creating post: #{post.errors.full_messages.join(', ')}"
end

在上述代码中,首先使用 Post.new 创建一个 Post 模型的新实例,并设置其属性。然后调用 save 方法将实例保存到数据库中。如果保存成功,save 方法返回 true,否则返回 false,并可以通过 post.errors 获取错误信息。 2. 使用 create 方法

post = Post.create(title: "Second Post", content: "Content of the second post.", published_at: Time.now)
if post.persisted?
  puts "Post created successfully!"
else
  puts "Error creating post: #{post.errors.full_messages.join(', ')}"
end

create 方法会同时创建实例并保存到数据库中。可以通过 persisted? 方法检查实例是否已持久化到数据库。

读取记录

  1. 获取所有记录 要获取 Post 模型对应的所有数据库记录,可以使用 all 方法:
posts = Post.all
posts.each do |post|
  puts "Title: #{post.title}, Content: #{post.content}, Published At: #{post.published_at}"
end

all 方法返回一个包含所有 Post 实例的数组,可以通过遍历数组来访问每个实例的属性。 2. 根据条件查询记录 使用 where 方法可以根据条件查询记录。例如,要查询所有已发布的文章(published_at 不为 nil):

published_posts = Post.where("published_at IS NOT NULL")
published_posts.each do |post|
  puts "Published Post - Title: #{post.title}"
end

还可以使用更复杂的条件,比如查询标题包含特定字符串的文章:

matching_posts = Post.where("title LIKE ?", "%ruby%")
matching_posts.each do |post|
  puts "Matching Post - Title: #{post.title}"
end
  1. 获取单个记录 使用 find 方法可以根据主键(通常是 id)获取单个记录:
begin
  post = Post.find(1)
  puts "Post with id 1 - Title: #{post.title}"
rescue ActiveRecord::RecordNotFound
  puts "Post with id 1 not found."
end

如果找不到指定 id 的记录,find 方法会抛出 ActiveRecord::RecordNotFound 异常,因此需要进行异常处理。

更新记录

  1. 更新单个属性
post = Post.find(1)
post.title = "Updated Title"
if post.save
  puts "Post title updated successfully."
else
  puts "Error updating post title: #{post.errors.full_messages.join(', ')}"
end

首先通过 find 方法获取要更新的记录,然后修改其属性,最后调用 save 方法保存更改。 2. 更新多个属性

post = Post.find(1)
post.update(title: "New Title", content: "New Content", published_at: Time.now)
if post.persisted?
  puts "Post updated successfully."
else
  puts "Error updating post: #{post.errors.full_messages.join(', ')}"
end

update 方法可以同时更新多个属性,并且会自动保存更改。

删除记录

使用 destroy 方法可以删除数据库记录:

post = Post.find(1)
if post.destroy
  puts "Post deleted successfully."
else
  puts "Error deleting post: #{post.errors.full_messages.join(', ')}"
end

destroy 方法会从数据库中删除对应的记录,并返回 true 表示删除成功,否则返回 false 并可以通过 errors 获取错误信息。还可以使用 delete 方法直接删除记录,但是 delete 方法不会触发模型的回调函数,而 destroy 方法会触发。

关联关系与数据库交互

一对一关联

在 Rails 中,一对一关联表示一个模型的实例与另一个模型的实例之间存在唯一的对应关系。例如,一个用户(User)可以有一个唯一的联系方式(Contact)。

  1. 定义关联user.rb 模型中:
class User < ApplicationRecord
  has_one :contact
end

contact.rb 模型中:

class Contact < ApplicationRecord
  belongs_to :user
end
  1. 数据库迁移 创建 users 表的迁移文件:
class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :name
      t.timestamps
    end
  end
end

创建 contacts 表的迁移文件:

class CreateContacts < ActiveRecord::Migration[7.0]
  def change
    create_table :contacts do |t|
      t.string :phone_number
      t.string :email
      t.belongs_to :user, foreign_key: true
      t.timestamps
    end
  end
end

注意在 contacts 表中通过 t.belongs_to :user, foreign_key: true 添加了外键关联到 users 表。 3. 使用关联

user = User.create(name: "John")
contact = user.build_contact(phone_number: "1234567890", email: "john@example.com")
if contact.save
  puts "Contact created for user."
else
  puts "Error creating contact: #{contact.errors.full_messages.join(', ')}"
end

# 获取用户的联系方式
user_contact = user.contact
puts "User's phone number: #{user_contact.phone_number}" if user_contact

build_contact 方法创建一个新的 Contact 实例但不保存到数据库,需要调用 save 方法。通过 user.contact 可以获取用户的联系方式。

一对多关联

一对多关联是比较常见的关系,比如一个用户可以有多个文章。

  1. 定义关联user.rb 模型中:
class User < ApplicationRecord
  has_many :posts
end

post.rb 模型中:

class Post < ApplicationRecord
  belongs_to :user
end
  1. 数据库迁移 users 表迁移同前面一对一关联中的 users 表迁移。posts 表迁移文件如下:
class CreatePosts < ActiveRecord::Migration[7.0]
  def change
    create_table :posts do |t|
      t.string :title
      t.text :content
      t.belongs_to :user, foreign_key: true
      t.timestamps
    end
  end
end
  1. 使用关联
user = User.create(name: "Jane")
post1 = user.posts.build(title: "First Post by Jane", content: "Content of the first post by Jane.")
post2 = user.posts.build(title: "Second Post by Jane", content: "Content of the second post by Jane.")
if post1.save && post2.save
  puts "Posts created for user."
else
  puts "Error creating posts: #{post1.errors.full_messages.join(', ')} #{post2.errors.full_messages.join(', ')}"
end

# 获取用户的所有文章
user_posts = user.posts
user_posts.each do |post|
  puts "User's Post - Title: #{post.title}"
end

user.posts.build 方法创建新的 Post 实例并关联到用户。通过 user.posts 可以获取用户的所有文章。

多对多关联

多对多关联表示两个模型之间存在多个对应多个的关系,比如一篇文章可以有多个标签,一个标签可以应用到多篇文章。

  1. 定义关联post.rb 模型中:
class Post < ApplicationRecord
  has_and_belongs_to_many :tags
end

tag.rb 模型中:

class Tag < ApplicationRecord
  has_and_belongs_to_many :posts
end
  1. 数据库迁移 除了 posts 表和 tags 表的迁移,还需要创建一个连接表的迁移:
class CreatePostTags < ActiveRecord::Migration[7.0]
  def change
    create_table :post_tags do |t|
      t.belongs_to :post, foreign_key: true
      t.belongs_to :tag, foreign_key: true
      t.timestamps
    end
  end
end
  1. 使用关联
post = Post.create(title: "Post with Tags", content: "This post has multiple tags.")
tag1 = Tag.create(name: "ruby")
tag2 = Tag.create(name: "rails")
post.tags << tag1
post.tags << tag2
post.save

# 获取文章的所有标签
post_tags = post.tags
post_tags.each do |tag|
  puts "Post's Tag - Name: #{tag.name}"
end

post.tags << tag1 方法将标签关联到文章,通过 post.tags 可以获取文章的所有标签。

高级数据库交互技巧

事务处理

事务是一组数据库操作,要么全部成功执行,要么全部失败回滚。在 Rails 中,可以使用 ActiveRecord::Base.transaction 方法来处理事务。 例如,假设我们要在创建一个用户的同时创建一个与之关联的联系方式,并且这两个操作要在一个事务中进行:

begin
  ActiveRecord::Base.transaction do
    user = User.create(name: "Alice")
    contact = user.build_contact(phone_number: "0987654321", email: "alice@example.com")
    unless contact.save
      raise ActiveRecord::Rollback
    end
  end
  puts "User and contact created successfully."
rescue ActiveRecord::Rollback
  puts "Transaction rolled back. Error creating user or contact."
end

在上述代码中,如果 contact.save 失败,会抛出 ActiveRecord::Rollback 异常,从而回滚整个事务,确保数据库状态的一致性。

自定义 SQL 查询

虽然 Rails 的 ActiveRecord 提供了丰富的方法来进行数据库操作,但在某些情况下,可能需要执行自定义的 SQL 查询。

  1. 使用 find_by_sql 方法
sql = "SELECT * FROM posts WHERE published_at IS NOT NULL ORDER BY published_at DESC LIMIT 10"
posts = Post.find_by_sql(sql)
posts.each do |post|
  puts "Title: #{post.title}, Published At: #{post.published_at}"
end

find_by_sql 方法执行自定义的 SQL 查询并返回结果集,结果集是 Post 模型实例的数组。 2. 使用 connection.execute 方法 如果只是执行一些不返回结果的 SQL 语句,比如更新操作,可以使用 connection.execute 方法:

ActiveRecord::Base.connection.execute("UPDATE posts SET content = 'Updated content' WHERE id = 1")

这种方式直接执行 SQL 语句,不会返回模型实例,适用于简单的数据库修改操作。

数据库索引优化

索引可以显著提高数据库查询的性能。在 Rails 中,可以在迁移文件中添加索引。 例如,为 posts 表的 title 列添加索引:

class AddIndexToPostsTitle < ActiveRecord::Migration[7.0]
  def change
    add_index :posts, :title
  end
end

执行 rails db:migrate 后,数据库会为 posts 表的 title 列创建索引,从而加快对 title 列的查询速度。如果需要创建唯一索引,可以使用:

class AddUniqueIndexToPostsSlug < ActiveRecord::Migration[7.0]
  def change
    add_index :posts, :slug, unique: true
  end
end

这样可以确保 slug 列的值在表中是唯一的,同时也能加快基于 slug 列的查询。

数据验证与数据库约束

  1. 模型层数据验证 在 Rails 模型中,可以使用多种验证方法来确保数据的有效性。例如,为 Post 模型添加标题不能为空的验证:
class Post < ApplicationRecord
  validates :title, presence: true
end

这样在保存 Post 实例时,如果标题为空,save 方法会返回 false,并可以通过 errors 获取错误信息。 2. 数据库约束 除了模型层的验证,数据库本身也可以设置约束。例如,在创建 posts 表时,可以在迁移文件中添加 NOT NULL 约束:

class CreatePosts < ActiveRecord::Migration[7.0]
  def change
    create_table :posts do |t|
      t.string :title, null: false
      t.text :content
      t.datetime :published_at
      t.timestamps
    end
  end
end

这样即使在模型层验证被绕过,数据库也会拒绝插入标题为空的记录,从而保证数据的完整性。

模型回调与数据库交互

回调类型

  1. 创建前回调(before_create before_create 回调在模型实例保存到数据库之前触发。例如,在保存 Post 模型实例之前,我们可能想要自动生成一个 slug(用于 URL 友好的标识):
class Post < ApplicationRecord
  before_create :generate_slug

  def generate_slug
    self.slug = title.parameterize
  end
end

上述代码中,before_create 回调调用 generate_slug 方法,该方法根据文章标题生成一个 slug。 2. 创建后回调(after_create after_create 回调在模型实例成功保存到数据库之后触发。例如,在创建一篇文章后,我们可能想要发送一封通知邮件:

class Post < ApplicationRecord
  after_create :send_notification_email

  def send_notification_email
    # 邮件发送逻辑,这里假设使用 ActionMailer
    PostMailer.new_post_notification(self).deliver_now
  end
end
  1. 更新前回调(before_update before_update 回调在模型实例更新到数据库之前触发。比如,在更新文章时,我们可能想要检查标题是否有变化,如果有变化则重新生成 slug:
class Post < ApplicationRecord
  before_update :update_slug_if_title_changed

  def update_slug_if_title_changed
    if title_changed?
      self.slug = title.parameterize
    end
  end
end
  1. 更新后回调(after_update after_update 回调在模型实例成功更新到数据库之后触发。例如,在文章更新后,我们可能想要更新文章的缓存:
class Post < ApplicationRecord
  after_update :update_post_cache

  def update_post_cache
    # 缓存更新逻辑,这里假设使用 Rails.cache
    Rails.cache.write("post_#{id}", self)
  end
end
  1. 删除前回调(before_destroy before_destroy 回调在模型实例从数据库中删除之前触发。例如,在删除一篇文章之前,我们可能想要删除与之关联的评论:
class Post < ApplicationRecord
  has_many :comments
  before_destroy :delete_comments

  def delete_comments
    comments.destroy_all
  end
end
  1. 删除后回调(after_destroy after_destroy 回调在模型实例成功从数据库中删除之后触发。比如,在删除文章后,我们可能想要清理相关的文件上传:
class Post < ApplicationRecord
  after_destroy :cleanup_uploads

  def cleanup_uploads
    # 文件清理逻辑,假设文章有相关的图片上传
    FileUtils.rm_rf(Rails.root.join('public', 'uploads', "post_#{id}")) if File.directory?(Rails.root.join('public', 'uploads', "post_#{id}"))
  end
end

回调链与优先级

  1. 回调链 Rails 模型的回调可以形成一个链。例如,一个模型可能有多个 before_create 回调,它们会按照定义的顺序依次执行。同样,after_create 回调也会按照顺序执行。
class Post < ApplicationRecord
  before_create :generate_slug
  before_create :set_default_published_at

  def generate_slug
    self.slug = title.parameterize
  end

  def set_default_published_at
    self.published_at ||= Time.now
  end
end

在上述代码中,generate_slug 回调先执行,然后 set_default_published_at 回调执行。 2. 优先级设置 可以通过 prepend_callbackappend_callback 方法来设置回调的优先级。例如,如果想要在已有的 before_create 回调之前添加一个新的回调,可以使用 prepend_callback

class Post < ApplicationRecord
  def new_before_create_callback
    # 新的回调逻辑
  end

  prepend_callback :create, :new_before_create_callback
end

这样 new_before_create_callback 会在其他已定义的 before_create 回调之前执行。如果使用 append_callback,则会在其他 before_create 回调之后执行。

通过合理利用模型回调,可以在数据库交互的不同阶段执行各种业务逻辑,确保数据的一致性和系统的完整性。同时,正确设置回调的优先级可以保证业务逻辑按照预期的顺序执行。

在 Ruby on Rails 开发中,深入理解模型与数据库的交互是构建高效、可靠应用的关键。从基本的增删改查操作到复杂的关联关系处理,再到高级的数据库优化技巧和模型回调应用,每一个环节都对应用的性能和功能有着重要影响。开发者需要根据具体的业务需求,灵活运用这些知识,打造出满足用户需求的优秀 Rails 应用。