Multi-table Data Retrieval via Single Frontend Request
Last updated
Last updated
We will not implement this at this time, as we will prioritize simplicity and clarity by adhering to the five conventional Rails actions. This approach ensures that we consistently deliver "pure" models or arrays of models to the frontend, and as such, we won't be implementing custom multi-table retrieval via a single request in the immediate future.
Overview:
Potential Implementation Scenarios (via custom controller actions)
ChatGPT conversation about this (for context & background info on decision)
Consistent TypeScript Typing for Predictability
Using consistent response types for standard CRUD actions ensures TypeScript types remain predictable.
Example:
A GET request to /projects
endpoint should reliably return data of type Project
(:show) or Project[]
(:index) ().
Knowing this allows for consistent typing on the frontend, enhancing scalability and maintainability.
Also, these frontend types should then be expected to stay in sync with their respective database models --project.rb
in this example
Varying this response format arbitrarily makes frontend typing challenging and can introduce scalability concerns.
Maximizing React Query's Caching Benefits
React Query provides built-in caching that can be effectively leveraged when sticking to standard CRUD actions.
By retrieving pure, singular data types, we can use these as "building blocks", pulling from cache and combining them on the frontend as needed and however needed. This provides great flexibility, optimizes performance, and reduces unnecessary backend calls.
Simplicity in Architecture
Sticking to conventional CRUD operations maintains a clear and straightforward backend structure, reducing complexity.
(Introducing combined data endpoints can add layers of complexity to both the backend and frontend, potentially leading to maintenance challenges and inefficiencies. See more about this in the ChatGPT conversation below.)
Additional Points:
Enhanced Debuggability
Keeping the data retrieval pattern consistent makes it easier to debug issues. When each endpoint has a predictable behavior and output, it's simpler to pinpoint and address potential problems.
Scalability with Evolving Requirements
Future features or changes might necessitate new data combinations or transformations. Keeping the initial design simple and modular allows for more effortless scalability and adaptations based on evolving requirements.
While the above reasons make a strong case for keeping CRUD actions pure and predictable, there are scenarios where custom controller actions can be beneficial:
Complex Business Logic Demands:
Sometimes, a particular business process requires a unique combination of data that would be inefficient to assemble on the frontend. In such cases, a custom backend endpoint might be the optimal solution.
Performance Considerations:
If combining data at the database level (through joins or other techniques) and retrieving it in a single call provides a significant performance boost over making multiple separate requests, a custom endpoint might be justified.
Aggregated Data Needs:
For operations like analytics or reporting, where aggregated data from multiple tables is required, a custom endpoint that retrieves the combined data set is often more efficient than multiple separate calls.
Third-Party Integrations:
When integrating with third-party services, there might be instances where a custom data format or combination is needed. Custom controller actions can cater to these specific requirements.
Frontend Complexity Mitigation:
If combining data on the frontend becomes too intricate, leading to excessive logic, state management, or rendering challenges, it might be simpler and more maintainable to introduce a custom backend action to streamline the process.
However, even when implementing custom controller actions for these scenarios, it's essential to maintain consistency in response formats and ensure proper documentation to keep the system understandable and maintainable.
Image:
Markdown (copy/pasted convo) (same as image):
Ok, I have a question. I like that with our API design, we will have conventional Rails standard CRUD actions and can expect back "pure" models (arrays of models) from those index/show functions. However, sometimes on the front end, we want to combine data from several different tables (as an example, let's say 3 total tables). If we stick with standard CRUD actions, we have to make 3 api calls, then combined the data on the frontend. I think it would be more efficient to be able to make 1 api call, and handle getting the data from the 3 tables on the backend. What are the standard ways to go about doing this? (the ones that come to mind are: (1) query params (e.g. "joins"), (2) creating new controller actions that specifically do this (though this doesn't seem very scalable), (3) ...that's all I've got for now)ChatGPT
You're correct in noting that while the conventional Rails CRUD actions provide a predictable API structure, sometimes the needs of a client application necessitate retrieving data that spans multiple models or tables. To serve such combined data, there are several strategies you can use:
Query Parameters with Conventional Actions: This approach involves using query parameters to signal to the API that additional related data should be included with the response.
Pros: Reuses conventional actions. Can be standardized with proper documentation.
Cons: Overloading conventional actions might make them complex over time.
Example: /users?include=posts,comments
Custom Actions: If a specific combined dataset is requested frequently, you can create a custom controller action to serve it.
Pros: Explicit and clear about its intent.
Cons: Can make controllers bloated if overused.
Example: def user_with_posts_and_comments
in UsersController
.
Dedicated Endpoints: Create dedicated controllers for combined datasets.
Pros: Isolates logic and makes it easier to manage combined datasets.
Cons: Might introduce new patterns which deviate from standard Rails conventions.
Example: A UserReportsController
that provides data-driven insights combining multiple models.
Graph-Based APIs (GraphQL): GraphQL is a query language for APIs. Instead of having multiple endpoints, you have a single endpoint where the client specifies exactly what data it needs.
Pros: Extremely flexible. Frontend can request exactly what it needs.
Cons: Adds a new layer of complexity. Requires learning GraphQL.
Example: Using the graphql-ruby
gem to setup a GraphQL endpoint.
Service Objects: Instead of stuffing logic into controllers, use service objects. These are plain Ruby objects responsible for handling specific business logic, like gathering data from multiple models.
Pros: Encourages single responsibility principle and keeps controllers clean.
Cons: Adds more files and abstractions.
Example: A UserReportService
that pulls data from User
, Post
, and Comment
models.
JSON Views (e.g., Jbuilder): Jbuilder is a DSL for crafting JSON responses. You can craft complex JSON structures that span multiple models.
Pros: Offers more control over the JSON output. Can combine data from multiple models in one view.
Cons: Another layer of abstraction.
Example: Using Jbuilder to craft a combined user
, posts
, and comments
JSON view.
Includes & Eager Loading: If the goal is to reduce the number of database queries, consider using .includes
or .eager_load
to load related data in a single or fewer queries.
Pros: Reduces database query count.
Cons: Might load more data than needed. Might introduce complex SQL joins.
Example: User.includes(:posts, :comments)
Serializer Libraries (e.g., Fast JSON API, ActiveModel::Serializers): These libraries allow you to define how a model should be serialized. They often support including associated records.
Pros: Standardized way of serializing data. Can include related records.
Cons: Additional abstraction and dependency.
Example: Using Fast JSON API to serialize a user
with its posts
and comments
.
Recommendation:
If the combined datasets are standard across the application, and you anticipate needing more such combinations, consider using GraphQL. It's a bit of an initial investment but can be worth it in the long run.
If the need is sporadic, using a combination of query parameters, dedicated endpoints, or service objects might be more appropriate. You can also utilize serializer libraries or JSON views to craft the desired output.
Remember, the goal is to find a balance between frontend and backend efficiency, code maintainability, and developer happiness.
Hmmm. Since we are using React Query on the front end (which has a good caching system, etc.), maybe it would be overkill (e.g. introduce too much complexity) to try to introduce this right now.ChatGPT
Using React Query on the frontend can indeed help alleviate some of the challenges associated with making multiple requests, thanks to its caching and data synchronization features. Given this context, let's evaluate the decision:
Advantages of Sticking with React Query and Multiple Requests:
Caching: React Query provides caching out of the box, which means subsequent requests for the same data can be served from cache instead of hitting the server again.
Simplicity on the Backend: By not introducing new combined endpoints or adding GraphQL, you keep your Rails backend simpler and closer to conventional CRUD actions.
Incremental Requesting: If the user navigates between views/pages, only the new data might need to be fetched, while the rest can be pulled from React Query's cache. This could result in better perceived performance.
Flexibility: Fetching data in smaller, more granular chunks gives you more flexibility on the frontend. If you decide to change how data is displayed, you might not need to make any backend changes.
Potential Challenges:
Overhead from Multiple Requests: Even with caching, making multiple requests can have more overhead than a single consolidated request, especially if the requests can't be parallelized effectively.
Data Consistency: Managing data consistency across multiple requests might be a bit more challenging. For example, if data from one request is updated, it might affect data from another request.
Complexity in Frontend Logic: If combining data from multiple requests frequently, the logic on the frontend can become complex.
Given the capabilities of React Query and considering the potential challenges, here's what you might consider:
Start Simple: If you're in the early stages of your application or feature, it's often a good idea to start simple. Make multiple requests and let React Query handle caching. As the application evolves and you identify specific performance bottlenecks or areas of complexity, consider optimizations.
Evaluate Real-World Usage: It's essential to understand how users interact with your application. If they typically access views that require combined data from multiple models, then optimizing for that scenario makes sense. Otherwise, it might be premature optimization.
Avoid Over-Optimizing Early: While it's tempting to optimize for all possible scenarios from the beginning, it's often more efficient to build what you know you need now and then refactor or optimize based on real-world usage and performance data.
In summary, given React Query's capabilities, starting with multiple requests and leveraging its caching and data synchronization might be a good approach. As you gather more data on how your application is used and where the bottlenecks are, you can make more informed decisions on potential optimizations or backend changes.