How to handle dependencies between Rails models in packs-rails experimentally?

This message was imported from the Ruby/Rails Modularity Slack server. Find more info in the import thread.

Hello folks! We’re currently evaluating packs-rails, experimentally extracting a first pack (roughly at the level of a typical Rails engine). One of the first tricky dependency bits we encountered was about associations between models within and the pack and models in the main pack. Any pointers on handling dependencies between Rails models (the fundamental, app-wide ones) in terms of public/private API?

Message originally sent by slack user U71KS21KBNC

We’re currently in the middle of doing the exact same thing and have been looking at namespacing our models to achieve this.

By adding a module (using the name of the pack) around the models of the pack, we have to specify class_name for any associations defined outside of the module, but we end up being very explicit that we are crossing a pack boundary.

Something else that’s interesting about this is we can also start having pack “variants” of models. For example we could have a base User model and then have a Pack::User < User model. For any associations to :user defined within the Pack namespace Rails will automatically use Pack::User and not User without having to specify a class_name

A downside of this is that STI and polymorphic type data stored in the database either needs to be migrated or, what we found works pretty well, we need to add self.store_full_class_name = false on the models that require this.

We would love to hear other teams’ approaches on this as well

Message originally sent by slack user U712YWCKK8T

We have a similar pattern. Suppose you have Foo and Pack::Bar and Pack::Bar belongs to Foo. The pattern I’ve been using (but am not super happy with) is to define both ends of the relationship in packs/bar.rb, e.g.

module Packs
  class Bar < ActiveRecord::Base
    belongs_to :foo
  end
end

Foo.class_eval do
  has_one :pack_bar, class_name: "Pack::Bar", dependent: :destroy
end

It’s working fine for us, but it feels kinda gross

Yeah, it’s a shame it’s not possible to set up a mapping for the values stored in type columns for STI / polmyorphic associations. :grimacing: But I’m getting sidetracked.

Thanks for your feedback! I guess it’s one of those things where dependencies are more implicit and cannot easily be discovered by static analysis like in Packwerk. Having to specificy a class_name is a good thing then, I agree. :+1:

Message originally sent by slack user U70TIGAX94P

Which dependencies in this context can not be discovered by packwerk?

Message originally sent by slack user U70TIGAX94P

If the underlying question here is what to do with cyclical associations, seeing that packwerk doesn’t allow cyclical dependencies, I would encourage people to take a look at DDD aggregates. They are essentially trees.

Do we really need cyclical associations?

No, not so much about cycles, although that’s also interesting with AR associations, true. “Not discovered”: I meant associations defined based on conventions, without a class_name .

Message originally sent by slack user U712YWCKK8T

Caveat for the solution I showed earlier: it breaks when Zeitwerk uses lazy loading, i.e. when running locally. So I’m not happy with that solution

Message originally sent by slack user U70TIGAX94P

Packwerk has special handling for associations and should understand things like has_one :product. Is that what you mean by associations defined based on conventions?

Packwerk has special handling for associations and should understand things like has_one :product.

Really? That’s… wild. Is that documented somewhere? Didn’t stumble across that so far.

Message originally sent by slack user U70TIGAX94P

don’t think it is explicitly documented, but it is right here https://github.com/Shopify/packwerk/blob/main/lib/packwerk/association_inspector.rb

Thanks for pointing that out!