Topic: Representing multidimensional data
Not sure if this should go in Models or Views section as they're related, but I'll try it here
Okay, this has been driving me nuts for a good while. I've been trying to avoid the problem, every time I come back to it I get stuck. I can't avoid it any longer and I'm just getting frustrated beyond belief simply because I should be able to figure it out but I just can't.
Plainly, I need to represent a 2d space in Rails using ActiveRecord. I want to do it with as many Rails niceties as possible. I'll be doing this a lot for various applications, so I'm looking for a generalized way that could be a candidate for an 'acts_as'. A good example would a chess board. You need to know what, if anything, is in each space and a way to locate that space. Further, I need to be able to search for the data that is in these spaces, i.e. 'Give me all boards where white knight 1 is at c8' There are techniques on representing 2d, 3d and higher spaces in an SQL database, which is essentially a 1d space, but I can't get it to play nice with Rails.
First shot is to just do a standard way of representing something like this in SQL:
class Board
has_many :squares
(columns id, name)class Square
belongs_to :board
(columns id, board_id, x, y, associated data)
This works, and it works fine. With default scoping you can reduce the number of queries to 2. The problem comes from getting and setting data in the way Rails likes for convenience.
My first shot ignored all the Rails convenience, generating the forms and reconstructing the data by hand in the controller, shuttling along JSON objects using jQuery to populate forms. This became hugely messy and felt like I was writing a prettier PHP. There has to be a better way.
Because this is essentially a 2d array it needs to be represented in a 2d fashion, an actual use for a table! However, the accepted way to generate forms with associated data is with fields_for. Rails 2.3 brought a handy view construction with fields_for as such:
form_for @board do |b|
b.fields_for :squares do |s|
#yielded construction
This works fantastic if you had a linear set, such as a list, or a singular association (Person belongs_to :organization). The problem is the yield to fields_for is called for every association. In the case of the chess board, this would give you 64 yields in a linear fashion. What you want to do is to yield 8 (the size of a row of a chessboard) times each to give you a chance to close out the row. I've done this with a weirdly convoluted template logic something like
row = 0
column = 0
<tr>
b.fields_for :squares do |s|
#form construction here
column = column.succ
if column % 8 == 0
row = row.succ
</tr>
next if row % 8 == 0
<tr>
Which to me is hugely chaotic, but it does work for the most part. Something similar could be accomplished by checking the row and column number from the record, but it wouldn't be much better I think.
Another thought I had in mind would be to change the associations deeper like
class Board; has_many :rows
class Row; belongs_to :board; has_many :squares
class Square; belongs_to :row
Which can give you a double level association that you can treat like a 2d array which allows you to keep track of position, and can be accomplished with a self-referencing table (has_many :squares, :class => :row, :foreign_key :row_number), but that may end up being convoluted again.
I've seen a presentation of someone else who is doing something similar, and they got around the problem by making an intermediate class between AR and the data. This seems like something that I shouldn't have to do and would require a lot more code to prop it up to fit in to the Rails structure (or maybe less than I think).