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

Ruby 与数据库交互基础

2021-09-072.3k 阅读

一、Ruby 数据库交互概述

在现代软件开发中,数据库交互是极为关键的一环。Ruby 作为一种功能强大且灵活的编程语言,提供了多种方式与不同类型的数据库进行交互。无论是关系型数据库(如 MySQL、PostgreSQL),还是非关系型数据库(如 MongoDB),Ruby 都能与之建立连接并执行各种操作,如数据的增删改查(CRUD)。

通过与数据库交互,Ruby 应用程序能够持久化存储数据,使得数据在程序关闭后依然能够保存,并在后续运行中被读取和处理。这对于构建各种类型的应用,从简单的命令行工具到复杂的 web 应用程序,都是不可或缺的。

二、Ruby 数据库交互的常用库

  1. ActiveRecord
    • ActiveRecord 是 Ruby on Rails 框架的一部分,但它也可以独立使用。它基于对象关系映射(Object - Relational Mapping,ORM)的思想,将数据库中的表映射为 Ruby 类,表中的行映射为类的实例对象,列映射为对象的属性。
    • 优点
      • 极大地简化了数据库操作,开发人员可以使用 Ruby 代码来操作数据库,而无需编写大量的 SQL 语句。
      • 提供了数据库事务支持,确保一系列数据库操作要么全部成功,要么全部失败,保证数据的一致性。
      • 具有良好的跨数据库兼容性,支持多种关系型数据库,如 MySQL、PostgreSQL、SQLite 等。
    • 缺点
      • 由于是基于 ORM 的,对于复杂的 SQL 查询,可能需要花费更多的精力去将其转换为 ActiveRecord 的语法,有时性能可能不如直接编写 SQL。
      • 对于不熟悉 ORM 概念的开发人员,可能需要一定的学习成本。
    • 示例:假设我们有一个名为 users 的数据库表,包含 idnameemail 字段。首先,我们需要配置数据库连接。如果使用 SQLite 数据库,可以这样设置:
require 'active_record'
ActiveRecord::Base.establish_connection(
  adapter:  'sqlite3',
  database: 'test.db'
)

class User < ActiveRecord::Base
end

# 创建一个新用户
new_user = User.new(name: 'John Doe', email: 'johndoe@example.com')
new_user.save

# 查找所有用户
users = User.all
users.each do |user|
  puts "Name: #{user.name}, Email: #{user.email}"
end
  1. Sequel
    • Sequel 是另一个流行的 Ruby 数据库库,它提供了一个通用的数据库抽象层,支持多种数据库类型。与 ActiveRecord 不同,Sequel 更加轻量级,并且在 SQL 生成方面给予开发人员更多的控制权。
    • 优点
      • 灵活性高,开发人员既可以使用类似 ActiveRecord 的 DSL(Domain - Specific Language)来操作数据库,也可以直接编写 SQL 语句,在复杂查询场景下更具优势。
      • 性能较好,由于其轻量级的设计,在处理大量数据时可能比 ActiveRecord 表现更优。
      • 支持多种数据库,包括关系型和一些非关系型数据库。
    • 缺点
      • 相对 ActiveRecord 来说,文档和社区支持在某些方面可能没有那么丰富,对于初学者可能不太友好。
      • 由于灵活性高,可能需要开发人员对数据库概念有更深入的理解,以便更好地利用其功能。
    • 示例:使用 Sequel 连接到 PostgreSQL 数据库并进行一些操作。假设我们有一个 products 表,包含 idnameprice 字段。
require'sequel'
DB = Sequel.connect(
  adapter:  'postgresql',
  database: 'test_db',
  user: 'postgres',
  password: 'password'
)

class Product < Sequel::Model(DB[:products])
end

# 创建一个新产品
new_product = Product.create(name: 'Widget', price: 10.99)

# 查找价格大于 5 的产品
products = Product.where(price: Sequel.lit('> 5')).all
products.each do |product|
  puts "Name: #{product.name}, Price: #{product.price}"
