【graphql-batch】カスタムローダーの運用を考えてみた
実際のプロジェクトでカスタムローダーを運用していると、graphql-rubyリポジトリに用意されているexamplesのように抽象化されたデータローダーと、アプリケーション固有の要件を満たすためのロジックを含んだデータローダーをそれぞれ作成することがあります。
上記のようなローダーをドメインローダーとユーティリティローダーに分類すると運用しやすくなるのではないかと思ったので、ドメインローダーとユーティリティーローダーの分け方について整理してみようと思います。
ユーティリティローダー
graphql-ruby公式のexamplesに存在するAssociationLoaderやRecordLoaderのようなもので、基本的にはどのようなActiveRecordのオブジェクトに対しても適用することができるような共通処理を扱うローダーです。
ドメインローダー
ローダー内にロジックを持たせたい場合があります。 例えば、記事投稿サービスを開発しているとして、複数のタグ情報からandやorの組み合わで対象の記事の一覧や記事の投稿数を取得したいとします。 perform時(バッチ処理の実行時)にLoaderの初期化に保持しておいた条件を使用して - タグのリストを分割したクエリによるidのリスト生成 - サブクエリ などを用いてkeysで取得できる集合に対して絞り込みをしたくなったりします。
perform時の特定の条件がポイントで、「特定の条件」はアプリケーションのユースケースに依存することが多いため、どのモデルにも適用できる汎用的な作りにすることが難しかったりします。
以下は、タグに応じてand検索とor検索を組み合わせた結果を取得し、レコードのカウントを取りたい場合を想定したローダーの例となります。
module Loaders class PostCountLoader < GraphQL::Batch::Loader def initialize(model, column: model.primary_key, where: nil, joins: nil, distinct_column: nil, tag_ids: nil) @model = model @column = column.to_s @column_type = model.type_for_attribute(@column) @where = where @joins = joins @distinct_column = distinct_column @tag_ids = tag_ids end def load(key) super(@column_type.cast(key)) end def perform(keys) query(keys).each { |key, count| fulfill(key, count) } keys.each { |key| fulfill(key, 0) unless fulfilled?(key) } end private def query(keys) scope = @model scope = scope.joins(@joins) if @joins if @tag_ids # 絞り込みに向けて複数のクエリを組み合わせ、検索の元となるIDのリストを作成する必要がある and_tag_ids = Tag.and_category_tags(@tag_ids).pluck(:id) post_ids = PostsTag.or_category_tags(@tag_ids).pluck(:tag_id) if and_tag_ids.count.positive? query = PostsTag.by_tags(and_tag_ids) query = query.where(post_id: post_ids) if post_ids.count.positive? post_ids = query end # 作成したidのリストを使用して検索 @where = @where.merge(id: post_ids) end scope = scope.where(@where) if @where scope.where(@column => keys).group(@column).distinct(@distinct_column).count end end
このようなローダーを「ドメインローダー」と分類します。
ユーティリティローダーとドメインローダーの分け方
今のところ以下のような観点で分類するとよさそうだと思っています。
- どのModelでも使用することができる
- →ユーティリティローダー
- Loaderのinitializerの引数やLoaderにアプリケーション固有の属性やロジックが含まれており、対象のModelが限定される
- →ドメインローダー
Railsでのディレクトリ構造
graphql/loaders
├utils
│ ├AssociationLoader.rb (ユーティリティーローダー)
│ ...
├ PostCountLoader.rb (ドメインローダー)
...
方針にするとよさそうです。
まとめ
カスタムローダーを共通のユースケースを想定したものと、アプリケーション固有のユースケースを想定したものに分類することで、役割が明確になり運用しやすくなるのではないかということを書いて来ましたが、まだ実際の運用には至っていないので、今後運用してみた結果も別記事かこの投稿に更新していこうと思っています。
参考
https://github.com/rmosolgo/graphql-ruby https://github.com/Shopify/graphql-batch
graphql-batchのカスタムローダーと作り方
カスタムローダーの作り方
graphql-batchのリポジトリに用意されているexamplesを参考に、カスタムローダーの作り方を見ていこうと思います。
まずLoaderと定義されているpublicメソッドを書き出してみます。
- AssociationLoader
- initialize
- load
- cache_key
- perform
- RecordLoader
- initialize
- load
- perform
- WindowKeyLoader
- initialize
- perform
- HTTPLoader
- initialize
- perform
- ActiveStorageLoader
- initialize
- perform
リファレンス実装されているどのLoaderでも共通して実装されているメソッドは、以下です。
- initialize
- perform
また、RecordLoaderでは、加えて「load」メソッド、AssociationLoaderでは加えて「load」、「cache_key」メソッドが実装されています。
共通で実装されるメソッドから役割や機能を見ていきましょう。
RecordLoaderを例にします。
initialize
initializeでは、graphql-rubyのfieldでLoaderを呼び出す際の、forメソッドに渡す引数を定義します。
class RecordLoader < GraphQL::Batch::Loader def initialize(model, column: model.primary_key, where: nil) super() @model = model @column = column.to_s @column_type = model.type_for_attribute(@column) @where = where end ... end
field :product, Types::Product, null: true do argument :id, ID, required: true end def product(id:) RecordLoader.for(Product).load(id) end
perform
performでは、GraphQL resolverの解決後に遅延実行されるバッチ処理を記述します。 各fieldの呼び出し時にキャッシュしてあるkeyの一覧を受け取った上で実行される、バッチ処理のロジック定義します。 バッチ処理の実行後はキャッシュキーを使用して、Promiseをfulfillして完了させます。
class RecordLoader < GraphQL::Batch::Loader ... def perform(keys) query(keys).each { |record| fulfill(record.public_send(@column), record) } keys.each { |key| fulfill(key, nil) unless fulfilled?(key) } end ... end
最低限、initializeとperofmメソッドを実装することでカスタムローダーが作成できますが、一部の例ではそれ以外のメソッドを実装しているので、そちらも見ていきたいと思います
load
cache keyへの登録処理をカスタマイズする場合に使用します。
RecordLoaderの場合、cache keyはそのまま、whereメソッドの特定のカラムの値になるため、loaderメソッドをoverrideして適切な型にあらかじめ変換しています。
... def load(key) super(@column_type.cast(key)) end ...
AssociatoinLoaderの場合、対象となるモデルに指定されたassociationが存在するかどうかのチェックと、すでに対象のレコードのassociationが呼び出されている場合はcache keyへの登録はスキップするようになっています。
... def load(record) raise TypeError, "#{@model} loader can't load association for #{record.class}" unless record.is_a?(@model) return Promise.resolve(read_association(record)) if association_loaded?(record) super end ...
cache_key
AssociatoinLoaderのみで実装されています。 cache keyをDBから取得したレコードのIDで設定すると、別々のAssociations間でcache keyが競合することから、ActiveRecordのレコードオブジェクトのIDをcache keyとして扱うようになっています。
# We want to load the associations on all records, even if they have the same id def cache_key(record) record.object_id end
まとめ
カスタムローダーを作成するにあたっての基本は - initializeメソッド - performメソッド を実装することが基本となります。
Initializeでバッチ処理対象のキーを受け取る、performでキャッシュされたキーを元にバッチ処理を実行し、Promiseを完了させます。
また、必要に応じてloadやcache_keyなどでキャッシュ対象のキーの前処理やcache_keyのカスタマイズなどを行う流れとなります。
実際にカスタムローダーを作成する場合は、N+1クエリの解消目的とすることが多いことから、まずはRecordLoaderやAssociationLoaderをベースにperformメソッドでのバッチ処理のロジックを拡張するとよいかと思います。
参考
graphql-batchの仕組みを調べてみた
graphql-batchを用いたバッチ処理の仕組みについて調べてみたので、まとめておきたいと思います。
graphql-batchの仕組み
公式のexamplesにあるRecordLoaderを使って、実際のバッチ処理を例にして処理流れや仕組みをみていきます。 例として、記事一覧の記事ごとに投稿したユーザーを一緒に取得する場合を想定します。
カスタムローダーの準備
モデルから特定のattribute(カラム)と任意の条件の組み合わせでレコードを取得する際のバッチ処理を扱うもので、belongs_toな関連を持つレコードの取得時に実装されることが多いです。
record_loader.rb
class RecordLoader < GraphQL::Batch::Loader def initialize(model, column: model.primary_key, where: nil) super() @model = model @column = column.to_s @column_type = model.type_for_attribute(@column) @where = where end def load(key) super(@column_type.cast(key)) end def perform(keys) query(keys).each { |record| fulfill(record.public_send(@column), record) } keys.each { |key| fulfill(key, nil) unless fulfilled?(key) } end private def query(keys) scope = @model scope = scope.where(@where) if @where scope.where(@column => keys) end end
fieldの定義
:postsはTypes::Postのオブジェクトを複数取得するfieldで、Types::Postのオブジェクトは記事の投稿ユーザーを表すuser fieldを持っています。 user fieldでは、object.user_idをkeyとして、RecordLoaderのforメソッド、loaderメソッドを呼び出しています。
class Types::User < GraphQL::Schema::Object field :name, String, null: false end class Types::Post < GraphQL::Schema::Object field :user, Types::User, null: false def user RecordLoader.for(User, :id).load(object.user_id) end end field :posts, [Types::Post], null: true def posts(id:) Post.all end
RecordLoaderを使用せずにクエリを実行すると、user fieldが記事の取得毎によびだされるため、N+1クエリが発生するからです。
N+1クエリが発生する例
class Types::Post < GraphQL::Schema::Object field :user, Types::User, null: false def user User.find(object.user_id) end end
上記のようなN+1クエリを防ぐために、RecordLoaderがどのようにバッチ処理を行っているのかを追っていきます。
Loaderの呼び出し
まず、user fieldでは、RecordLoader.for(User, :id)が呼び出されます。 forメソッドには、RecordLoaderクラスのinitializerで定義した引数を渡します。
内部的には、fieldから渡された引数を元にRecordLoaderクラスのインスタンスを作成し、executorにRecordLoaderクラスのオブジェクトを登録しています。
executorには、forで指定した引数とオブジェクト自身(RecordLoader)の組み合わせをキー(loader_key)としてローダーオブジェクトが登録されます。
lib/graphql/batch/loader.rb
... else def self.for(*group_args) current_executor.loader(loader_key_for(*group_args)) { new(*group_args) } end end ... # loaderのキーを作成 def self.loader_key_for(*group_args, **group_kwargs) [self, group_kwargs, group_args] end
すでに同じキーでローダーオブジェクトが登録されている場合は、既存のローダーオブジェクトを使い回します。
forメソッドにより、RecordLoaderオブジェクトの作成もしくは取得に成功すると、load(object.user_id)でバッチ処理の対象となるモデルのカラムの値をLoaderに渡します。
内部的には、loadメソッドは親クラス(GraphQL::Batch::Loader)のloadメソッドを呼び出しています。
RecordLoaderクラスでは、loadメソッドに渡す引数はクエリの実行時にwhereメソッドの引数となるため、フィルター対象のカラムに合わせた型にキャストしています。
def load(key) super(@column_type.cast(key)) end
superで親クラスのloadメソッドを呼びだし、Promiseオブジェクトの作成を行い、キューおよびLoaderのキャッシュストレージ(インメモリ)に保存します。
def load(key) # キャッシュからkeyを元にPromiseオブジェクトを取得する # ない場合はsourceを自身(RecordLoader)としてキャッシュに保存した上でPromiseオブジェクトを作成し、作成したオブジェクトを返す cache[cache_key(key)] ||= begin queue << key ::Promise.new.tap { |promise| promise.source = self } end end
このタイミングでuser fieldではPromiseのオブジェクトがセットされている状態になります。 RecordLoaderの観点からは、user fieldに対して、複数のPromiseオブジェクトを持つ1つのRecordLoaderオブジェクトという形になっています。
Promiseオブジェクトのlazy_resolve
ここから、取得対象のPostの数だけuser fieldが呼び出されることにより、Promiseオブジェクトが作成された後の処理を見ていきたいと思います。
resolverがresolveされた後、lazy_resolve(::Promise, :sync)メソッドにより、作成されたPromiseオブジェクトのsyncメソッドが呼び出されます。
lazy_resolveは、graphql-rubyで用意されている遅延実行用の仕組みです。
lazy_resolve(::Promise, :sync)メソッドは、graphql-batchのセットアップ手順にあるuse GraphQL::Batchで定義されることになります。
どのようにlazy_resolveが呼び出されているかは、lib/graphql/batch.rbのself.useメソッドを見てみるとよいです。
xx_schema.rb
class MySchema < GraphQL::Schema query MyQueryType mutation MyMutationType use GraphQL::Batch end
Promiseオブジェクトのsyncメソッドが呼び出されると、Promiseのsourceとして登録されているLoaderクラスのwait(sync)が実行されます。
waitメソッドはloaderのresolveメソッドを実行します。
resolveメソッドはLoaderクラスの継承クラス(RecordLoader)で実装されたperformメソッドを実行します。その際引数として、load_keysとしてキューに保存されていたkeyの一覧が渡されます。
Loaderオブジェクトはすでに呼び出されたPromiseオブジェクトの実行結果を状態として保持しているため、resolved?でperformでロジックの実行が必要がないPromiseオブジェクトの場合はそのまま処理を終了させます。(最初の実行ですでに解決されているため)
def resolve #:nodoc: return if resolved? load_keys = queue @queue = nil around_perform do perform(load_keys) end check_for_broken_promises(load_keys) rescue => err reject_pending_promises(load_keys, err) end
Loaderのperformメソッドでは、受け取ったkeysを元にバッチ処理を実行し、keyに対応するキャッシュ中のPromiseオブジェクトをfulfill(成功したものとして完了)させます。
fulfillする際には、後続の処理(fieldへのマッピングやthenのコールバック)で必要となる値、RecordLoaderの場合は取得したレコードのカラムをkey(loadでkeyにした値)に、実際のレコードをvalueとして引数に渡します。
RecordLoaderでは指定したkeyに対応するレコードが存在しない場合もあるため、keys.each { |key| fulfill(key, nil) unless fulfilled?(key) }で漏れた分のkeyに対応するPromiseを解決しています。
record_loader.rb
... def perform(keys) query(keys).each { |record| fulfill(record.public_send(@column), record) } keys.each { |key| fulfill(key, nil) unless fulfilled?(key) } end private def query(keys) scope = @model scope = scope.where(@where) if @where scope.where(@column => keys) end ...
ここでバッチ処理が完了し、user fieldへバッチ処理を経て取得した値がマッピングされることになります。
Promiseを使うメリット
ここまでgraphql-batchの処理を追ってきましたが、その中でPromiseオブジェクトがよく登場してきました。 lazy objectを作成してlazy_resolveを使用するという、graphql-rubyで用意されている機能を使うだけでもバッチ処理は可能ですが、graphql-batchはバッチ処理時の
- .thenでLoaderの実行結果を使用したデータの加工ができること
- Loaderの結果に依存する別のLoaderの呼び出しができること
- Promise.allで複数のLoader(promise object)をまとめることができる
などのような後工程でのデータの変換や直列、並列なバッチ処理をシンプルに実現するための手段としてPromiseを使用しているようです。
まとめ
fieldに定義したLoaderがどのように遅延実行されバッチ処理が実現されるのかを、graphql-batchのソースコードを読みながら自分なりにまとめてみました。
自分への備忘として、最後にgraphql-batchによるバッチ処理のポイントをまとめておきたいと思います。
- 1fieldに対して、1Loader、Loaderはfield呼び出しで作成された複数のPromiseオブジェクトを保持する実装であること
- lazy_resolve実行時のPromiseオブジェクトのresolveは1Loaderにつき一度だけ実行され、同じものはスキップされる
- カスタムLoader実装時はperoformでのfulfill処理が後続処理(fieldへの値のマッピングやthenなどの後工程への値のパス)に向けて必須になる
- Promiseを使うことで、後工程でのデータの変換や直列、並列なバッチ処理をシンプルに実現するための手段としてPromiseを使用している
参考
【GraphQL Ruby】InputObjectのField参照について
graphql-rubyではFieldとして引数をとることができます。さらに引数を定義するための型として、以下があります。
- Scalar
- Enum
- InputObject
- List
- NonNull
特にInputObjectに関して、オブジェクトに定義されたFieldを参照する方法として
- メソッドとして参照する方法
#[]としてSymbolで参照する方法
が用意されていますが、参照方法が2通りある背景と使い分けについて整理しておきたいと思います。
参照方法が2通りある背景
graphql-rubyのInputObjectに関する公式ドキュメントを見てみると「Using Input Objects」の欄に
- calling its method, corresponding to the name (underscore-cased)
- calling
#[]with the camel-cased name of the argument (this is for compatibility with previous GraphQL-Ruby versions)
とあり、#[]は前バージョンとの後方互換性のためのものであるようです。
#[]による参照は非推奨なのでしょうか?もう少し調べたいと思います。
graphql-rubyのChangelogに参照方法の変更に関しての記述がありました。
- calling its method, corresponding to the name (underscore-cased)
- Field arguments may be accessed as methods on the
argsobject. This is an alternative to#[]syntax which provides did-you-mean behavior instead of returningnilon a typo. #924 For example:...The old syntax is not deprecated.
# using hash syntax: args[:limit] # => 10 args[:limittt] # => nil # using method syntax: args.limit # => 10 args.limittt # => NoMethodError
- Hash形式の参照時にタイポなどで未定義のFieldへのアクセスした場合にnil値を返すのではなく、NoMethodErrorにするための機能であること
- 旧式(Hash形式での参照方法)も今の所、非推奨ではない
ことが分かりました。
使い分け
- Changelogにもあるように、タイポに気づきやすくする
- 後のアップデートで非推奨になる可能性がある
ことから、基本的にメソッドによるFieldの参照(args.limit)を行う方法で実装するとよさそうです。
参考
https://graphql-ruby.org/fields/arguments.html https://github.com/rmosolgo/graphql-ruby/blob/93ecc6d8c21606a571d0c87c6e3430b71b5acd84/CHANGELOG.md#170-18-sept-2017 https://graphql-ruby.org/type_definitions/input_objects.html#using-input-objects https://graphql-ruby.org/api-doc/2.0.20/GraphQL/Schema/InputObject#[]-instance_method
Pumaとメトリクス
WebサーバーとしてPumaを使用している場合の以下についてまとめておく
- 監視の観点
- メトリクスの収集方法
監視の観点
- Pumaの動作するサーバーに問題が起きていないか
- Pumaに問題が起きていないか
Pumaの動作するサーバーに問題が起きていないか
ホストマシンの以下の指標をみておく
- CPU
- sar
- メモリ
- vmstat
- ネットワーク
観点: メモリを使いすぎていないか?
- メモリ容量の上限値を超える場合はworker数減らすかメモリ上限を上げるなどの検討をする
Pumaに問題が起きていないか
監視上の観点とPuma.statsで提供される情報から、指標として使用するとよさそうなものをピックアップしてみる
観点: 必要なプロセスやスレッドが正常に動作しているか?
- puma.workers: 起動中のワーカー数
puma.running: 起動中のスレッド数
設定値通りにプロセスやスレッドが起動しているかを確認する
観点: リクエストに対して受け入れ可能な状態にあるか?
- puma.backlog: キューにあるリクエストの待ち行列の数
puma.pool_capacity: スレッドプール中の使用可能なスレッド数
- pool_capacityのスレッド数が常に0に値の場合は、workerあたりのthread数を増やすか、worker数を増やす
DatadogでPumaのメトリクスを収集する
puma-plugin-statsdgemを使用し、ddagentのカスタムメトリクスとして追加する方法で収集してみた。
※ ddagentがインストールされていてかつ起動されることが前提
Gemfileに以下を追記
... gem 'puma-plugin-statsd' ...
config/puma.rbに以下を追記
... plugin :statsd ...
動作環境の環境変数として以下を追加する
STATSD_HOST=127.0.0.1
ddagentのintegrationでもPumaのメトリクスを収集できるようなので、こちらを使った方がよさそう
参考
https://speakerdeck.com/s4ichi/metorikusuke-shi-hua-karashi-meru-rails-uebusabafalsetiyuningu-kaigi-on-rails-2021 https://blog.naoty.dev/373/
「コーディングを支える技術」を読んでだので、クラスに関して整理してみた
背景
「コーディングを支える技術」を読んだので、クラスに関して理解したことを整理するとともに、 普段Rubyを使う機会が多いため、クラスの機能がRubyでどのように表現できるのか?に関しても考えてみたいと思います。
クラスとは?
クラスという機能がプログラミング言語に導入されるに至った背景や、概念としての機能を整理しておきます。
- オブジェクト指向とクラス
- クラスの機能(概念)
- まとまったもの(object)を作る生成器
- 型
- どのような操作が可能かという仕様
- インターフェース
- コードを再利用する単位
- 継承
- まとまったもの(object)を作る生成器
オブジェクト指向プログラミングにおけるクラスの機能
前述のクラスの機能に関しての概念をもとに、オブジェクト指向プログラミングにおけるクラスの機能を書き出したのが以下となります。
Rubyにおけるクラスの表現
Rubyにおいて前述の「クラスの機能」を表現する方法として以下の2機能が用意されています。
- module
- メソッドと定数をまとめたもの
- インスタンスを生成することはできない
- クラスとその他のモジュールにmix-inすることができる
- 継承することはできない
- class
- インスタンスを作成することができる
- インタンス変数として内部に状態を持つことができる
- 他のクラスから継承することができる
- モジュールを継承することはできない
modlueとclassを使用して、「クラスの機能」をRubyで表現する方法に関して整理や使い分けに関して考えてみようと思います。
まとまったもの(object)を作る生成器
「module」や「class」が該当します。 それぞれの役割として基本的には
- module: メソッドや定数をまとめたいが、内部に状態(インスタンス変数)を持つ必要がない
- class: 内部に状態を持つ必要がある
の観点で使い分けます。
また、型に関しては、Rubyは動的型付け言語であるためオブジェクトの型はそのクラスとは厳密には結びついておらず、オブジェクトが反応するメソッドの集合であることから、objectの生成器としてではなく、操作の仕様(後述)として分類することにしました。
どのような操作が可能かという仕様
- ダックタイピング
- オブジェクトが反応するメソッドの集合
- Javaのinterfaceのようなものを、moduleを使って表現することもできる
動的型付け言語のため、インターフェースの定義のようなものは言語機能としては用意されていないため、使い分けなどの観点などはなさそうでした。 (インタフェースの定義を実現したい場合は、moduleを使用することで、近しいものを実装することは可能のようです。) また、ポリモーフィズムはダックタイピングにより実現されています。
コードを再利用する単位
- 継承
- Rubyにおいては単一継承のみが可能
- 多重継承を実現する場合はmix-inを活用する
- mix-in
- 基本的にモジュールのメンバメソッドやクラスメソッドに委譲して使用する
- インスタンス変数は使用せず、委譲し、引数経由で状態の受け渡しを行うことで再利用性を高く保ようにする
classによる単一継承、moduleを活用したmix-inが用意されています。 基本はmix-inを活用し、委譲によりコードの再利用をすることが推奨されていますが、以下の観点で使い分けを行うとよさそうです。
- 継承
- 1つのクラスから再利用できる部分が1つだけの場合
- mix-in
- 1つのクラスから再利用できる部分が複数にわたる(オブジェクトに共通の振る舞いがある)場合
どちらの場合もテンプレートメソッドパターンやフックメソッドを使用して、使用者に変化可能な箇所とそうではないインターフェースに関して伝える努力をすることが重要です。
また、継承やmix-inの使い分け以前に「継承可能なコード」を書くことがポイントとなります。
継承可能なコードの状態とは、以下のような状態を指します。 ※「オブジェクト指向設計実践ガイド」に詳しく書かれています。
- 抽象スーパークラス内のコードを使わないサブクラスがないこと
- リスコフの置換原則に従っていること
- テンプレートメソッドが使用されていること
- 継承する側でsuperを呼び出すことを避け、継承時の親子関係を疎結合にすること(フックメッセージの活用)
- 階層構造を浅く保つこと です。
上記より継承かmix-inかを決めきれない場合には、以下のような流れで設計を進めるとよさそうです。
- 継承可能なコードにする
- その上で、共通の振る舞いを持つオブジェクトがあれば、その振る舞いをmoduleとして切り出しmix-inする
おわりに
クラスという機能が言語仕様として実装された背景や機能としての概念をベースに、Rubyのクラスという機能の表現に関して改めて考えることができました。
gemやフレームワークを提供する側になると使用者にどのように使ってほしいか?をコードからも意図伝える必要が出くるのではないでしょうか。
前述した、Javaのinterfaceに近しいものをmoduleで実現するように、その他の言語におけるクラスの機能を持ち込むための工夫(abstract classを実現する)に関しても今後深堀っていきたいと思います。
参考
【書籍】 www.amazon.co.jp
【その他】
【RealityKit】モデルにMaterialを設定する方法
作成したmodelをEntityとして読み込む
guard let model = try? Entity.load(named: "sample.scnassets/sample") else { return }
読み込んだEntityからmaterialを設定する対象のEntityを検索して、ModelComponentを取得する
if var modelComponent = model.findEntity(named: "設定対象のEntity名").components[ModelComponent.self] as? ModelComponent { }
取得したModelComponentのmaterialsプロパティに任意のmaterialを設定して更新する
if var modelComponent = model.findEntity(named: "設定対象のEntity名").components[ModelComponent.self] as? ModelComponent { modelComponent.materials = [UnlitMaterial(color: UIColor.green.withAlphaComponent(0.8))] // 例として、UnlitMaterialを設定 model.findEntity(named: "設定対象のEntity名").components[ModelComponent.self] = modelComponent }