Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filter properties in the database instead of in memory #5

Closed
wants to merge 6 commits into from

Conversation

alimi
Copy link

@alimi alimi commented May 29, 2024

This PR has a few changes, but they were all motivated by changing how RailsProperties::Base#properties works. #properties accepts var as an argument and returns the target's matching property object. It has done this by fetching all of the targets property objects and filtering them in memory to find the requested var.

property_objects.detect { |s| s.var == var.to_s } || property_objects.build(:var => var.to_s, :target => self)

Although this filtering returns only one property object, the others are still loaded in memory and associated with the target. This can lead to strange side affects because callers aren't expecting the other objects to be loaded in memory. To fix this we can filter property objects at the database level so only the requested objects are loaded in memory.

property_objects.find_or_initialize_by(var: var)

The migration template has an index on target_type, target_id, var so the database query should be performant.

To avoid conflicts when writing new property objects, memoize the requested property object after it's been initialized. This was done implicitly before when all the objects were loaded in memory. Without this memoization, test like this fail.

Along with this change, we drop support for old versions of Rails. As a result, we can also remove RailsProperties#can_protect_attributes? and the related code. #can_protect_attributes? was used to support the Protected Attributes gem on old versions of Rails, and Protected Attributes doesn't support Rails 5+.

Ali Ibrahim added 5 commits May 29, 2024 14:37
  * Starting in Rails 6, ActiveRecord's Sqlite adapter restricted the
    sqlite3 gem version to ~> 1.4. This is still true in Rails 7.1. See
    rails/rails#35844.
  * Use the same restriction here to ensure a compatible sqlite3 version
    is installed for the dev and test.
  * The version restriction has been relaxed on Rails edge but it hasn't
    landed in a released Rails version yet. See
    rails/rails#51592.
  * can_protect_attributes? was used to support old Rails versions that
    were using the Protected Attributes gem to migrate to strong
    parameters. Protected Attributes hasn't been supported since Rails
    5.0, and since rails-properties only supports 6+ we don't need
    can_protect_attributes? any more.
  * properties was loading all of a target's property objects into
    memory and filtering the collection to only return the var that was
    requested. Although the filtering returns only one property object,
    the others are still loaded in memory and associated with the
    target. This can lead to strange side affects because callers
    aren't expecting the other objects to be loaded in memory.
  * Instead of filtering the property objects in memory, filter them at
    the database level so only the requested objects are loaded in
    memory.
  * To avoid conflicts when writing new property objects, memoize the
    requested property object after it's been initialized. This was done
    implicitly before when all the objects were loaded in memory.
  * Update the queries_spec to reflect the new behavior—each request for
    a target's property objects will result in unique queries because
    filtering is done in the database and not in memory.
@github-advanced-security
Copy link

This pull request sets up GitHub code scanning for this repository. Once the scans have completed and the checks have passed, the analysis results for this pull request branch will appear on this overview. Once you merge this pull request, the 'Security' tab will show more code scanning analysis results (for example, for the default branch). Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results. For more information about GitHub code scanning, check out the documentation.

@alimi alimi changed the title Update properties fetching Filter properties in the database instead of in memory May 30, 2024
else
property_objects.detect { |s| s.var == var.to_s } || property_objects.build(:var => var.to_s, :target => self)
end
cached_properties[var] ||= property_objects.find_or_initialize_by(var: var)
Copy link
Author

@alimi alimi May 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This memoization is being done to support new property objects. If a property object isn't in the database, property_objects.find_or_initialize_by(var: var) returns an new object in memory. Since we can write multiple values to a new property like

account.properties(:portal).premium = true
account.properties(:portal).fee = 42.5

#properties needs to return the same instance of the property object. Otherwise, we'll have two different property objects for the same var (:portal in the above example), and that's not allowed.

This memoization was happening implicitly before when everything was being loading into memory.

@@ -1,3 +1,3 @@
module RailsProperties
VERSION = '3.4.3'
VERSION = '4.0.0'
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like a big enough change to warrant a major version bump.

@@ -51,7 +51,7 @@
user.properties(:dashboard).bar = 'string'
user.properties(:calendar).bar = 'string'
user.save!
}.to perform_queries(3)
}.to perform_queries(4)
Copy link
Author

@alimi alimi May 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The number of queries increases because we're not loading all of the target's property objects into memory anymore. So we have one query to get the user's :dashboard property object and another to get the user's :calendar property object.

@alimi alimi marked this pull request as ready for review May 30, 2024 15:20
Copy link

@jmmastey-chime jmmastey-chime left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it reasonable to add a test that exercises the buggy behavior that we'd seen? Or is that a lot to set up?

@jmmastey-chime jmmastey-chime requested a review from noelrappin May 30, 2024 17:09
@alimi
Copy link
Author

alimi commented May 30, 2024

It'd be a lot of setup because a lot of different factors at play. But the underlying change that will fix the bug is covered by the queries_spec referenced in https://github.com/1debit/rails-properties/pull/5/files/e441b9a25d3d6c5b93de5d08f3c624ccd412d36a#r1620940022.

  * Calling reload on the target is not reseting the associated property
    objects anymore. This is broken because
    RailsProperties::Base#properties is memoizing the ActiveRecord
    property_objects records. This before was working before because
    ActiveRecord was doing the memoiziation for us. This should be fixed
    before merging.
@alimi
Copy link
Author

alimi commented Jun 3, 2024

I just pushed up a failing spec in fe28690. Calling reload on the target is not resetting the associated property objects anymore. This is broken because RailsProperties::Base#properties is memoizing the ActiveRecord property_objects records. This was working before because ActiveRecord was doing the memoiziation for us. This should be fixed before merging; I saw this issue when testing these changes in a Rails app.

@alimi
Copy link
Author

alimi commented Jun 6, 2024

Closing this since we haven't been able to figure out fixing the reload behavior.

@alimi alimi closed this Jun 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants