Assignment 3: Standalone ActiveRecord
Extension: Now due before 11:59 PM EST, Wed., Oct. 28
Errata
- If you try to save a game with the same name as an existing game, delete the saved game, and all of its associated Pylons and Players. You do not need to provide a warning or ask the user if this is ok.
- I converted the bit below on validation into a list to make it easier to understand.
- The download is slightly revised (e168-assignment3-jgn-1.0.004.zip), and fixes two bugs. To fix the bugs by hand:
- In game_persister_file.rb, add a $ to the end of each regular expression that identifies Pylons or Players in the file. This is not the greatest fix, but it will do. Thus:
if s =~ /pylon (.+?) '(.)' '(.)' ([\-\d]+?)\s+([\-\d]+?)$/ # . . . . . . elsif s =~ /player (.+?) '(.)' '(.)' ([\-\d]+?)\s+([\-\d]+?)\s+([\-\d]+?)\s+([\-\d]+?)$/
Thanks go to Ron.
- The other problem is more serious. In the Rakefile, remove this line from the package task:
t.package_files.exclude("#{db_dir}migrate/**/*")Thanks go to Gabriel.
- In game_persister_file.rb, add a $ to the end of each regular expression that identifies Pylons or Players in the file. This is not the greatest fix, but it will do. Thus:
The purpose of this assignment is to give you some experience with ActiveRecord. You’ll define migrations for tables support ActiveRecord models, and run the migrations. You’ll be doing all of this “by hand.” When we get to Rails, you’ll see that there are a lot of convenient scripts and shortcuts for generating models and tables, but with AR, my experience has been: no pain, no gain. This is also why the schema for this assignment is very simple. As well, there are no tests. The problem with tests for “declarative” code (which is what much of ActiveRecord is), is that the tests reveal the solution.
There is a Firefox addon that allows for easy perusal of your database: I recommend that you install this addon: https://addons.mozilla.org/en-US/firefox/addon/5817
To do this assignment, download the ZIP (e168-assignment3-jgn-1.0.004.zip), and unzip it. Copy your student_solution.rb (and supporting files, if any) into the lib/world/ directory. Verify that your code works in this new context. We made some small changes which are documented in readme.rdoc. If your code doesn’t work, check it over very carefully; if it still doesn’t work, ZIP up your project for #3 and e-mail it directly to John.
What you are going to do is write a subclass of a new class called GamePersister. To give you the flavor of this, we provide a GamePersisterFile, which saves the state of the GameManager and any Pylons or Players to a text file. This is not the most elegant solution and is not very DRY, because we want the code to be as obvious as possible.
Now, if you’ve been reading the book and have been studying the Assignment 2 code, you may be thinking that we want to have the GameManager and Entity classes be ActiveRecord models. Or you may think that GameManager, Pylon, and Player ought to be models. We’re not going to do this. The reason is that to have Pylon be a model, we would have to subclass ActiveRecord::Base. But we are already committed to a class hierarchy. Therefore, we are going to create new models called SavedGame, SavedPylon, and SavedPlayer. The only reason our models for Pylons and Players have “Saved” in their names is to differentiate them from our existing classes. If you were were starting a project from scratch and knew that you were going to save data to a database, you might very well have subclassed ActiveRecord::Base from the start. [If we use this assignment next year, we'll handle the persistence with modules. But that requires changes to the core classes that we don't want to make at this time.]
Here’s a diagram of the tables/models you must implement:
The “crow’s feet” (http://en.wikipedia.org/wiki/Entity-relationship_diagram#Crow.27s_Foot) in the lines between saved_games and saved_players and between saved_games and saved_pylons indicates which item refers to the other. In this case, the foreign keys are on saved_players and saved_pylons. This means that a SavedPlayer (and a SavedPylon) “belong to” a SavedGame. In reverse, Each SavedGame “has many” SavedPlayers and SavedPylons. As you get into this, you should review carefully the naming, case, and pluralization conventions used in code for ActiveRecord tables, models, and association names (in AWDR, see section 17.1, 18.3, and elsewhere; with regarding to associations, you want to watch for language such as this, in 18.3: “The parent class name is assumed to be the mixed-case singular form of the attribute name, and the foreign key field is the lowercase singular form of the parent class name with _id appended.” What this means is that in the SavedPlayer class, you would write “belongs_to :saved_game”, while in the database, the foreign key is going to be saved_game_id).
Now, because the GameManager manages a Pylon (for instance), but our ActiveRecord class is called a SavedPylon, we need some code that can add a Pylon to a GameManager based on the data in a SavedPylon. We have supplied code that will do this in GamePersister. For example:
# Add a SavedPylon to a GameManager def add_saved_pylon(game_manager, saved_pylon) # Remember that a class name is just a constant. # game_manager.class.const_get(name) means: # Find the constant with the given name, as the GameManager's class # would understand that constant name. So, for instance, if the # name is ShyPlayer, we will get the constant: ShyPlayer # Now we can call .new on the constant (that is, on the class) pylon = game_manager.class.const_get(saved_pylon.class_name).new(game_manager, saved_pylon.x, saved_pylon.y) pylon.identity = saved_pylon.identity pylon.appearance = saved_pylon.appearance end
There is a similar method to add a SavedPlayer, and there are a couple more methods to add collections of SavedPylons or SavedPlayers.
Another aspect of this that is a bit tricky is that when a SavedPylon or SavedPlayer is saved to the database, we include the class name of the Pylon or Player. So, when we get a SavedPylon out of the database, and want to “new” an appropriate Pylon, we have to utilize that saved class name. See the comments in the quoted code above for how we do this. Arguably, someone might use ActiveRecord’s “single table inheritance”feature to make this happen.
So, here’s what you have to do:
- Define migrations in db/migrate to create the saved_games, saved_pylons, and saved_players tables. The attributes must be as given in the diagram above. For the foreign key columns, use the “references” method. In your migrations, you must ensure that none of the column values may be null.
- Define ActiveRecord models for SavedGame, SavedPylon, and SavedPlayer. Please put these in the root of the lib/ directory. They should not be in any module.
- Once you have the models, define the appropriate validations and associations (has_many, belongs_to, etc. You must decide on the associations that make sense with the diagram). For associations and validations:
- when a SavedGame is destroyed, any associated SavedPylons or SavedPlayers should also be destroyed automatically (hint: this requires a modifier on the associations);
- the name of each game must be unique;
- all non-key attributes, except appearance and identity, must not be “empty”;
- the identity and appearance attributes must have length 1;
- all non-key attributes that are supposed to be numbers must be validated for numericality, and must only be integers;
- the tick number of a SavedGame must be greater than or equal to 0.
The reason appearance and identity are excluded from the the requirement that attributes be non-empty is because they can be a single space. The Validators consider a single space to be “empty,” so we have to handle them a bit differently.
Everything up to this point should be pretty easy. We will give a lot of credit for a perfect job on the steps above.
- Implement GamePersisterRdb. This should have the same behavior as GamePersisterFile (except that the data is stored to the database).
- For this assignment, no writeup is required. If you have notes you want to add, you may, though, put them in readme-student.txt
To load and save games, use example_game_persistent.rb. If you add “rdb” to the command line, it will use the rdb persister; otherwise it will use the file persister:
# uses the file persister ruby example_game_persistent.rb # uses the rdb persister ruby example_game_persistent.rb rdb
For the “file” version, we added a sample game (called “sample.game”). When you enter the game name, leave off the “.game” part.
Questions and Answers:
- Q: I added some attributes to my own Pylons and Players, so they don’t get saved or loaded properly.
A: Saving such Pylons and Players is out-of-scope. There is a fairly reasonable strategy to save Pylons and Players with extra attributes: Keep an eye on the lectures for the “serialize” method for ActiveRecord. - Q: It looks like there’s an opportunity to “share” Pylons and Players across games if they have exactly the same attributes. Should we check if a Pylon or Player is already in the database?
A: No. The foreign key “saved_game_id” can only point to own game. To do what you’re talking about would require a “join table” in the middle, so as to support has_many :through. - Q: It seems as though there are opportunities for further validations. For example, I could require that the SavedGame’s x_min is less than its x_max; I could require that all SavedPylons and SavedPlayers are within their game boards.
A: I would advise not doing that. Since the domain code is not too finicky about these details (except that it will cause exceptions to be raised!) it would be premature to lock down such details for the models. - Q: Are there any additional docs (compared to Assignment 2)?
A: No.

Hey John,
yeah thanks, I think I'm ok for now. Though the shell script isn't ideal e.g. it doesn't give me a clean slate in between each test, it is getting me by.
Testing is cool :) I found a bug in my Model and it was related to my usage of validates_presence_of.
I don't understand the difference between SavedGame.new & SavedGame.create.
I call both of them with valid parameters, and only the #create one actually saves to the DB.
but according to slide 44 in lecture 6, new should also...
@Lateral Punk
That's right. "new" just instantiates an instance. To save it to the database, do saved_game.save
.create instantiates and then saves.
Notice how in slide 44, in the block, u.save! is called . . . So the sequence is, as always, .new to instantiate, then .save. If you want to do it all at the same time, use .create
I'm doing this with a game id that does *not* exist in the DB, but all other params are valid:
sp = SavedPlayer.create(:saved_game_id => 121.....
# sp should NOT be valid
assert !sp.valid?
But the assertion is failing. That is, the system is saying sp is valid. How come? I would have thought that Rails checks the DB for the existence of game id 121. I have the appropriate beongs_to, has_many association set correctly. am I to be using validates_associated somehow?
No, Rails does not automatically check that for you. You can try to enforce it at the database level with your own SQL (example: http://jjinux.blogspot.com/2009/08/rails-foreign-key-constraints.html), but sqlite3 doesn't provide foreign key constraints.
There are various gems and plugins to do this kind of thing:
http://blog.hasmanythrough.com/2007/7/14/validate-your-existence
http://github.com/bogdans83/validates_existence_of
http://blog.weiskotten.com/2008/01/you-should-use-foreign-key-constraints.html
@Lateral Punk: If your code can get as far as an SQLException then you are not properly validating.
thanks for all the answers!
New question: the new game id is not saved out correctly if i do it like this:
SavedPlayer.create(:saved_game_id=>new_game
but like this, it works:
SavedPlayer.create(:saved_game_id=>new_game.id
why? All the examples I have seen do it the first way. And I even recall John saying in class that the first way should work.
yeah it's really weird, some times I explicitly need .id and other times I dont'. Is there a rule of thumb for this? it's so funny cause I was working with this game engine once, I had the exact same issue with their objects and usage of .id method. brings back ol' memories :)
My code does this
and it seems to work. What specifically happens (or doesn't happen) when you run your code?
so you are doing it the first way compared to me. I don't think a create vs. block would make a difference??
specifically what happens is that just doing new_game always returns like 1
does the record get saved to the database incorrectly? or not at all?
maybe this is your problem -- you are assigning an ActiveRecord::Base object (saved_game) to an integer attribute (saved_game_id) -- don't do that.
Ahh, thanks Ron!!!
Interesting that this doesn't cause any kind of exception. The joys of using a language without static type checking....
Lateral and Ron: Just as a general tip, you can avoid assigning the SavedPlayer's saved_game_id by starting on the has_many side.
Then you don't have to fool around with id's or assigning "parents" at all, and any qualms about the possibility of assigning the "wrong" type go away.
In AWDR, see p. 366 (print), p. 370 (PDF).
orders.create( . . . )
e.g.,
some_customer.orders.create( . . . )
By doing this, the particular order's customer_id is set automatically.
Thanks. I assume these also take an optional block? (The book doesn't say.)
@Ron Newman
That's a good question -- try it and report back.
OK, I tried it. The block does get called. BUT, if you use .build instead of .create, and you do a save! inside the block, you will get an SQLException as it tries to store a NULL value for the foreign key. You have to either use .create, or do your save! after the block finishes.
@Ron Newman
Good to know about being able to use block syntax.
What you do with .build is leave out the .save! for the specific instance -- and then when the parent instance is re-saved, all the kiddies get saved at the same time. That's what .build is for: To add on a child, but defer saving to the parent.
This may very well be badly documented in AWDR.
In the API docs http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html, here is the key sentence (under "has_many"):
"collection.build(attributes = {}, …)
Returns one or more new objects of the collection type that have been instantiated with attributes and linked to this object through a foreign key, but have not yet been saved. Note: This only works if an associated object already exists, not if it‘s nil!"
What they mean by "associated object" is some object to the "left" of the collection. So it would like . . .
post.comments.build(...)
post.save!
This is a very common pattern. It might not be suitable for SavedGame / SavedPylons, because the presumption is that the SavedGame has already been saved at least once.
(NOTE that were you doing this manually, the sequence of SQL would seem right; it wouldn't seem as though the initial save of SavedGame, just to get .build to work, is redundant.)
@john
Just to reinforce the point, you can do:
a = Author.new(..)
a.posts.build(..)
a.save #saves an author and a post
as well as:
a = Author.find(:first)
a.posts.build(..)
a.save #re-saves author, saves a post
In both cases, the posts that you build on 'a' will be saved (provided everything is valid).
@Keith
Just want to make sure, though: Does it work for sure with an unsaved instance?
a = Author.new(..)
a.posts.build(..)
a.save #saves an author and a post
as opposed to
a = Author.create(..)
a.posts.build(..)
(Trying to understand the doc saying: "Note: This only works if an associated object already exists, not if it‘s nil!")
@john: Yes, that worked for me. (I can e-mail you my code if you're curious.)
@Ron Newman
No need -- but the doc seems really opaque. Any have a clue what "Note: This only works if an associated object already exists, not if it‘s nil!" means?
@john
I'm not sure why they call out nil as a special case, since you'll get a NoMethodError if you try to access the association on nil. When you call save on the parent object, all unsaved children will be saved and the foreign key id will be set in each child.
I'm getting a strange error that I'm having trouble tracking down. I can create and store my saved players but I get the following message when I call SavedPylon.new:
c:/ruby19/lib/ruby/gems/1.9.1/gems/activesupport-2.3.3/lib/active_support/dependencies.rb:105:in `rescue in const_missing': uninitialized constant GamePers
isterRdb::SavedPlyon (NameError)
from c:/ruby19/lib/ruby/gems/1.9.1/gems/activesupport-2.3.3/lib/active_support/dependencies.rb:94:in `const_missing'
from C:/cscie168/assignments/assignment_3/e168-assignment3-jgn-1.0.004/game_persister_rdb.rb:49:in `block in save'
from C:/cscie168/assignments/assignment_3/e168-assignment3-jgn-1.0.004/game_persister_rdb.rb:47:in `each'
from C:/cscie168/assignments/assignment_3/e168-assignment3-jgn-1.0.004/game_persister_rdb.rb:47:in `save'
from example_game_persistent.rb:53:in `'
Any tips?
Thanks,
Gary
Is your saved_pylon.rb file in lib/ ?
Do you have this at the top of your game_persister_rdb.rb :
require File.expand_path(File.join(File.dirname(__FILE__), 'lib', 'world'))
?
@john
I do have it in lib and I do have that line at the top. I just thought that cryptic error message might point to something in particular. I will persevere.
What it means is that SavedPylon is being referred to, but it hasn't seen it already as a result of some "require." Therefore, since it occurs in the class GamePersisterRdb, the class loader (actually ActiveSupport), is saying: Um, this is a class local to GamePersisterRdb. So, somehow, it's not seeing your SavedPylon from the saved_pylon.rb file.
(At least I think that's what's going on.)
Are you sure that you also have this line at the top of your game_persister_rdb.rb ? (It was in the original file provided by the assignment, and you shouldn't change it):
require File.expand_path(File.join(File.dirname(__FILE__), 'rdb_setup'))
rdb_setup.rb is the file that includes lib/saved_pylon.rb ,which is where your "class SavedPylon" should be defined.
@Gary
SavedPlyon is your problem. "uninitialized constant GamePersisterRdb::SavedPlyon (NameError)" -- you've got a mispelling.
@Keith
Oh sheesh, should have caught that one . . . Helped another student with a misspelling issue in e-mail, too.
@john
Arrghh! Thanks!
Reading the code a bit, suppose I do this
p = SavedPlayer.new
p.x = "1.3"
p.x returns 1 but
p.read_attribute_before_type_cast("x")
returns "1.3". I suppose a fussier validator might compare p.read_attribute("x") with p.read_attribute_before_type_cast("x") and become disturbed at mismatches.
A style question.
In adapter-like code, one might imagine strings of assignments like
newFont.style = oldFont.style
newFont.variant = oldFont.variant
newFont.weight = oldFont.weight
newFont.size = oldFont.size
In Ruby, there's a tendency to use parallel assignment:
newFont.style, newFont.variant, newFont.weight, newFont.size = oldFont.style, oldFont.variant, oldFont.weight, oldFont.size
This is more compact but, if the "old" list reversed weight and size, it might take me a while to find it.
On the other hand, I can imagine working off a list of symbols [:style, :variant, :weight, :size] and using __send__ or some other reflection trick so that I have only one assignment but a complicated one sitting in a block or loop. It seems to me that that would only be clear and easy to read if it were a Ruby idiom. Otherwise, the first way of doing it seems clearer and safer.
How do real Rubyists do this?
Is a font just a collection of attributes, or does it have other methods?
If there are only attributes, it might as well just be a Hash.
Then you just get to use Hash keys.
@john
I'm thinking about the assignment, actually, where I can get a hash out of one side (via ActiveRecord::Base#attributes) but I cannot one out of the other.
OK.
So are you trying to create a game-style Pylon from a SavedPylon, or the other way around?
You must mean the latter: Creating a game pylon, such as, say, MyPylon, from a SavedPylon ? (easily.) For the assignment, I would just do direct assignment, but I did show some code in lecture where the "initialize" method on a class was set to take a Hash. Then you could do something like:
MyPylon.new(some_saved_pylon.attributes)
Hmm, if Ruby has a
move_corresponding(from, to, attr_list)
method (or you write one), then you could do this:
move_corresponding(pylon, saved_pylon, [:x, :y, :identity, :appearance])
@Ron Newman
Sure.
I would bet that it's very natural, though, for the constructor.
The method name "merge" might be suggestive (since that is used for Hashes).
For this assignment, you should not need to write code to create a Pylon from a SavedPylon (or a Player from a SavedPlayer), because that's already written for you in game_persister.rb . See the methods add_saved_pylon, add_saved_player, set_pylons, and set_players.
In case anyone else ran into this, the SQLite Manager addon for Firefox will not work if the Delicious bookmark addon is also installed.
The workaround is to open a new tab and type "chrome://sqlitemanager/content/sqlitemanager.xul" in the address bar.
More info at http://code.google.com/p/sqlite-manager/issues/detail?id=325
@Scott Aldort
Wow, that stinks.
For what it's worth, I really prefer the old-skool bookmarklet:
javascript:location.href='http://delicious.com/post?v=4;url='+encodeURIComponent(location.href)+';title='+encodeURIComponent(document.title)
You just stick that into a bookmark.
For a desktop app, Mac, Linux and Windows, Ive had good luck with SQLite Browser
http://sqlitebrowser.sourceforge.net/
-peter
@Scott Aldort
I should qualify my post by saying that in fact, the SQLite Manager FF add-on *does* work using the workaround; the problem is that it doesn't launch properly. (which initially gave me the impression that it didn't work)
I should also add that I tagged the workaround URL using the Delicious addon, and then clicked the bookmark, and SQLite Manager launched just fine. Now there's a bit of irony. :-)
@peter
Thanks for the link.