This message was imported from the Ruby/Rails Modularity Slack server. Find more info in the import thread.
Hey people, how are you doing? When we started applying packwerk in our projects we noticed that one of the hardest things to fix is preventing AR models from crossing packs’ boundaries. Reading conversations in this slack, I think there is general consensus around that…
We realized that as_json is a very useful tool and that we could have our packs’ public APIs call it on the AR model, to return only data without any of the other AR stuff. But on the downside, we’d need to adapt all of our code to work with hashes instead of objects. A possible next step would be creating POROs that store all the values in methods, but that turns out to be A LOT of boilerplate… Also, people could be tempted to start adding more and more responsibilities to those POROs. Ideally, we would like to be able to replace AR models with ActiveModel data-only objects.
So we decided to create a very simple gem called ar2dto (ActiveRecord to DTO) to help with this. It’s in a very early phase, but I’d like to know your thoughts on it, maybe you have similar solutions in your app, or you think this gem could be of help to you, or if you have any concerns about it, etc.
In our opinion, it can be useful because it not only helps reduce boilerplate but also helps standardize how to work with AR models in an app using packwerk. And even if not using packwerk, this gem can help you avoid using AR all over the place, because it’s as simple as calling something.to_dto to get a harmless data object to work within your app. Something that we consider to be key is that it follows a “gradual” mentality, such as packwerk or sorbet, you can start using it gradually and see if it works for you and your team
I think that is because we want to prevent changes to the internal representation to create changes to the external API of a package. I.e., we want to consciously design and control what the API includes.
Regarding your first comment about other similar work that attempted to strip AR functionality, there is also this one from John from GitHub, he gave a talk in RailsConf about it: https://github.com/seejohnrun/proxy_record
And then regarding some of the things you mention like “wanting to consciously design and control what the API includes”, in ar2dto you can use options like methods, include (for associations), exclude & only, which allow to configure what things you want the DTO to grab and expose (or not) from the original Active Record (these options are based on as_json)
This gem is an amazing idea and work from @santib, he may have more comments to add.
We would like to see real-world applications of it and get feedback
Thanks Stephan, your book and Alex’s work have been an inspiration for the gem. I think we need to improve the README, but as Seba said, it’s inspired in ActiveModel’s as_json, which uses serializable_hash and has support for the options he mentioned.
I do see the benefits of creating the POROs yourself, specially if your interface does not match your AR records 1-1. I think ar2dto can be specially helpful during migrations to start using packwerk, were you are already using AR models in other components and you want to start changing that without having to refactor where and how they are used. Still the two approaches can live together and I think that’d be totally fine.
To prevent the “POROs attracting functionality problem” we tend to use structs (of the ruby or the sorbet variety)
We had a huge internal debate around this at some point. My conclusion is that while you might often want “just structs”, in many other cases you want to be able to return behavior that is then injected into other packages. It certainly helps with dependency inversion.
My conclusion is that while you might often want “just structs”, in many other cases you want to be able to return behavior that is then injected into other packages.
just some comments regarding the data only objects returned by the gem:
In some way you can have “behavior” being triggered during the initialization of the object, because the method [record.to](http://record.to)_dto supports the methods option such as as_json does (e.g. [record.to](http://record.to)_dto(methods: [:my_method])). The method will be executed and the result will be stored in the DTO. We also transform the result into something that is considered to be “data only” (e.g. an ActiveRecord object would be transformed into a DTO or hash)
if you need more customizations, you can create your own class and even inherit from AR2DTO::DTO[class_name] (e.g. class UserDTO < AR2DTO::DTO[User] ) and there you could add more behavior
if you want to return some other objects with behaviors in your pack’s public API, that’s fine, this gem is only to help with ActiveRecord models, but it won’t be the only thing you want to return in your public APIs
Hey Matt, the fact that Data objects are truly immutable sounds like a good reason to at least try it out. I’m not sure that it’s gonna work because we have some non-data things like making the objects quack like ActiveModel and the equality comparator. Any thoughts?
To be fair, I kinda like the additional strictness of “this is not an ActiveModel, this is just data”, and I could see this as an option for ar2dto.
For example, if you set active_model_compliace then you get a Data object, otherwise you get what you get today.
If this feels too strict, Data.define classes can be extended in theory, so they might be able to provide enough ActiveModel stuff to make Rails happy.
On the equality comparator, I’m not too sure to be honest, but considering Data.define classes support pattern matching, then they should be able to support this as well. And if they don’t, you can always define the == method for them
Thanks Matt, that makes sense to me. I created this issue https://github.com/santib/ar2dto/issues/41 feel free to add any comment or open a PR. I’ll experiment with it as soon as I have some time.
On another note, have you used them gem in your projects? If yes, how have been your experience with it? Where did it help and what shortcomings did you encounter?
Yes we just recently integrated ar2dto in our project! The introduction was smooth, although we’re still only testing it out now.
The plan is to make it mandatory for all component’s public APIs to return ar2dto objects instead of AR records/relations.
The initial work was done by another engineer, but I should be able to work with it myself over the next few weeks, I’ll make sure to provide any feedback !