ハイサイ!オースティンやいびーん。
概要
WordPressのWP_QueryをRustで実装してみた体験を振り返ります。
主にRust学習のためにやってみたのですが、ちょっと面白かったので共有したくなりました。
ソースコード
記事を読んで気になった方は、ソースコードを公開していますのでご参考までにみていただければと思います。
Crate
使いたかったら、Crateも公開しているので、どうぞ!
使い方
クレートをインストールして使おうとすると、以下のようなAPIになります。
use wp_query_rs::{ParamBuilder, WP_Query};
fn main() {
let bldr = ParamBuilder::new()
.author(1)
.order(wp_query_rs::SqlOrder::Desc)
.orderby(wp_query_rs::query::WpOrderBy::Date)
.posts_per_page(5)
.post_type("guide".to_string());
let wp_q = WP_Query::new(bldr.params()).unwrap();
let titles = wp_q
.posts
.iter()
.map(|p| p.post_title.clone() + ", published: " + &p.post_date.to_string());
for (i, title) in titles.enumerate() {
println!("Title {}: {}", i + 1, title);
}
}
上記のコードを自身のWP開発用データベースに対して実行すると以下のように出力されます。
Title 1: BroadCast test 4, published: 2023-08-09
Title 2: Casino de Español, published: 2023-08-01
Title 3: Casino un Vaso de Agua, published: 2023-07-27
Title 4: Casino Queso, published: 2023-07-27
Title 5: Casino Orange, published: 2023-07-25
基本的にWP_Queryの使い方をそのままRustに持ってきたかったので、WP_Queryに入っているほとんどの条件を上記にParamBuilderで追加できるようになっています。
上記のような使い方をするには以下の環境変数を設定していないといけません。WordPressの公式Dockerイメージが指定するものと同じです。
WORDPRESS_DB_HOST=localhost
WORDPRESS_DB_USER=root
WORDPRESS_DB_PASSWORD=password
WORDPRESS_DB_NAME=wordpress
なお、自身のmysqlコネクションプールを用意している場合は、以下の関数でコネクションのインスタンスを渡せばできます。
let wp_q = WP_Query::with_connection(&mut conn, bldr.params());
実装で大変だったこと、面白かったこと
大変でありながら、それが故に解決した時の楽しさがある。それがまさに我々が愛するプログラミングの醍醐味ではありませんか?
カラム数が多かった
Rustのmysqlクレートには、query_mapを使うと自動的にSQLのバイナリーをRustの型に変換してくれるという非常にありがたい機能があります。
let selected_payments = conn
.query_map(
"SELECT customer_id, amount, account_name from payment",
|(customer_id, amount, account_name)| {
Payment { customer_id, amount, account_name }
},
)?;
引用:公式ドキュメント – https://docs.rs/mysql/latest/mysql/
しかし、この機能は12項目までしか使えず、12項目以上だと手動て列(Row)を開梱していかないといけません。
WordPressのwp_postsテーブルの絡む数は十数項目ありますので、手動開梱になります。
pub fn unwrap_row(row: &mut Row) -> Result<WP_Post, FromValueError> {
let id: u64 = row.take_opt(0).unwrap()?;
let post_author: u64 = row.take_opt(1).unwrap()?;
let comment_count: u64 = row.take_opt(2).unwrap()?;
let post_parent: u64 = row.take_opt(3).unwrap()?;
...
let post_content_filtered: String = row.take_opt(19).unwrap()?;
let guid: String = row.take_opt(20).unwrap()?;
let post_type: String = row.take_opt(21).unwrap()?;
let post_mime_type: String = row.take_opt(22).unwrap()?;
Ok(WP_Post {
ID: id,
post_author,
post_date,
...
menu_order,
post_type,
post_mime_type,
comment_count,
})
}
SQLクエリの値の順番を間違えなければうまくいきますが、この解決に至るまで苦労がありました。
mysql_common::ToValueのTraitを使えば楽に値をバイナリーに変換できる
mysql_commonというクレートにはMySQLのPrepared Statementクエリーで使えるように値を変換するValueというEnumがあります。これがとても興味深い作りです。
/// Client side representation of a value of MySql column.
///
/// The `Value` is also used as a parameter to a prepared statement.
#[derive(Clone, PartialEq, PartialOrd)]
pub enum Value {
NULL,
Bytes(Vec<u8>),
Int(i64),
UInt(u64),
Float(f32),
Double(f64),
/// year, month, day, hour, minutes, seconds, micro seconds
Date(u16, u8, u8, u8, u8, u8, u32),
/// is negative, days, hours, minutes, seconds, micro seconds
Time(bool, u32, u8, u8, u8, u32),
}
引用: mysql_common – https://docs.rs/mysql_common/latest/mysql_common/
ちなみに、文字列はBytesになります。RustのStringの考え方と似ていて面白いと思いました。元を言えばUTF8の文字列とはそういうものなので、JavaScript開発の自分では文字列に十分に敬意を払っていなかったと反省しています。
上記のEnumの他に、ToValueというTraitがあり、これを満たせば、Valueのバイナリーに変換してくれるのです!
pub struct DateQuery {
pub year: Option<u16>,
pub month: Option<u8>,
pub day: Option<u8>, //Day of the month (from 1 to 31).
pub hour: Option<u8>, // Hour (from 0 to 23).
pub minute: Option<u8>, // Minute (from 0 to 59).
pub second: Option<u8>,
pub after: Option<DateQueryAfterBefore>,
pub before: Option<DateQueryAfterBefore>,
pub inclusive: bool,
pub column: DateColumn,
pub relation: SqlConditionOperator,
}
impl ToValue for DateQuery {
fn to_value(&self) -> mysql_common::Value {
mysql_common::Value::Date(
self.year.unwrap_or(2023),
self.month.unwrap_or(1),
self.day.unwrap_or(1),
self.hour.unwrap_or(0),
self.minute.unwrap_or(0),
self.second.unwrap_or(0),
0u32,
)
}
}
上記のように、自分の場合は、after等はクエリに無関係(別のところで使う)ので、ToValueで時刻に関係する値だけをとっておくようにできました。
let date_query = DateQuery::new();
date_query.to_value() // mysqlが使えるValue型に変換されます!
とても面白かったのです。
まとめ
WordPressというブログ巨人にRustのサビをもたらしてみましたが、改めてWordPressというのは体力の絶えない悪魔だなと思いました。
やっていることがそんなに複雑に思えないのですが、そのAPIを一つまともに実装しようとするとかなりの努力が必要なのです。WordPressに賛否の声があるかと思いますが、取って代わるものはすぐに現れないでしょう。
逆に、Rustで部分的に書き換えていけたら面白いのかもしれません。無理でしょうが。
Rustの勉強が非常に楽しくてこのちょっとしたプロジェクトが一段落したから、筆者はまた勉強に励みたいと思います!