end
  1. DataMapper
    • DataMapper 也是一个 ORM 库,它旨在提供一种简单、直观的方式来进行数据库交互。它强调约定优于配置(Convention over Configuration)的原则,减少了开发人员需要编写的配置代码。
    • 优点
      • 简单易用,对于初学者来说容易上手,特别是那些习惯约定优于配置理念的开发人员。
      • 提供了对多种数据库的支持,并且在不同数据库之间切换相对容易。
      • 支持数据验证,确保插入或更新到数据库中的数据符合一定的规则。
    • 缺点
      • 相对 ActiveRecord 而言,社区活跃度可能稍低,这可能导致在遇到问题时获取支持的难度稍大。
      • 在处理复杂业务逻辑和大规模数据时,可能不如 ActiveRecord 和 Sequel 灵活和高效。
    • 示例:假设我们有一个 books 表,包含 idtitleauthor 字段。使用 DataMapper 进行操作。
require 'data_mapper'
DataMapper.setup(:default,'sqlite3:test.db')

class Book
  include DataMapper::Resource
  property :id, Serial
  property :title, String
  property :author, String
end

DataMapper.finalize.auto_upgrade!

# 创建一本新书
new_book = Book.new(title: 'Ruby Basics', author: 'Jane Smith')
new_book.save

# 查找所有书籍
books = Book.all
books.each do |book|
  puts "Title: #{book.title}, Author: #{book.author}"
end

三、与关系型数据库交互

  1. MySQL
    • 安装和配置:首先,需要安装 mysql2 gem,这是 Ruby 与 MySQL 交互的主要库。可以通过 gem install mysql2 命令进行安装。安装完成后,配置数据库连接。假设 MySQL 运行在本地,用户名是 root,密码是 password,数据库名为 test_db
require'mysql2'
client = Mysql2::Client.new(
  username: 'root',
  password: 'password',
  database: 'test_db',
  host: '127.0.0.1'
)
  • 基本操作
    • 插入数据:假设我们有一个 employees 表,包含 nameagedepartment 字段。
name = 'Alice'
age = 30
department = 'Engineering'
client.query("INSERT INTO employees (name, age, department) VALUES ('#{name}', #{age}, '#{department}')")

这里需要注意 SQL 注入的风险,更好的方式是使用参数化查询:

name = 'Bob'
age = 25
department = 'Marketing'
stmt = client.prepare('INSERT INTO employees (name, age, department) VALUES (?,?,?)')
stmt.execute(name, age, department)
 - **查询数据**:
results = client.query('SELECT * FROM employees')
results.each do |row|
  puts "Name: #{row['name']}, Age: #{row['age']}, Department: #{row['department']}"
end
 - **更新数据**:
new_age = 31
name = 'Alice'
stmt = client.prepare('UPDATE employees SET age =? WHERE name =?')
stmt.execute(new_age, name)
 - **删除数据**:
name = 'Bob'
stmt = client.prepare('DELETE FROM employees WHERE name =?')
stmt.execute(name)
  1. PostgreSQL
    • 安装和配置:安装 pg gem,使用 gem install pg 命令。配置连接,假设 PostgreSQL 运行在本地,用户名是 postgres,密码是 password,数据库名为 test_db
require 'pg'
conn = PG.connect(
  user: 'postgres',
  password: 'password',
  dbname: 'test_db',
  host: '127.0.0.1'
)
  • 基本操作
    • 插入数据:假设我们有一个 customers 表,包含 nameemailphone 字段。
name = 'Charlie'
email = 'charlie@example.com'
phone = '123 - 456 - 7890'
conn.exec_params('INSERT INTO customers (name, email, phone) VALUES ($1, $2, $3)', [name, email, phone])
 - **查询数据**:
results = conn.exec('SELECT * FROM customers')
results.each do |row|
  puts "Name: #{row['name']}, Email: #{row['email']}, Phone: #{row['phone']}"
end
 - **更新数据**:
new_email = 'charlie_new@example.com'
name = 'Charlie'
conn.exec_params('UPDATE customers SET email = $1 WHERE name = $2', [new_email, name])
 - **删除数据**:
name = 'Charlie'
conn.exec_params('DELETE FROM customers WHERE name = $1', [name])
  1. SQLite
    • 安装和配置:Ruby 标准库中已经包含了对 SQLite 的支持,无需额外安装 gem。配置连接,假设数据库文件名为 test.db
require'sqlite3'
db = SQLite3::Database.new('test.db')
  • 基本操作
    • 插入数据:假设我们有一个 tasks 表,包含 titledescriptioncompleted 字段。
