はじめに
Ruby on Railsを学習している方はActiveRecordという名称のORマッパーを目にする事が度々あると思います。
私もプログラミングの学習をRubyから始めたのですが、ORマッパーと聞くとなんとなく「DBからのデータの取り出しを簡単にやってくれているんだな」くらいの認識でした。
Railsがあまりに様々な事をよしなにしてくれるので、Rails初学者の方は生のSQLを書く機会もほとんどないのではないかと思います。
最近Goを勉強し始めた事で、そのRailsがよしなにしてくれていた部分を基本から学び直す必要が出てきたので、ORMに関しても一つ学び直してここに備忘録として残したいと思います。
対象者
- プログラミング初学者
- SQLは書いた事があるがORMは利用した事がない。あるいはORMは利用した事があるが生のSQLを書く事はあまりない。
概要
Goのデータベースとのやり取りを、生のSQLで行った場合とORマッパーを利用した場合とでどうコードの違いが出るのかを比較して見ていきます。
Goを扱っていますが、簡単なコードなのでGoを触った事が無い方でも理解出来ると思います。
データベースはPostgreSQL、ORMはgormを使用します。
環境
Go 1.16
PostgreSQL 13.0
コード全容
store.goが生SQLで記述した場合。
store2.goがORMを使用した場合のコードです。
create table users (
id serial primary key,
name text
);
create table posts (
id serial primary key,
content text,
user_id integer references users(id)
);
package main
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
)
// テーブルに対応した構造体を定義
type User struct {
Id int
Name string
Posts []Post
}
type Post struct {
Id int
Content string
User *User
}
// データベースへ接続
var Db *sql.DB
var err error
func init() {
Db, err = sql.Open("postgres", "user=test dbname=test password=test sslmode=disable")
if err != nil {
panic(err)
}
}
func main() {
// ユーザデータの作成
user := User{Name: "堀口太郎"}
sql := "INSERT INTO \"users\" (name) values ($1) RETURNING id"
Db.QueryRow(sql, user.Name).Scan(&user.Id)
// ポストデータの作成
post := Post{Content: "どうも皆さんこんにちは", User: &user}
sql2 := "INSERT INTO posts (content, user_id) values ($1, $2)"
Db.Exec(sql2, post.Content, post.User.Id)
// ユーザの取得
var readUser User
sql3 := "SELECT id, name FROM users WHERE name = $1"
usename := "堀口太郎"
Db.QueryRow(sql3, usename).Scan(&readUser.Id, &readUser.Name)
// ユーザからコメント取得
var posts []Post
sql4 := "SELECT Id, Content FROM posts WHERE user_id = $1"
rows, _ := Db.Query(sql4, readUser.Id)
for rows.Next() {
post := Post{User: &readUser}
rows.Scan(&post.Id, &post.Content)
posts = append(posts, post)
}
}
package main
import (
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/lib/pq"
)
// テーブルに対応した構造体を定義
type User struct {
Id int
Name string
Posts []Post
}
type Post struct {
Id int
Content string
UserId int
}
// データベースへ接続
var Db *gorm.DB
func init() {
var err error
Db, err = gorm.Open("postgres", "user=test dbname=test password=test sslmode=disable")
if err != nil {
panic(err)
}
// 構造体の内容を基にテーブルを自動作成
Db.AutoMigrate(&User{}, &Post{})
}
func main() {
// ユーザデータの作成
user := User{Name: "山田太郎"}
Db.Create(&user)
// ポストデータの作成
post := Post{Content: "どうも皆さんこんにちは"}
Db.Model(&user).Association("Posts").Append(post)
// ユーザの取得
var readUser User
username := "山田太郎"
Db.Where("name=$1", username).First(&readUser)
// ユーザからコメント取得
var posts []Post
Db.Model(&readUser).Related(&posts)
}
ORMとは
オブジェクト関係マッピング(英: Object-relational mapping, O/RM, ORM)とは、データベースとオブジェクト指向プログラミング言語の間の非互換なデータを変換するプログラミング技法である。オブジェクト関連マッピングとも呼ぶ。実際には、オブジェクト指向言語から使える「仮想」オブジェクトデータベースを構築する手法である。 - Wikipedia
簡単に言うと、**オブジェクト指向(Object)とリレーショナルデータベース(Relational)の対応付け(Mapping)**を行う事でデータベースから取得するレコードデータをオブジェクト化し、直感的に扱えるようにしたものになります。
なぜそのような対応付けが必要になるかというと、現実世界のモデルに即したデータモデルであるオブジェクト指向と、データのCRUD処理に最適化されたデータモデルであるRDBではその設計思想の違いにより、インピーダンスミスマッチが発生してしまう為と言われています。
解説
※store.goが生SQLで作成したコード、store2.goがgormを使用したコードになります。
今回はとてもシンブルなテーブル構造のデータベースに対してCreate,及びReadを行った場合の違いを比較していきます。
データベース側はtestという名称のユーザ、同じくtestという名称のDBをそれぞれ作成しています。
テーブル以下のUserとPostの二つです。
User |
---|
id |
name |
Post |
---|
id |
content |
user_id |
次に、この二つのテーブルを作成していきます。
ORMを利用しない場合は以下スクリプトファイルを作成し、データベース側でテーブルの作成を行います。(Go側で作成する事も可能ですが、今回はデータベース側行います。)
create table users (
id serial primary key,
name text
);
create table posts (
id serial primary key,
content text,
user_id integer references users(id)
);
以下のコマンドを実行し、テーブルを作成します。
$ psql -U test -f .\setup.sql -d test
対して、ORMを利用した場合の処理を見ていきます。
type User struct {
Id int
Name string
Posts []Post
}
type Post struct {
Id int
Content string
UserId int
}
Db.AutoMigrate(&User{}, &Post{})
テーブルの作成処理を行っている部分は上記のコードになります。
レコードを格納するための構造体を定義する事で、その構造体を利用してテーブルの作成を行う事が出来ます。
次に実際のデータの操作を見ていきます。
Create
// ユーザデータの作成
user := User{Name: "堀口太郎"}
sql := "INSERT INTO \"users\" (name) values ($1) RETURNING id"
Db.QueryRow(sql, user.Name).Scan(&user.Id)
// ポストデータの作成
post := Post{Content: "どうも皆さんこんにちは", User: &user}
sql2 := "INSERT INTO posts (content, user_id) values ($1, $2)"
Db.Exec(sql2, post.Content, post.User.Id)
// ユーザデータの作成
user := User{Name: "堀口太郎"}
Db.Create(&user)
// ポストデータの作成
post := Post{Content: "どうも皆さんこんにちは"}
Db.Model(&user).Association("Posts").Append(post)
Read
// ユーザの取得
var readUser User
sql3 := "SELECT id, name FROM users WHERE name = $1"
usename := "堀口太郎"
Db.QueryRow(sql3, usename).Scan(&readUser.Id, &readUser.Name)
// ユーザからコメント取得
var posts []Post
sql4 := "SELECT Id, Content FROM posts WHERE user_id = $1"
rows, _ := Db.Query(sql4, readUser.Id)
for rows.Next() {
post := Post{User: &readUser}
rows.Scan(&post.Id, &post.Content)
posts = append(posts, post)
}
// ユーザの取得
var readUser User
username := "堀口太郎"
Db.Where("name=$1", username).First(&readUser)
// ユーザからコメント取得
var posts []Post
Db.Model(&readUser).Related(&posts)
どのコードを比較しても、ORMを使用した場合は短く、そして直感的に理解しやすいコードになっているかと思います。
まとめ
実際にORMを利用した場合と生のSQLを利用した場合でどのような違いが出るのか簡単にではありますが比較してみました。
今回はとても簡単なコードで比較したので一見メリットしかないように見えますが、ORMにも様々なデメリットが存在しています。
今回はその点については触れませんが、調べてみると賛否両論色んな意見が出てくると思いますので興味がある方は是非調べて見て下さい。
また、私自身Goの学習を始めたばかりなので気になる点があれば是非ご指摘頂けると幸いです。