README.md in positioning-0.1.7 vs README.md in positioning-0.2.0

- old
+ new

@@ -37,15 +37,15 @@ ### Declaring Positioning To declare that your model should keep track of the position of its records you can use the `positioned` method. Here are some examples: ```ruby -# The scope is global (all records will belong to the same list) and the databse column +# The scope is global (all records will belong to the same list) and the database column # is 'positioned' positioned -# The scope is on the belongs_to relationship 'list' and the databse column is 'positioned' +# The scope is on the belongs_to relationship 'list' and the database column is 'positioned' # We check if the scope is a belongs_to relationship and use its declared foreign_key as # the scope value. In this case it would be 'list_id' since we haven't overridden the # default foreign key. belongs_to :list positioned on: :list @@ -149,25 +149,25 @@ # or other_item.id # => 11 item.update position: {after: 11} ``` -##### Relative Positining in Forms +##### Relative Positioning in Forms -It can be tricky to provide the hash forms of relative positining using Rails form helpers, but it is possible. We've declared a special `Struct` for you to use for this purpose. +It can be tricky to provide the hash forms of relative positioning using Rails form helpers, but it is possible. We've declared a special `Struct` for you to use for this purpose. -Firstly you need to allow nested Strong Parameters for the `position` column like so: +Firstly you need to allow both scalar and nested Strong Parameters for the `position` column like so: ```ruby def item_params - params.require(:item).permit :name, position: [:before] + params.require(:item).permit :name, :position, { position: :before } end ``` In the example above we're always declaring what item (by its `id`) we want to position our item **before**. You could change this to `:after` if you'd rather. -Next, in your `new` method you may wish to intialise the `position` column with a value supplied by incoming parameters: +Next, in your `new` method you may wish to initialise the `position` column with a value supplied by incoming parameters: ```ruby def new item.position = { before: params[:before] } end @@ -175,11 +175,11 @@ You can now just pass the `before` parameter (the `id` of the item you want to add this record before) via the URL to the `new` action. For example: `items/new?before=22`. In the form itself, so that your intended position survives a failed `create` attempt and form redisplay you can declare the `position` value like so: -```erb +``` <% if item.new_record? %> <%= form.fields :position, model: Positioning::RelativePosition.new(item.position_before_type_cast) do |fields| %> <%= fields.hidden_field :before %> <% end %> <% end %> @@ -223,16 +223,34 @@ item.update list: other_list, position: {after: 11} ``` It's important to note that in the examples above, `other_item` must already belong to the `other_list` scope. +## Concurrency + +The queries that this gem runs, especially those that seek the next position integer available are vulnerable to race conditions. To this end, we've introduced an Advisory Lock to ensure that our model callbacks that determine and assign positions run sequentially. In short, an advisory lock prevents more than one process from running a particular block of code at the same time. The lock occurs in the database (or in the case of SQLite, on the filesystem), so as long as all of your processes are using the same database, the lock will prevent multiple positioning callbacks from executing on the same table and positioning column combination at the same time. + +If you are using SQLite, you'll want to add the following line to your database.yml file in order to increase the exclusivity of Active Record's default write transactions: + +```yaml +default_transaction_mode: EXCLUSIVE +``` + +You may also want to try `IMMEDIATE` as a less aggressive alternative. + +You're encouraged to review the Advisory Lock code to ensure it fits with your environment: + +https://github.com/brendon/positioning/blob/main/lib/positioning/advisory_lock.rb + +If you have any concerns or improvements please file a GitHub issue. + ## Development After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. This gem is tested against SQLite, PostgreSQL and MySQL. The default database for testing is MySQL. You can target other databases by prepending the environment variable `DB=sqlite` or `DB=postgresql` before `rake test`. For example: `DB=sqlite rake test`. -The PostgreSQL and MySQL environments are configured under `test/support/database.yml`. You can edit this file, or preferrably adjust your environment to support passwordless socket based connections to these two database engines. You'll also need to manually create a database named `positioning_test` in each. +The PostgreSQL and MySQL environments are configured under `test/support/database.yml`. You can edit this file, or preferably adjust your environment to support password-less socket based connections to these two database engines. You'll also need to manually create a database named `positioning_test` in each. To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). ## Contributing