title = 'Learn Ruby'
description = 'Study Ruby programming language'
completed = false
db.execute('INSERT INTO tasks (title, description, completed) VALUES (?,?,?)', title, description, completed)
 - **查询数据**:
results = db.execute('SELECT * FROM tasks')
results.each do |row|
  puts "Title: #{row[0]}, Description: #{row[1]}, Completed: #{row[2]}"
end
 - **更新数据**:
new_title = 'Master Ruby'
title = 'Learn Ruby'
db.execute('UPDATE tasks SET title =? WHERE title =?', new_title, title)
 - **删除数据**:
title = 'Master Ruby'
db.execute('DELETE FROM tasks WHERE title =?', title)

四、与非关系型数据库交互

  1. MongoDB
    • 安装和配置:安装 mongo gem,使用 gem install mongo 命令。配置连接,假设 MongoDB 运行在本地,默认端口 27017。
require'mongo'
client = Mongo::Client.new(['mongodb://127.0.0.1:27017'], database: 'test_db')
collection = client[:users]
  • 基本操作
    • 插入数据
user = { name: 'David', age: 28, email: 'david@example.com' }
collection.insert_one(user)
 - **查询数据**:
results = collection.find(age: { '$gt': 25 })
results.each do |user|
  puts "Name: #{user['name']}, Age: #{user['age']}, Email: #{user['email']}"
end

这里 $gt 是 MongoDB 的查询操作符,表示“大于”。 - 更新数据

new_email = 'david_new@example.com'
name = 'David'
collection.update_one({ name: name }, { '$set': { email: new_email } })

$set 操作符用于设置字段的值。 - 删除数据

name = 'David'
collection.delete_one({ name: name })
  1. Redis
    • 安装和配置:安装 redis - ruby gem,使用 gem install redis - ruby 命令。配置连接,假设 Redis 运行在本地,默认端口 6379。
require'redis'
redis = Redis.new(host: '127.0.0.1', port: 6379)
  • 基本操作
    • 设置键值对
redis.set('name', 'Eva')
 - **获取值**:
name = redis.get('name')
puts name
 - **删除键值对**:
redis.del('name')
  • 更复杂的操作:Redis 支持多种数据结构,如列表、哈希等。例如,使用哈希存储用户信息:
user_key = 'user:1'
redis.hset(user_key, 'name', 'Frank')
redis.hset(user_key, 'age', 32)
user = redis.hgetall(user_key)
puts "Name: #{user['name']}, Age: #{user['age']}"
redis.del(user_key)

五、事务处理

  1. 关系型数据库中的事务
    • ActiveRecord 中的事务:在 ActiveRecord 中,事务处理非常简单。假设我们要在一个事务中创建两个用户。
ActiveRecord::Base.transaction do
  user1 = User.new(name: 'User1', email: 'user1@example.com')
  user1.save!
  user2 = User.new(name: 'User2', email: 'user2@example.com')
  user2.save!
end

如果其中任何一个 save! 操作失败,整个事务将回滚,两个用户都不会被保存到数据库中。这里 save! 方法在保存失败时会抛出异常,从而触发事务回滚。

  • Sequel 中的事务:使用 Sequel 进行事务处理。假设我们有一个 accounts 表,包含 balance 字段,我们要从一个账户向另一个账户转账。
DB.transaction do
  from_account = Account.where(id: 1).first
  to_account = Account.where(id: 2).first
  amount = 100
  from_account.update(balance: from_account.balance - amount)
  to_account.update(balance: to_account.balance + amount)
end

如果任何一个 update 操作失败,事务将回滚,账户余额不会发生变化。 2. 非关系型数据库中的事务

  • MongoDB 中的事务:从 MongoDB 4.0 开始支持多文档事务。假设我们有两个集合 ordersorder_items,我们要在一个事务中创建一个订单及其相关的订单项。
session = client.start_session
session.with_transaction do
  order = { order_number: '12345', total: 100 }
  order_id = session[:orders].insert_one(order).inserted_id
  item1 = { order_id: order_id, product: 'Product1', quantity: 1, price: 50 }
  item2 = { order_id: order_id, product: 'Product2', quantity: 1, price: 50 }
  session[:order_items].insert_many([item1, item2])
end

