MySQL to ActiveRecord Converter Guide
Complete guide for converting MySQL queries to Rails ActiveRecord with MySQL-specific patterns
What you'll learn:
- • Convert MySQL-specific syntax to ActiveRecord
- • Handle MySQL data types and functions
- • Optimize MySQL queries in Rails applications
- • Work with MySQL-specific features like full-text search
Table of Contents
1. MySQL Setup in Rails
Database Configuration
Gemfile Setup
# Gemfile
gem 'mysql2', '~> 0.5'
Database Configuration
# config/database.yml
default: &default
adapter: mysql2
encoding: utf8mb4
pool: 5
username: root
password:
socket: /tmp/mysql.sock
development:
<<: *default
database: myapp_development
test:
<<: *default
database: myapp_test
production:
<<: *default
database: myapp_production
username: myapp
password:
host:
MySQL-Specific Configuration
# config/database.yml - Additional MySQL options
default: &default
adapter: mysql2
encoding: utf8mb4
collation: utf8mb4_unicode_ci
pool: 5
reconnect: false
strict: true # Enable strict mode
variables:
sql_mode: STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO
2. Basic Query Conversion
Common MySQL to ActiveRecord Patterns
LIMIT and OFFSET
MySQL SQL:
SELECT * FROM users
ORDER BY created_at DESC
LIMIT 10 OFFSET 20;
Rails ActiveRecord:
User.order(created_at: :desc)
.limit(10)
.offset(20)
# Or using page-based pagination
User.order(created_at: :desc)
.limit(10)
.offset(page * 10)
CASE Statements
MySQL SQL:
SELECT *,
CASE
WHEN age < 18 THEN 'Minor'
WHEN age < 65 THEN 'Adult'
ELSE 'Senior'
END as age_group
FROM users;
Rails ActiveRecord:
User.select("*,
CASE
WHEN age < 18 THEN 'Minor'
WHEN age < 65 THEN 'Adult'
ELSE 'Senior'
END as age_group")
Date and Time Functions
MySQL SQL:
SELECT * FROM orders
WHERE DATE(created_at) = CURDATE();
SELECT * FROM users
WHERE YEAR(created_at) = 2024;
Rails ActiveRecord:
Order.where('DATE(created_at) = CURDATE()')
# Better: Use Rails date handling
Order.where(created_at: Date.current.all_day)
User.where('YEAR(created_at) = ?', 2024)
# Better: Use Rails date range
User.where(created_at: Date.new(2024).all_year)
3. MySQL-Specific Functions
String Functions
CONCAT and String Manipulation
MySQL SQL:
SELECT CONCAT(first_name, ' ', last_name) as full_name
FROM users;
SELECT SUBSTRING(email, 1, LOCATE('@', email) - 1) as username
FROM users;
Rails ActiveRecord:
User.select("CONCAT(first_name, ' ', last_name) as full_name")
User.select("SUBSTRING(email, 1, LOCATE('@', email) - 1) as username")
# Or define in model
class User < ApplicationRecord
def full_name
"#{first_name} #{last_name}"
end
end
REGEXP and Pattern Matching
MySQL SQL:
SELECT * FROM users
WHERE email REGEXP '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$';
SELECT * FROM products
WHERE name REGEXP '(phone|mobile|cell)';
Rails ActiveRecord:
User.where('email REGEXP ?', '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
Product.where('name REGEXP ?', '(phone|mobile|cell)')
# Using Rails validations is often better
class User < ApplicationRecord
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
end
Numeric and Aggregate Functions
Mathematical Functions
MySQL SQL:
SELECT
ROUND(price, 2) as rounded_price,
CEIL(price) as ceiling_price,
FLOOR(price) as floor_price
FROM products;
SELECT ABS(profit_loss) as absolute_value
FROM financial_records;
Rails ActiveRecord:
Product.select('
ROUND(price, 2) as rounded_price,
CEIL(price) as ceiling_price,
FLOOR(price) as floor_price
')
FinancialRecord.select('ABS(profit_loss) as absolute_value')
GROUP_CONCAT
MySQL SQL:
SELECT
user_id,
GROUP_CONCAT(tag_name SEPARATOR ', ') as tags
FROM user_tags
GROUP BY user_id;
Rails ActiveRecord:
UserTag.group(:user_id)
.select("user_id, GROUP_CONCAT(tag_name SEPARATOR ', ') as tags")
# Alternative with Rails associations
User.joins(:tags).group(:id)
.select('users.*, GROUP_CONCAT(tags.name SEPARATOR ", ") as tag_list')
4. Data Types and Storage
MySQL Data Types in Rails
Text and Binary Types
# Migration with MySQL-specific types
class CreatePostsWithMysqlTypes < ActiveRecord::Migration[7.0]
def change
create_table :posts do |t|
t.string :title, limit: 255 # VARCHAR(255)
t.text :content, limit: 65535 # TEXT
t.text :long_content, limit: 4294967295 # LONGTEXT
t.binary :thumbnail, limit: 65535 # BLOB
t.json :metadata # JSON (MySQL 5.7+)
t.timestamps
end
end
end
Numeric Types
# Migration with MySQL numeric types
class CreateProductsWithMysqlTypes < ActiveRecord::Migration[7.0]
def change
create_table :products do |t|
t.integer :stock, limit: 1 # TINYINT
t.integer :views, limit: 2 # SMALLINT
t.integer :sales, limit: 3 # MEDIUMINT
t.integer :total_orders, limit: 4 # INT
t.integer :big_number, limit: 8 # BIGINT
t.decimal :price, precision: 10, scale: 2
t.float :rating, limit: 24 # FLOAT
t.float :score, limit: 53 # DOUBLE
t.timestamps
end
end
end
JSON Support (MySQL 5.7+)
MySQL SQL:
SELECT * FROM users
WHERE JSON_EXTRACT(preferences, '$.theme') = 'dark';
SELECT JSON_UNQUOTE(JSON_EXTRACT(data, '$.name')) as name
FROM products;
Rails ActiveRecord:
User.where("JSON_EXTRACT(preferences, '$.theme') = ?", 'dark')
Product.select("JSON_UNQUOTE(JSON_EXTRACT(data, '$.name')) as name")
# Rails automatically handles JSON serialization
user = User.create(preferences: { theme: 'dark', lang: 'en' })
user.preferences['theme'] # => 'dark'
5. Full-Text Search
MySQL FULLTEXT Search
Setting up FULLTEXT Index
# Migration for FULLTEXT index
class AddFulltextIndexToPosts < ActiveRecord::Migration[7.0]
def up
execute "ALTER TABLE posts ADD FULLTEXT(title, content)"
end
def down
execute "ALTER TABLE posts DROP INDEX title"
end
end
Basic FULLTEXT Search
MySQL SQL:
SELECT *, MATCH(title, content) AGAINST('rails activerecord') as relevance
FROM posts
WHERE MATCH(title, content) AGAINST('rails activerecord')
ORDER BY relevance DESC;
Rails ActiveRecord:
search_term = 'rails activerecord'
Post.select("*, MATCH(title, content) AGAINST(?) as relevance", search_term)
.where("MATCH(title, content) AGAINST(?)", search_term)
.order('relevance DESC')
Boolean and Natural Language Search
MySQL SQL:
-- Boolean mode search
SELECT * FROM posts
WHERE MATCH(title, content) AGAINST('+rails -php' IN BOOLEAN MODE);
-- Natural language search
SELECT * FROM posts
WHERE MATCH(title, content) AGAINST('database optimization' IN NATURAL LANGUAGE MODE);
Rails ActiveRecord:
# Boolean mode search
Post.where("MATCH(title, content) AGAINST(? IN BOOLEAN MODE)", '+rails -php')
# Natural language search
Post.where("MATCH(title, content) AGAINST(? IN NATURAL LANGUAGE MODE)", 'database optimization')
# Scope for reusability
class Post < ApplicationRecord
scope :fulltext_search, ->(query) { where("MATCH(title, content) AGAINST(?)", query) }
end
6. JOINs and Subqueries
Complex JOIN Patterns
Multiple Table JOINs
MySQL SQL:
SELECT
u.name,
p.title,
c.name as category_name,
COUNT(co.id) as comment_count
FROM users u
JOIN posts p ON p.user_id = u.id
JOIN categories c ON p.category_id = c.id
LEFT JOIN comments co ON co.post_id = p.id
GROUP BY u.id, p.id, c.id;
Rails ActiveRecord:
User.joins(posts: [:category])
.left_joins(posts: :comments)
.select('users.name, posts.title, categories.name as category_name, COUNT(comments.id) as comment_count')
.group('users.id, posts.id, categories.id')
Subqueries with EXISTS
MySQL SQL:
SELECT * FROM users u
WHERE EXISTS (
SELECT 1 FROM posts p
WHERE p.user_id = u.id
AND p.published = 1
);
Rails ActiveRecord:
User.where('EXISTS (?)',
Post.where('posts.user_id = users.id')
.where(published: true)
.select('1')
)
# Or using joins (often more efficient)
User.joins(:posts).where(posts: { published: true }).distinct
Correlated Subqueries
MySQL SQL:
SELECT *,
(SELECT COUNT(*) FROM posts WHERE user_id = users.id) as post_count,
(SELECT MAX(created_at) FROM posts WHERE user_id = users.id) as last_post_date
FROM users;
Rails ActiveRecord:
User.select('users.*,
(SELECT COUNT(*) FROM posts WHERE user_id = users.id) as post_count,
(SELECT MAX(created_at) FROM posts WHERE user_id = users.id) as last_post_date')
# Better: Use includes to prevent N+1
User.left_joins(:posts)
.select('users.*, COUNT(posts.id) as post_count, MAX(posts.created_at) as last_post_date')
.group('users.id')
7. Performance Optimization
MySQL-Specific Optimizations
Index Strategy
# Migration with MySQL-optimized indexes
class OptimizeMysqlIndexes < ActiveRecord::Migration[7.0]
def change
# Composite index for common query patterns
add_index :posts, [:user_id, :published, :created_at]
# Covering index to avoid table lookups
add_index :users, [:email], name: 'index_users_on_email_covering'
# Partial index for active records only
add_index :users, :created_at, where: "active = 1"
# FULLTEXT index for search
execute "ALTER TABLE posts ADD FULLTEXT(title, content)"
end
end
Query Optimization
# Use EXPLAIN to analyze queries
User.where(active: true).explain
# Force index usage when needed
User.from('users FORCE INDEX (index_users_on_email)')
.where(email: 'user@example.com')
# Use SQL_CALC_FOUND_ROWS for pagination (MySQL specific)
ActiveRecord::Base.connection.execute('SELECT SQL_CALC_FOUND_ROWS * FROM users LIMIT 10')
total_count = ActiveRecord::Base.connection.execute('SELECT FOUND_ROWS()').first[0]
MySQL Performance Tips
- • Use composite indexes for multi-column WHERE clauses
- • Consider query cache settings for read-heavy applications
- • Use LIMIT to prevent large result sets
- • Optimize MyISAM vs InnoDB storage engine choice
- • Use connection pooling for concurrent requests
- • Consider read replicas for scaling read operations
8. Migration Strategies
Step-by-Step Migration Process
1. Audit Existing MySQL Queries
# Find MySQL-specific syntax in your codebase
grep -r "LIMIT.*OFFSET\|GROUP_CONCAT\|REGEXP\|FULLTEXT" app/
grep -r "FORCE INDEX\|USE INDEX\|IGNORE INDEX" app/
2. Convert Common Patterns First
# Start with simple conversions
# MySQL: SELECT * FROM users WHERE email LIKE '%@gmail.com' LIMIT 10
User.where('email LIKE ?', '%@gmail.com').limit(10)
# Then move to complex queries
# MySQL: SELECT u.*, COUNT(p.id) as post_count FROM users u LEFT JOIN posts p ON p.user_id = u.id GROUP BY u.id
User.left_joins(:posts).group(:id).select('users.*, COUNT(posts.id) as post_count')
3. Handle MySQL-Specific Features
# Create helper methods for MySQL-specific functions
class ApplicationRecord < ActiveRecord::Base
scope :fulltext_search, ->(query) do
where("MATCH(title, content) AGAINST(?)", query) if query.present?
end
def self.group_concat(column, separator = ',')
select("GROUP_CONCAT(#{column} SEPARATOR '#{separator}') as concatenated")
end
end
9. Common Conversion Patterns
Frequently Used MySQL to ActiveRecord Conversions
Pagination with Total Count
MySQL SQL:
SELECT SQL_CALC_FOUND_ROWS *
FROM users
WHERE active = 1
ORDER BY created_at DESC
LIMIT 10 OFFSET 0;
SELECT FOUND_ROWS() as total;
Rails ActiveRecord:
# Using Kaminari gem
User.where(active: true)
.order(created_at: :desc)
.page(1)
.per(10)
# Manual pagination
users = User.where(active: true)
.order(created_at: :desc)
.limit(10)
.offset(0)
total = User.where(active: true).count
Dynamic WHERE Clauses
MySQL SQL:
SELECT * FROM products
WHERE 1=1
AND (category_id = ? OR ? IS NULL)
AND (price BETWEEN ? AND ? OR ? IS NULL)
AND (name LIKE ? OR ? IS NULL);
Rails ActiveRecord:
products = Product.all
products = products.where(category_id: category_id) if category_id.present?
products = products.where(price: min_price..max_price) if min_price && max_price
products = products.where('name LIKE ?', "%#{name}%") if name.present?
# Using scopes
class Product < ApplicationRecord
scope :by_category, ->(cat) { where(category_id: cat) if cat.present? }
scope :by_price_range, ->(min, max) { where(price: min..max) if min && max }
scope :by_name, ->(name) { where('name LIKE ?', "%#{name}%") if name.present? }
end
Ranking and Top N Queries
MySQL SQL:
SELECT
u.*,
@rank := @rank + 1 as rank
FROM users u
CROSS JOIN (SELECT @rank := 0) r
ORDER BY score DESC
LIMIT 10;
Rails ActiveRecord:
# Using ROW_NUMBER() (MySQL 8.0+)
User.select('*, ROW_NUMBER() OVER (ORDER BY score DESC) as rank')
.order(score: :desc)
.limit(10)
# For older MySQL versions
users = User.order(score: :desc).limit(10)
users.each_with_index { |user, index| user.rank = index + 1 }
Migration Checklist
- • ✅ Update database.yml to use mysql2 adapter
- • ✅ Convert MySQL-specific SQL to ActiveRecord methods
- • ✅ Update FULLTEXT search queries
- • ✅ Optimize indexes for ActiveRecord query patterns
- • ✅ Test performance with realistic data volumes
- • ✅ Update any MySQL-specific configuration
- • ✅ Consider connection pooling settings