ActiveRecordMysqlRepl
Prerequisites
Ruby version 3.0.0 or higher
e.g) Using rbenv
$ rbenv install 3.3.6
mysql-client 8.0
e.g) on macOS
$ brew install mysql-client@8.0
$ gem install mysql2 -v 0.5.6 -- --with-ldflags=-L$(brew --prefix zstd)/lib --with-mysql-dir=/opt/homebrew/opt/mysql-client@8.4
Installation
Just by installing the gem, you can use the army
command.
$ gem install active_record_mysql_repl
Successfully installed active_record_mysql_repl-x.x.x
Parsing documentation for active_record_mysql_repl-x.x.x
Installing ri documentation for active_record_mysql_repl-x.x.x
Done installing documentation for active_record_mysql_repl after 0 seconds
1 gem installed
$ army
ActiveRecordMysqlRepl Version: x.x.x
If you want to use the zsh completion, you can add the following line to your .zshrc
file.
$ eval "$(army --zsh-completion)"
Then you can use the completion feature like below.
$ army [TAB]
option
-c -- path to .armyrc file
-d -- Database name
-e -- output erd
Usage
Sample Configurations
From the following link, you can Download the sample configuration files https://github.com/nogahighland/active_record_mysql_repl/tree/main/sample_config
Also you can try with sample database on your local MySQL server https://github.com/nogahighland/active_record_mysql_repl/blob/main/sample_config/sample_db.sql
Sample Database
CREATE DATABASE test;
The sample configuration assumes the sample database is running on 127.0.0.1:3306/test (user: root, password: root)
$ mysql -u root -p test < sample_db.sql
This ER Diagram can be generated by the following command
army -c sample_config/.army.sample.yml -d test -e erd
Showcases
army -c /path/to/sample_config/.army.sample.yml -d test
Ensureing connection to test on port 127.0.0.1:33060
Loading tables
Loading custom extensions from /path/to/sample_config/./.army.sample/extensions
Now you can access to the table classes as User
if the table name is users
.
[1] test(main)> User.all
D, [2024-12-12T12:57:38.581127 #2816] DEBUG -- : User Load (10.5ms) SELECT `users`.* FROM `users`
[
[0] #<User:0x0000000123188de0> {
:id => "1",
:login_id => "user1",
:profile_id => 1
}
]
.d
method shows the schema of the table. Let's see the Order
table schema.
[8] test(main)> Order.d
D, [2024-12-12T12:32:20.100309 #2816] DEBUG -- : (10.6ms) DESCRIBE orders
D, [2024-12-12T12:32:20.107976 #2816] DEBUG -- : (6.9ms) SHOW INDEX FROM orders
# orders
+---------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------+-------------+------+-----+---------+-------+
| id | varchar(64) | NO | PRI | | |
| user_id | varchar(64) | NO | | | |
| item_id | varchar(64) | NO | | | |
+---------+-------------+------+-----+---------+-------+
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| orders | 0 | PRIMARY | 1 | id | A | 1 | | | | BTREE | | | YES | |
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
.ddl
method shows the DDL of the table.
[9] test(main)> Order.ddl
D, [2024-12-12T12:32:21.878444 #2816] DEBUG -- : (8.7ms) SHOW CREATE TABLE orders
CREATE TABLE `orders` (
`id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`user_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`item_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
models
is globally defined to get and array of all the table classes. By .map(&:d)
you can see the schema of all the tables.
[6] test(main)> puts models.map(&:d)
D, [2024-12-12T01:26:36.677988 #25446] DEBUG -- : Brand Load (2.1ms) SELECT `brands`.* FROM `brands`
D, [2024-12-12T01:26:36.681307 #25446] DEBUG -- : Category Load (2.8ms) SELECT `categories`.* FROM `categories`
D, [2024-12-12T01:26:36.683967 #25446] DEBUG -- : Item Load (2.0ms) SELECT `items`.* FROM `items`
D, [2024-12-12T01:26:36.687282 #25446] DEBUG -- : Order Load (3.0ms) SELECT `orders`.* FROM `orders`
D, [2024-12-12T01:26:36.691204 #25446] DEBUG -- : UserProfile Load (3.6ms) SELECT `user_profiles`.* FROM `user_profiles`
D, [2024-12-12T01:26:36.694081 #25446] DEBUG -- : User Load (2.2ms) SELECT `users`.* FROM `users`
# users
+------------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+-------------+------+-----+---------+-------+
| id | varchar(64) | NO | PRI | | |
| login_id | varchar(64) | NO | | | |
| profile_id | int | YES | | | |
+------------+-------------+------+-----+---------+-------+
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| users | 0 | PRIMARY | 1 | id | A | 1 | | | | BTREE | | | YES | |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
# user_profiles
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id | varchar(64) | NO | PRI | | |
| name | varchar(64) | NO | | | |
+-------+-------------+------+-----+---------+-------+
+---------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+---------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| user_profiles | 0 | PRIMARY | 1 | id | A | 1 | | | | BTREE | | | YES | |
+---------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
# orders
+---------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------+-------------+------+-----+---------+-------+
| id | varchar(64) | NO | PRI | | |
| user_id | varchar(64) | NO | | | |
| item_id | varchar(64) | NO | | | |
+---------+-------------+------+-----+---------+-------+
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| orders | 0 | PRIMARY | 1 | id | A | 1 | | | | BTREE | | | YES | |
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
# items
+-------------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------------+-------------+------+-----+---------+-------+
| id | varchar(64) | NO | PRI | | |
| category_id | varchar(64) | NO | | | |
| brand_id | varchar(64) | NO | | | |
+-------------+-------------+------+-----+---------+-------+
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| items | 0 | PRIMARY | 1 | id | A | 1 | | | | BTREE | | | YES | |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
# categories
+-----------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------+-------------+------+-----+---------+-------+
| id | varchar(64) | NO | PRI | | |
| name | varchar(64) | NO | | | |
| parent_id | varchar(64) | NO | | | |
+-----------+-------------+------+-----+---------+-------+
+------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| categories | 0 | PRIMARY | 1 | id | A | 2 | | | | BTREE | | | YES | |
+------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
# brands
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id | varchar(64) | NO | PRI | | |
| name | varchar(64) | NO | | | |
+-------+-------------+------+-----+---------+-------+
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| brands | 0 | PRIMARY | 1 | id | A | 1 | | | | BTREE | | | YES | |
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
To get the last record of order,
[1] test(main)> Order.last
D, [2024-12-11T23:45:49.743857 #96076] DEBUG -- : Order Load (5.5ms) SELECT `orders`.* FROM `orders` ORDER BY `orders`.`id` DESC LIMIT 1
#<Order:0x00000001293677c8> {
:id => "1",
:user_id => "1",
:item_id => "1"
}
You can get the last order's user because user_id
column exists on orders
table. For the same reason you can get the order's item and its category too.
All the associations are chainable by the native functionality of ActiveRecord.
[5] test(main)> Order.last.user
D, [2024-12-11T23:46:11.154678 #96076] DEBUG -- : Order Load (6.9ms) SELECT `orders`.* FROM `orders` ORDER BY `orders`.`id` DESC LIMIT 1
D, [2024-12-11T23:46:11.176424 #96076] DEBUG -- : User Load (2.9ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = '1' LIMIT 1
#<User:0x000000012961fda8> {
:id => "1",
:login_id => "user1",
:profile_id => 1
}
[7] test(main)> Order.last.item
D, [2024-12-11T23:46:17.098874 #96076] DEBUG -- : Order Load (3.5ms) SELECT `orders`.* FROM `orders` ORDER BY `orders`.`id` DESC LIMIT 1
D, [2024-12-11T23:46:17.112940 #96076] DEBUG -- : Item Load (2.2ms) SELECT `items`.* FROM `items` WHERE `items`.`id` = '1' LIMIT 1
#<Item:0x000000012a416c40> {
:id => "1",
:category_id => "2",
:brand_id => "1"
}
[8] test(main)> Order.last.item.category
D, [2024-12-11T23:46:21.104696 #96076] DEBUG -- : Order Load (2.1ms) SELECT `orders`.* FROM `orders` ORDER BY `orders`.`id` DESC LIMIT 1
D, [2024-12-11T23:46:21.106696 #96076] DEBUG -- : Item Load (1.5ms) SELECT `items`.* FROM `items` WHERE `items`.`id` = '1' LIMIT 1
D, [2024-12-11T23:46:21.117169 #96076] DEBUG -- : Category Load (2.5ms) SELECT `categories`.* FROM `categories` WHERE `categories`.`id` = '2' LIMIT 1
#<Category:0x000000012a5d7b38> {
:id => "2",
:name => "category2",
:parent_id => "1"
}
A category's parent is fetched from the same table because parent_id
's parent
is treated by ActiveRecordMysqlRepl
as inplicitly pointing to the same table.
[9] test(main)> Order.last.item.category.parent
D, [2024-12-11T23:46:23.553443 #96076] DEBUG -- : Order Load (2.0ms) SELECT `orders`.* FROM `orders` ORDER BY `orders`.`id` DESC LIMIT 1
D, [2024-12-11T23:46:23.555918 #96076] DEBUG -- : Item Load (2.1ms) SELECT `items`.* FROM `items` WHERE `items`.`id` = '1' LIMIT 1
D, [2024-12-11T23:46:23.560142 #96076] DEBUG -- : Category Load (3.8ms) SELECT `categories`.* FROM `categories` WHERE `categories`.`id` = '2' LIMIT 1
D, [2024-12-11T23:46:23.562496 #96076] DEBUG -- : Category Load (1.3ms) SELECT `categories`.* FROM `categories` WHERE `categories`.`id` = '1' LIMIT 1
#<Category:0x000000012a73dd60> {
:id => "1",
:name => "category1",
:parent_id => ""
}
You can get user's profile by .profile
because the custom association is defined here
[6] test(main)> Order.last.user.profile
D, [2024-12-12T18:23:41.382606 #2816] DEBUG -- : Order Load (8.6ms) SELECT `orders`.* FROM `orders` ORDER BY `orders`.`id` DESC LIMIT 1
D, [2024-12-12T18:23:41.389954 #2816] DEBUG -- : User Load (2.4ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = '1' LIMIT 1
D, [2024-12-12T18:23:41.392060 #2816] DEBUG -- : UserProfile Load (1.8ms) SELECT `user_profiles`.* FROM `user_profiles` WHERE `user_profiles`.`id` = '1' LIMIT 1
#<UserProfile:0x000000012330a010> {
:id => "1",
:name => "user1"
}
Even though users.login_id
column exists, because the column is not an external key it should be ignored from the association.
The column is ignored because the custom association is defined here and User#login
causes a NoMethodError
[1] test(main)> User.last.login
D, [2024-12-12T22:17:49.291618 #37808] DEBUG -- : User Load (2.5ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` DESC LIMIT 1
NoMethodError: undefined method `login' for an instance of User
from /Users/hiroki.kishi/develop/private/active_record_mysql_repl/vendor/bundle/ruby/3.3.0/gems/activemodel-7.2.2/lib/active_model/attribute_methods.rb:512:in `method_missing'
You can use transaction by globally defined transaction
.
[7] test(main)> u2 = User.new(id:2, profile_id: up2, login_id: 'login2')
[9] test(main)> up2 = UserProfile.new(id:2, name: 'user2');
[10] test(main)> transaction { [u2, up2].map(&:save) }
[9] test(main)> up2 = UserProfile.new(id:2, name: 'user2');
[10] test(main)> transaction { [u2, up2].map(&:save) }
D, [2024-12-12T22:25:32.266790 #37808] DEBUG -- : TRANSACTION (2.2ms) BEGIN
D, [2024-12-12T22:25:32.272857 #37808] DEBUG -- : User Create (8.3ms) INSERT INTO `users` (`id`, `login_id`) VALUES ('2', 'login2')
D, [2024-12-12T22:25:32.275213 #37808] DEBUG -- : UserProfile Create (2.1ms) INSERT INTO `user_profiles` (`id`, `name`) VALUES ('2', 'user2')
D, [2024-12-12T22:25:32.281765 #37808] DEBUG -- : TRANSACTION (6.4ms) COMMIT
[
[0] true,
[1] true
]
The following example shows the following steps:
- Find the users with id 1 and 2
- Change the login_id of the users
- Show the changes
- Save the changes in a transaction
[11] test(main)> u1, u2 = User.find([1, 2])
D, [2024-12-12T22:27:55.979311 #37808] DEBUG -- : User Load (11.7ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IN ('1', '2')
[
[0] #<User:0x0000000144b325a0> {
:id => "1",
:login_id => "user1",
:profile_id => 1
},
[1] #<User:0x0000000144b32460> {
:id => "2",
:login_id => "login2",
:profile_id => nil
}
]
[12] test(main)> [u1, u2].each { |u| u.login_id = "login_id#{u.id}" }
[
[0] #<User:0x0000000144b325a0> {
:id => "1",
:login_id => "login_id1",
:profile_id => 1
},
[1] #<User:0x0000000144b32460> {
:id => "2",
:login_id => "login_id2",
:profile_id => nil
}
]
[13] test(main)> [u1, u2].map(&:changes)
[
[0] {
"login_id" => [
[0] "user1",
[1] "login_id1"
]
},
[1] {
"login_id" => [
[0] "login2",
[1] "login_id2"
]
}
]
[14] test(main)> transaction { [u1, u2].map(&:save) }
D, [2024-12-12T22:29:14.179176 #37808] DEBUG -- : TRANSACTION (1.2ms) BEGIN
D, [2024-12-12T22:29:14.188392 #37808] DEBUG -- : User Update (9.1ms) UPDATE `users` SET `users`.`login_id` = 'login_id1' WHERE `users`.`id` = '1'
D, [2024-12-12T22:29:14.191429 #37808] DEBUG -- : User Update (2.6ms) UPDATE `users` SET `users`.`login_id` = 'login_id2' WHERE `users`.`id` = '2'
D, [2024-12-12T22:29:14.196802 #37808] DEBUG -- : TRANSACTION (5.2ms) COMMIT
[
[0] true,
[1] true
]
By tab
(an alias to tabulate
), the filtered records are shown in a table format. .tab(orientation)
accepts the :h(orizontal)
(default when <5 columns), and :v(ertival)
(default when >=5 columns)
[3] test(main)> Order.all.tab
D, [2024-12-11T23:48:46.013935 #96381] DEBUG -- : Order Load (2.2ms) SELECT `orders`.* FROM `orders`
+----+---------+---------+
| id | user_id | item_id |
+----+---------+---------+
| 1 | 1 | 1 |
+----+---------+---------+
[4] test(main)> Order.all.tab(:v)
D, [2024-12-11T23:48:49.461243 #96381] DEBUG -- : Order Load (3.2ms) SELECT `orders`.* FROM `orders`
+---------+-------+
| Name | Value |
+---------+-------+
| id | 1 |
| user_id | 1 |
| item_id | 1 |
+---------+-------+
.j
is aliased to .to_json
and .jp
is defined to generate pretty json.
active_record_mysql_repl/lib/active_record_mysql_repl/extensions/object.rb
Lines 128 to 132 in 9f7c917
[5] test(main)> Order.all.j
D, [2024-12-11T23:48:55.267761 #96381] DEBUG -- : Order Load (4.4ms) SELECT `orders`.* FROM `orders`
[{"id":"1","user_id":"1","item_id":"1"}]
[6] test(main)> Order.all.jp
D, [2024-12-11T23:48:57.250492 #96381] DEBUG -- : Order Load (1.8ms) SELECT `orders`.* FROM `orders`
[
{
"id": "1",
"user_id": "1",
"item_id": "1"
}
]
Convenient syntax sugar such as 'value'.{table_name}
'value'.{table_name}s
'value'.{table_name}_by_{column_name}
are available as dynamically defined methods.
[1] test(main)> :'1'.order
D, [2024-12-13T11:40:17.290699 #48059] DEBUG -- : Order Load (2.2ms) SELECT `orders`.* FROM `orders` WHERE `orders`.`id` = '1' LIMIT 1
#<Order:0x0000000122e7f0e0> {
:id => "1",
:user_id => "1",
:item_id => "1"
}
[2] test(main)> '1'.order
D, [2024-12-13T11:40:21.217724 #48059] DEBUG -- : Order Load (2.4ms) SELECT `orders`.* FROM `orders` WHERE `orders`.`id` = '1' LIMIT 1
#<Order:0x000000010512c450> {
:id => "1",
:user_id => "1",
:item_id => "1"
}
[3] test(main)> '1'.orders
D, [2024-12-13T11:40:22.781925 #48059] DEBUG -- : Order Load (2.7ms) SELECT `orders`.* FROM `orders` WHERE `orders`.`id` = '1' LIMIT 1
#<Order:0x0000000105129750> {
:id => "1",
:user_id => "1",
:item_id => "1"
}
[4] test(main)> '1'.user_by_profile_id
D, [2024-12-13T11:40:47.497002 #48059] DEBUG -- : User Load (7.9ms) SELECT `users`.* FROM `users` WHERE `users`.`profile_id` = 1
[
[0] #<User:0x00000001243fc0f8> {
:id => "1",
:login_id => "login_id1",
:profile_id => 1
}
]
.csv(orientation)
is defined to generate csv format. The default orientation is same as .tab
.
[1] test(main)> Order.all.csv
D, [2024-12-12T01:29:12.844339 #26252] DEBUG -- : Order Load (2.7ms) SELECT `orders`.* FROM `orders`
id,user_id,item_id
1,1,1
.cp
is defined to copy to clipboard the receiver object's .to_s
representation.
[7] test(main)> Order.all.jp.cp
D, [2024-12-11T23:48:59.443202 #96381] DEBUG -- : Order Load (2.0ms) SELECT `orders`.* FROM `orders`
true
.exec_sql
is defined to execute the sql query and return the result as an array of hash.
[8] test(main)> exec_sql('select 1')
D, [2024-12-11T23:49:15.879841 #96381] DEBUG -- : (1.2ms) select 1
[
[0] {
"1" => 1
}
]
[9] test(main)> exec_sql('select 1').tab
D, [2024-12-11T23:49:17.187358 #96381] DEBUG -- : (2.1ms) select 1
+---+
| 1 |
+---+
| 1 |
+---+
[10] test(main)> exec_sql('select 1 as num').tab
D, [2024-12-11T23:49:22.406631 #96381] DEBUG -- : (1.3ms) select 1 as num
+-----+
| num |
+-----+
| 1 |
+-----+
You can define your own extension script. For example .upcase_name
is defined on UserProfile
by the sample extension which is specified in the .army.sample.yml
file.
[3] test(main)> UserProfile.last.upcase_name
D, [2024-12-12T12:09:10.987656 #60808] DEBUG -- : UserProfile Load (1.7ms) SELECT `user_profiles`.* FROM `user_profiles` ORDER BY `user_profiles`.`id` DESC LIMIT 1
USER1
Development
TODO
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/nogahifhland/active_record_mysql_repl. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the ActiveRecordMysqlRepl project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.