概要
ROM(Ruby Object Mapper)というORM(Object Relational Mapper)があります。
Railsだとデフォルトで ActiveRecord
が組み込まれているので基本的に使うことはないと思いますが、HanamiではROMをラップした Hanami::Model
がデフォルトのORMになっています。
ROM自体はデータソースがRDB以外でも使うことのできる汎用的なORMで、単なるHashやArrayでも扱うことができます。
今回はROM 4.0のassociationでcross-database associationを設定する書き方をメモっておきます。
想定用途
- 複数のデータベースを運用しており、データベース間でassociationを設定したいとき
- アカウント情報を保存するDBと、個別のデータを保存するDBが別々の場合等
例えば以下のスキーマを定義して users
テーブルと posts
テーブルを別々のDBで管理していた場合、SQLでJOINすることができません。
CREATE DATABASE `db1`;
USE `db1`;
CREATE TABLE `users`(
`id` INTEGER UNSIGNED PRIMARY KEY AUTO_INCREMENT,
`uuid` VARCHAR(36) NOT NULL,
`name` VARCHAR(20) NOT NULL
);
CREATE DATABASE `db2`;
USE `db2`;
CREATE TABLE `posts`(
`id` INTEGER UNSIGNED PRIMARY KEY AUTO_INCREMENT,
`user_uuid` VARCHAR(36) UNSIGNED NOT NULL,
`title` VARCHAR(20) NOT NULL
);
この場合ActiveRecordではassociationを設定できず、 User
と Post
のインスタンスを別々に作成するしかありません。
一方ROMのassociationでは、別々のDBからのデータを内部で結合することができ、JOINした場合と同じ結果を得ることができます。
# ActiveRecordでの例
# SwitchPoint等のgemを使って複数DBへの接続を管理している前提です
user = User.find_by(id: 1) #=> #<User id=1 uuid="uuid" name="name">
post = Post.find_by(user_uuid: user.uuid) #=> #<Post id=1 user_uuid="uuid" title="title">
ROMで書く
データベース接続の設定
初期設定としてデータベースの接続先を指定します。
ROMでは接続情報を内包した Container
を作成し、これを Repository
のコンストラクタに渡すことでRepositoryに接続先を教えます。
ROM::Configuration
のコンストラクタには、接続先が一つの場合はそのURLを、複数の場合は { db_alias: 'db_url' }
の形式のHashを渡します。
container = ROM.container(
ROM::Configuration.new(
default: 'mysql2://root@localhost/db1',
db2: 'mysql2://root@localhost/db2'
)
)
Relation, Repositoryの定義
ROMを使うにあたって、Relation(RDBでいうテーブル定義)とRepository(データソースとやり取りするためのロジックをまとめた層)を定義する必要があります。
class Users < ROM::Relation[:sql]
schema(infer: true) do
# :view, :override, :combine_keys を指定するのがポイントです
has_many :posts, foreign_key: :uuid, view: :for_user, override: true, combine_keys: { uuid: :user_uuid }
end
end
class Posts < ROM::Relation[:sql]
gateway :db2 # gateway :alias の書式でどの接続先を使うか指定します。省略すると :default につながります
schema(infer: true)
def for_user(_assoc, user) # データを結合するときの条件をviewとして指定します
where(user_uuid: user.pluck(:uuid))
end
end
class UserRepo < ROM::Repository[:users]
def find_with_posts_by(uuid:)
users.combine(:posts).where(uuid: uuid).one # usersメソッドで Relation のインスタンスが取れます
end
end
使う
定義は済んでいるので、あとは使うだけです
user_repo = UserRepo.new(container)
user = user_repo.find_with_posts_by(uuid: 'uuid')
#=> #<ROM::Struct::User id=1 uuid="uuid" name="name" posts=[#<ROM::Struct::Post id=1 user_uuid="uuid" title="title">]>
という感じで、複数DBにまたがるテーブル間でもちゃんとassociationを定義した状態でレコードを取得することができました。