如果在事务中任何插入操作失败,整个事务将回滚,订单和订单项都不会被保存。

  • Redis 中的事务:Redis 提供了 MULTIEXECDISCARD 命令来实现事务。假设我们要在 Redis 中对一个计数器进行递增和递减操作。
redis.multi do
  redis.incr('counter')
  redis.decr('counter')
end

MULTIEXEC 之间的命令会被放入队列,然后原子性地执行。如果在 EXEC 执行前调用 DISCARD,则队列中的命令会被取消。

六、性能优化

  1. 批量操作
    • 在关系型数据库中,例如使用 ActiveRecord 插入多条记录时,可以使用 create 方法的数组形式。假设我们有一个 posts 表,要插入多篇文章。
posts = [
  { title: 'Post1', content: 'Content of Post1' },
  { title: 'Post2', content: 'Content of Post2' }
]
Post.create(posts)

这样比逐个插入记录效率更高,因为它只执行一次数据库插入操作,而不是多次。

  • 在 MongoDB 中,插入多个文档可以使用 insert_many 方法。
documents = [
  { name: 'Doc1', value: 10 },
  { name: 'Doc2', value: 20 }
]
collection.insert_many(documents)
  1. 索引优化
    • 在关系型数据库中,合理创建索引可以显著提高查询性能。例如,在 users 表中,如果经常根据 email 字段进行查询,可以创建如下索引:
# 使用 ActiveRecord 迁移创建索引
class AddIndexToUsersEmail < ActiveRecord::Migration[6.0]
  def change
    add_index :users, :email
  end
end
  • 在 MongoDB 中,也可以创建索引来优化查询。假设在 products 集合中经常根据 price 字段查询,可以这样创建索引:
collection.create_index({ price: 1 })

这里 1 表示升序索引, - 1 表示降序索引。 3. 缓存

  • 在 Ruby 应用程序中,可以使用缓存来减少数据库查询次数。例如,使用 ActiveSupport::Cache 来缓存查询结果。假设我们有一个查询所有用户的方法,并且这个查询不经常变化。
def get_all_users
  Rails.cache.fetch('all_users') do
    User.all
  end
end

第一次调用 get_all_users 方法时,会查询数据库并将结果缓存起来。后续调用时,如果缓存未过期,直接从缓存中获取数据,从而提高性能。

  • 在 Redis 中,也可以将数据库查询结果缓存起来。例如,将查询某个用户的结果缓存到 Redis 中。
def get_user(user_id)
  user = redis.get("user:#{user_id}")
  if user
    JSON.parse(user)
  else
    user = User.find(user_id).to_json
    redis.set("user:#{user_id}", user)
    JSON.parse(user)
  end
end

七、错误处理

  1. 关系型数据库错误处理
    • 在使用 mysql2 gem 时,如果数据库连接失败,会抛出 Mysql2::Error 异常。例如:
begin
  client = Mysql2::Client.new(
    username: 'root',
    password: 'wrong_password',
    database: 'test_db',
    host: '127.0.0.1'
  )
rescue Mysql2::Error => e
  puts "Database connection error: #{e.message}"
end
  • 在 ActiveRecord 中,如果保存数据失败,会抛出 ActiveRecord::RecordInvalid 异常。例如:
begin
  user = User.new(name: '', email: 'invalid_email')
  user.save!
rescue ActiveRecord::RecordInvalid => e
  puts "Data save error: #{e.message}"
end
  1. 非关系型数据库错误处理
    • 在使用 mongo gem 时,如果插入文档失败,可能会抛出 Mongo::Error::OperationFailure 异常。例如:
begin
  collection.insert_one({ name: nil })
rescue Mongo::Error::OperationFailure => e
  puts "MongoDB insert error: #{e.message}"
end
  • 在使用 redis - ruby gem 时,如果执行命令失败,会抛出 Redis::CommandError 异常。例如:
begin
  redis.get('non_existent_key', 'WRONGTYPE Operation against a key holding the wrong kind of value')
rescue Redis::CommandError => e
  puts "Redis command error: #{e.message}"
end

通过以上对 Ruby 与数据库交互的各个方面的介绍,包括常用库、与不同类型数据库的交互、事务处理、性能优化以及错误处理等,开发人员可以根据具体的应用需求,选择合适的方式来实现高效、可靠的数据库交互功能。无论是构建小型的本地应用,还是大型的分布式系统,这些知识都将是非常有价值的。