[ANN] Active Record 0.8.2: Inheritable callback ques

  • Thread starter David Heinemeier Hansson
  • Start date
D

David Heinemeier Hansson

What's new in Active Record 0.8.2?
==================================

Besides the overwriteable callback methods, it's now also possible to
register callbacks through the use of the callback macros. Their main
advantage is that the macros add behavior into a callback que that is
kept intact down through an inheritance hierarchy. Example:

class Topic < ActiveRecord::Base
before_destroy :destroy_author
end

class Reply < Topic
before_destroy :destroy_readers
end

Now, when Topic#destroy is run only destroy_author is called. When
Reply#destroy is run both destroy_author and destroy_readers is called.
Contrast this to the situation where we've implemented the save
behavior through overwriteable methods:

class Topic < ActiveRecord::Base
def before_destroy() destroy_author end
end

class Reply < Topic
def before_destroy() destroy_readers end
end

In that case, Reply#destroy would only run destroy_readers and _not_
destroy_author. So use the callback macros when you want to ensure that
a certain callback is called for the entire hierarchy and the regular
overwriteable methods when you want to leave it up to each descendent
to decide whether they want to call super and trigger the inherited
callbacks.

Additionally, these new callback macros will accept method fragments,
which will be evaluated with the binding of the callback spot.

All this might seem pretty hard to understand why it's really cool.
That's until you see the first application of it:

* Added :dependent option to has_many and has_one, which will
automatically destroy associated objects when the holder is destroyed:

class Album < ActiveRecord::Base
has_many :tracks, :dependent => true
end

All the associated tracks are destroyed when the album is.

Pretty neat, hu?

Read more about inheritable callback ques:
http://ar.rubyonrails.org/classes/ActiveRecord/Callbacks.html

Read more about dependent option for has_many/one:
http://ar.rubyonrails.org/classes/ActiveRecord/Associations/
ClassMethods.html#M000005


Also in 0.8.2:

* Added Base.create as a factory that'll create, save, and return a new
object in
one step.

* Automatically convert strings in config hashes to symbols for the
_connection methods.
This allows you to pass the argument hashes directly from yaml. (Luke)

* Fixed the install.rb to include simple.rb [Spotted by Kevin Bullock]

* Modified block syntax to better follow our code standards outlined in
http://rails.rubyonrails.org/show/CodingStandards

Get the release and read more at http://activerecord.rubyonrails.org/


Hang out with the Ruby on Rails crowd
=====================================

Come by the IRC channel #rubyonrails on Freenode. Design decisions are
aired here and you'll be able to ask questions about Active Record and
the framework in general. Oh, and we're really friendly too!


Call for help!
==============

Do you have working knowledge with and access to either Oracle, ODBC,
Sybase, or DB2, I'd be really grateful if you would consider writing an
adapter for Active Record. Adapters are usually just around 100 lines
of code. You'll have three examples to look at, a well-specified
interface[1], and almost 100 test cases to make it real easy. Luke
Holden reports that he spent just a few hours getting SQLite and
PostgreSQL adapters working.

[1]
http://ar.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/
AbstractAdapter.html


Active Record -- Object-relation mapping put on rails
=====================================================

Active Record connects business objects and database tables to create a
persistable
domain model where logic and data is presented in one wrapping. It's an
implementation of the object-relational mapping (ORM) pattern by the
same name as described by Martin Fowler:

"An object that wraps a row in a database table or view, encapsulates
the database access, and adds domain logic on that data."

Active Records main contribution to the pattern is to relieve the
original of two stunting problems: lack of associations and
inheritance. By adding a simple domain language-like set of macros to
describe the former and integrating the Single Table Inheritance
pattern for the latter, Active Record narrows the gap of functionality
between the data mapper and active record approach.

A short rundown of the major features:

* Automated mapping between classes and tables, attributes and columns.
class Product < ActiveRecord::Base; end

...is automatically mapped to the table named "products", such as:

CREATE TABLE products (
id int(11) NOT NULL auto_increment,
name varchar(255),
PRIMARY KEY (id)
);

...which again gives Product#name and Product#name=(new_name)


* Associations between objects controlled by simple meta-programming
macros.
class Firm < ActiveRecord::Base
has_many :clients
has_one :account
belong_to :conglomorate
end


* Aggregations of value objects controlled by simple meta-programming
macros.
class Account < ActiveRecord::Base
composed_of :balance, :class_name => "Money",
:mapping => %w(balance amount)
composed_of :address,
:mapping => [%w(address_street street),
%w(address_city city)]
end


* Validation rules that can differ for new or existing objects.
class Post < ActiveRecord::Base
def validate # validates on both creates and updates
errors.add_on_empty "title"
end

def validate_on_update
errors.add_on_empty "password"
end
end


* Callbacks as methods or ques on the entire lifecycle
(instantiation, saving, destroying, validating, etc).

class Person < ActiveRecord::Base
def before_destroy # is called just before Person#destroy
CreditCard.find(credit_card_id).destroy
end
end

class Account < ActiveRecord::Base
after_find :eager_load, 'self.class.announce(#{id})'
end

Learn more in link:classes/ActiveRecord/Callbacks.html


* Observers for the entire lifecycle
class CommentObserver < ActiveRecord::Observer
def after_create(comment) # is called just after Comment#save
NotificationService.send_email("(e-mail address removed)", comment)
end
end


* Inheritance hierarchies
class Company < ActiveRecord::Base; end
class Firm < Company; end
class Client < Company; end
class PriorityClient < Client; end


* Transaction support on both a database and object level. The latter
is implemented
by using Transaction::Simple

# Just database transaction
Account.transaction do
david.withdrawal(100)
mary.deposit(100)
end

# Database and object transaction
Account.transaction(david, mary) do
david.withdrawal(100)
mary.deposit(100)
end


* Direct manipulation (instead of service invocation)

So instead of (Hibernate example):

long pkId = 1234;
DomesticCat pk = (DomesticCat) sess.load( Cat.class, new
Long(pkId) );
// something interesting involving a cat...
sess.save(cat);
sess.flush(); // force the SQL INSERT

Active Record lets you:

pkId = 1234
cat = Cat.find(pkId)
# something even more interesting involving a the same cat...
cat.save


* Database abstraction through simple adapters (~100 lines) with a
shared connector

ActiveRecord::Base.establish_connection:)adapter => "sqlite",
:dbfile => "dbfile")

ActiveRecord::Base.establish_connection(
:adapter => "mysql",
:host => "localhost",
:username => "me",
:password => "secret",
:database => "activerecord"
)


* Logging support for Log4r and Logger

ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.logger = Log4r::Logger.new("Application Log")


Philosophy
==========

Active Record attempts to provide a coherent wrapping for the
inconvenience that is object-relational mapping. The prime directive
for this mapping has been to minimize the amount of code needed to
built a real-world domain model. This is made possible by relying on a
number of conventions that make it easy for Active Record to infer
complex relations and structures from a minimal amount of explicit
direction.

Convention over Configuration:
* No XML-files!
* Lots of reflection and run-time extension
* Magic is not inherently a bad word

Admit the Database:
* Lets you drop down to SQL for odd cases and performance
* Doesn't attempt to duplicate or replace data definitions
 
R

Raphael Bauduin

David said:
What's new in Active Record 0.8.2?
==================================

[snip]

Hi,

I just tested it and it seems really easy to link Classes to tables. However, I encountered
a blocking problem: it seems the column that's the primary key has to be named id. From the code
in the docs (http://ar.rubyonrails.org/classes/ActiveRecord/Base.html#M000057) it seems this is hardcoded.
Is it planned to have the name of the "id column" extracted from the database, or at least to let the user specify a name. I have none of my columns name id.... :)

I hope there's a way to do what I describe, as Active Record seems to be really cool.

Raph
 
C

Carl Youngblood

David said:
All this might seem pretty hard to understand why it's really cool.
That's until you see the first application of it:

* Added :dependent option to has_many and has_one, which will
automatically destroy associated objects when the holder is destroyed:

class Album < ActiveRecord::Base
has_many :tracks, :dependent => true
end

All the associated tracks are destroyed when the album is.

Pretty neat, hu?

This all does look pretty cool, because it has the potential to make
application development truly rapid. I do have some concerns about
performance though. How does this approach compare with an ON DELETE
CASCADE, for example? It seems like if you kept more of the application
logic in the database things would run much faster. On the other hand,
this approach also has some definite drawbacks.

Carl
 
C

Carl Youngblood

Raphael said:
I just tested it and it seems really easy to link Classes to tables.
However, I encountered
a blocking problem: it seems the column that's the primary key has to
be named id. From the code
in the docs
(http://ar.rubyonrails.org/classes/ActiveRecord/Base.html#M000057) it
seems this is hardcoded.
Is it planned to have the name of the "id column" extracted from the
database, or at least to let the user specify a name. I have none of
my columns name id.... :)

I usually prefer to name my primary ID columns <tablename>id just to
avoid confusion. For example, the primary key of the user table is
called userid. On many-table JOINS it gets confusing to have a lot of
IDs floating around. So I agree that being able to change the id column
to something else would be handy. Another thing that some people might
be confused about is when you have a table whose primary key consists of
multiple columns that are combinatorially unique. In these cases I
think the solution is to add an integer primary key and use it for the
ID column, and then make a unique index across the fields that you wish
to be combinatorially unique.

Carl
 
C

Carl Youngblood

David said:
All this might seem pretty hard to understand why it's really cool.
That's until you see the first application of it:

* Added :dependent option to has_many and has_one, which will
automatically destroy associated objects when the holder is destroyed:

class Album < ActiveRecord::Base
has_many :tracks, :dependent => true
end

All the associated tracks are destroyed when the album is.

One other question: Are these operations atomic? Does a transaction get
started before the cascading deletes occur and end when they are
finished? Because otherwise you could have orphaned rows if the server
gets hosed while you are still in the process of deleting something's
children.
 
K

Kirk Haines

On Sat, 5 Jun 2004 00:41:55 +0900, Carl Youngblood wrote
I usually prefer to name my primary ID columns <tablename>id just to
avoid confusion. For example, the primary key of the user table is
called userid. On many-table JOINS it gets confusing to have a lot
of IDs floating around. So I agree that being able to change the id
column to something else would be handy. Another thing that some
people might be confused about is when you have a table whose
primary key consists of multiple columns that are combinatorially
unique. In these cases I think the solution is to add an integer
primary key and use it for the ID column, and then make a unique
index across the fields that you wish to be combinatorially unique.

I still haven't delved into ActiveRecord's code as much as I want to, but in
theory these things should be supportable.

Kansas, which is similar to ActiveRecord but takes a few different
approaches, allows things like this:

create table jukebox (
id int unsigned primary key,
serial_number int unsigned unique,
blah blah blah)

create table albums (
id int unsigned primary key,
jukebox_serial int,
blah blah blah)

create table tracks (
id int unsigned primary key,
album_id int,
blah blah blah)

class Jukebox
to_many:)albums, :Albums, :jukebox_serial, :serial_number)
end

class Albums
to_many:)tracks, :Tracks, :album_id)
belongs_to:)jukebox, :Jukebox, :serial_number, :jukebox_serial)
end

class Tracks
belongs_to:)album, :Albums, :album_id)
end


So, a Jukebox has many Albums. However, instead of using Jukebox's primary
key, serial_number on the Jukebox maps to jukebox_serial, which is simply a
unique key, on the Album.

Albums have many tracks, and are correlated through the primary key on Album
and the album_id field on Track. An Album also belongs to a Jukebox, and is
correlated via the jukebox_serial field on the Album and the serial_number
field on the Jukebox.

Tracks belong to an Album, and are correlated via the album_id field of the
Track to the primary key of the Album.

I don't yet have complete support for multiple field primary keys, but
that's not far off. There is a release with some added documentation
explaining the stuff above, among other things, coming hopefully by the end
of the day.


Kirk Haines
 
A

Aredridel

--=-gY1zHbz8GZv34OhNggLD
Content-Type: text/plain
Content-Transfer-Encoding: 7bit

Raphael said:
I just tested it and it seems really easy to link Classes to tables.
However, I encountered
a blocking problem: it seems the column that's the primary key has to
be named id. From the code
in the docs
(http://ar.rubyonrails.org/classes/ActiveRecord/Base.html#M000057) it
seems this is hardcoded.
Is it planned to have the name of the "id column" extracted from the
database, or at least to let the user specify a name. I have none of
my columns name id.... :)

Attached is a patch that adds a primary key class method, and alters the
code to use it:

class User < ActiveRecord::Base
def self.primary_key
"userid"
end
end

User.find("jdoe")

It removes support for automatically calling to_i on key parameters, and
instead passes them to the database as strings, so it may interpret as
it sees fit. Also added is a little extra debugging for the internal
calls to eval.

Ari

--=-gY1zHbz8GZv34OhNggLD
Content-Disposition: attachment; filename=ar-pkey.patch
Content-Type: text/x-patch; name=ar-pkey.patch; charset=utf-8
Content-Transfer-Encoding: 7bit

? .svn
? Rakefile
? debug.log
? doc
? dev-utils/.svn
? diagrams/.svn
? diagrams/activeRecordSketch.gif
? examples/.svn
? lib/.svn
? lib/active_record/.svn
? lib/active_record/connection_adapters/.svn
? lib/active_record/support/.svn
? lib/active_record/value_objects/.svn
? lib/active_record/vendor/.svn
? test/.svn
? test/debug.log
? test/connections/.svn
? test/connections/dbi_mysql/.svn
? test/connections/mysql_ruby/.svn
? test/connections/native_mysql/.svn
? test/connections/native_postgresql/.svn
? test/connections/native_sqlite/.svn
? test/connections/ruby_mysql/.svn
? test/fixtures/.svn
? test/fixtures/accounts/.svn
? test/fixtures/companies/.svn
? test/fixtures/customers/.svn
? test/fixtures/db_definitions/.svn
? test/fixtures/developer_project/.svn
? test/fixtures/developers/.svn
? test/fixtures/developers_projects/.svn
? test/fixtures/projects/.svn
? test/fixtures/topics/.svn
Index: dev-utils/eval_debugger.rb
===================================================================
RCS file: /var/cvs/activerecord/activerecord/dev-utils/eval_debugger.rb,v
retrieving revision 1.4
diff -u -r1.4 eval_debugger.rb
--- dev-utils/eval_debugger.rb 30 May 2004 15:19:55 -0000 1.4
+++ dev-utils/eval_debugger.rb 4 Jun 2004 17:30:04 -0000
@@ -3,7 +3,7 @@
class Module
alias :eek:ld_module_eval :module_eval
def module_eval(*args, &block)
- $stderr.puts("\n#{self.name} is 'module_eval'ing:\n#{args[0]}") if args[0]
+ puts("in #{self.name}, #{if args[1] then "file #{args[1]}" end} #{if args[2] then "on line #{args[2]}" end}:\n#{args[0]}") if args[0]
old_module_eval(*args, &block)
end
end
Index: lib/active_record/associations.rb
===================================================================
RCS file: /var/cvs/activerecord/activerecord/lib/active_record/associations.rb,v
retrieving revision 1.28
diff -u -r1.28 associations.rb
--- lib/active_record/associations.rb 3 Jun 2004 09:43:45 -0000 1.28
+++ lib/active_record/associations.rb 4 Jun 2004 17:30:04 -0000
@@ -97,14 +97,14 @@
collection_finder = "#{collection_class_name}.find_by_sql(\"#{options[:finder_sql]}\")"
collection_counter = "#{collection_class_name}.count_by_sql(\"#{counter_sql}\")"
else
- collection_finder = <<-end_eval
+ collection_finder = <<-"end_eval"
#{collection_class_name}.find_all(
- "#{class_primary_key_name} = \#{id}#{options[:conditions] ? " AND " + options[:conditions] : ""}",
+ "#{class_primary_key_name} = '\#{id}'#{options[:conditions] ? " AND " + options[:conditions] : ""}",
#{options[:eek:rder] ? "\"" + options[:eek:rder] + "\"" : "nil" }
)
end_eval

- collection_counter = "#{collection_class_name}.count(\"#{class_primary_key_name} = \#{id}\")"
+ collection_counter = "#{collection_class_name}.count(\"#{class_primary_key_name} = '\#{id}'\")"
end

has_collection_method(collection_name)
@@ -203,12 +203,12 @@
association_name, association_class_name, class_primary_key_name =
associate_identification(association_id, options[:class_name], options[:foreign_key])

- association_class_primary_key_name = options[:foreign_key] || association_class_name.downcase + "_id"
-
+ association_class_primary_key_name = options[:foreign_key] || association_class_name.gsub(/^.*::/, '').downcase! + "_id"
+
if options[:remote]
association_finder = <<-"end_eval"
#{association_class_name}.find_first(
- "#{class_primary_key_name} = \#{id}#{options[:conditions] ? " AND " + options[:conditions] : ""}",
+ "#{class_primary_key_name} = '\#{id}'#{options[:conditions] ? " AND " + options[:conditions] : ""}",
#{options[:eek:rder] ? "\"" + options[:eek:rder] + "\"" : "nil" }
)
end_eval
@@ -275,7 +275,7 @@
associate_identification(association_id, options[:class_name], options[:foreign_key])


- association_foreign_key = options[:foreign_key] || association_class_name.downcase + "_id"
+ association_foreign_key = options[:foreign_key] || association_class_name.gsub(/^.*::/, '').downcase + "_id"

association_table_name = options[:table_name] || table_name(association_class_name)
my_key = options[:key] || name.downcase + "_id"
@@ -322,11 +322,11 @@
end

def associate_identification(association_id, association_class_name, foreign_key)
- return association_id.id2name, (association_class_name || class_name(table_name_prefix + association_id.id2name + table_name_suffix)), (foreign_key || name.downcase + "_id")
+ return association_id.id2name, (association_class_name || class_name(table_name_prefix + association_id.id2name + table_name_suffix)), (foreign_key || name.gsub(/.*::/, '').downcase + "_id")
end

def collection_reader_method(collection_name, collection_finder)
- module_eval <<-end_eval
+ module_eval <<-"end_eval", __FILE__, __LINE__
def #{collection_name}(force_reload = false)
if @#{collection_name}.nil? || force_reload
@#{collection_name} = #{collection_finder}
@@ -338,7 +338,7 @@
end

def has_collection_method(collection_name)
- module_eval <<-end_eval
+ module_eval <<-"end_eval", __FILE__, __LINE__
def has_#{collection_name}?(force_reload = false)
#{collection_name}(force_reload).length > 0
end
@@ -346,7 +346,7 @@
end

def collection_count_method(collection_name, collection_counter)
- module_eval <<-end_eval
+ module_eval <<-"end_eval", __FILE__, __LINE__
def #{collection_name}_count(force_reload = false)
if @#{collection_name}.nil? || force_reload
#{collection_counter}
@@ -358,7 +358,7 @@
end

def association_reader_method(association_name, association_finder)
- module_eval <<-end_eval
+ module_eval <<-"end_eval", __FILE__, __LINE__
def #{association_name}(force_reload = false)
if @#{association_name}.nil? || force_reload
@#{association_name} = #{association_finder}
@@ -370,7 +370,7 @@
end

def association_comparison_method(association_name, association_class_name)
- module_eval <<-end_eval
+ module_eval <<-"end_eval", __FILE__, __LINE__
def #{association_name}?(comparison_object, force_reload = false)
if comparison_object.kind_of?(#{association_class_name})
begin
@@ -386,7 +386,7 @@
end

def has_association_method(association_name)
- module_eval <<-end_eval
+ module_eval <<-"end_eval", __FILE__, __LINE__
def has_#{association_name}?(force_reload = false)
begin
!#{association_name}(force_reload).nil?
@@ -398,7 +398,7 @@
end

def build_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
- module_eval <<-end_eval
+ module_eval <<-"end_eval", __FILE__, __LINE__
def #{method_prefix + collection_name}(attributes = {})
association = #{collection_class_name}.new
association.attributes = attributes.merge({ "#{class_primary_key_name}" => id})
@@ -408,7 +408,7 @@
end

def create_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
- module_eval <<-end_eval
+ module_eval <<-"end_eval", __FILE__, __LINE__
def #{method_prefix + collection_name}(attributes = {})
association = #{collection_class_name}.new
association.attributes = attributes.merge({ "#{class_primary_key_name}" => id})
@@ -419,17 +419,17 @@
end

def find_in_collection_method(collection_name, collection_class_name, class_primary_key_name, conditions = nil)
- module_eval <<-end_eval
+ module_eval <<-"end_eval", __FILE__, __LINE__
def find_in_#{collection_name}(association_id)
- #{collection_class_name}.find_on_conditions(
- association_id, "#{class_primary_key_name} = \#{id}#{conditions ? " AND " + conditions : ""}"
+ #{collection_class_name}.find_with_constraint(
+ association_id, "#{class_primary_key_name} = '\#{id}'#{conditions ? " AND " + conditions : ""}"
)
end
end_eval
end

def add_association_relation(association_name, insert_sql)
- module_eval <<-end_eval
+ module_eval <<-"end_eval", __FILE__, __LINE__
def add_#{association_name}(*items)
items.flatten.each { |item| connection.insert "#{insert_sql}" }
@#{association_name} = nil
@@ -438,7 +438,7 @@
end

def remove_association_relation(association_name, foreign_key, delete_sql)
- module_eval <<-end_eval
+ module_eval <<-"end_eval", __FILE__, __LINE__
def remove_#{association_name}(*items)
if items.flatten.length < 1
connection.delete "#{delete_sql}"
Index: lib/active_record/base.rb
===================================================================
RCS file: /var/cvs/activerecord/activerecord/lib/active_record/base.rb,v
retrieving revision 1.30
diff -u -r1.30 base.rb
--- lib/active_record/base.rb 3 Jun 2004 16:50:21 -0000 1.30
+++ lib/active_record/base.rb 4 Jun 2004 17:30:05 -0000
@@ -98,8 +98,8 @@
ids = [ ids ].flatten

if ids.length > 1
- ids_list = ids.map{ |id| id.to_i }.join(", ")
- objects = find_all("id IN (#{ids_list})", "id")
+ ids_list = ids.map{ |id| "'#{id}'" }.join(", ")
+ objects = find_all("#{primary_key} IN (#{ids_list})", "id")

if objects.length == ids.length
return objects
@@ -108,7 +108,7 @@
end
else
id = ids.first
- sql = "SELECT * FROM #{table_name} WHERE id = #{id.to_i} "
+ sql = "SELECT * FROM #{table_name} WHERE #{primary_key} = '#{id}'"
sql << "AND type = '#{name}'" unless descents_from_active_record?

if record = connection.select_one(sql, "#{name} Find")
@@ -124,8 +124,8 @@
# Example:
# Person.find_on_conditions 5, "first_name LIKE '%dav%' AND last_name = 'heinemeier'"
def find_on_conditions(id, conditions)
- find_first("id = #{id.to_i} AND #{sanitize_conditions(conditions)}") ||
- raise(RecordNotFound, "Couldn't find #{name} with ID = #{id} on the condition of #{conditions}")
+ find_first("#{primary_key} = '#{id}'} AND #{sanitize_conditions(conditions)}") ||
+ raise(RecordNotFound, "Couldn't find #{name} with #{primary_key} = #{id} on the condition of #{conditions}")
end

# Returns an array of all the objects that could be instantiated from the associated
@@ -133,8 +133,9 @@
# such as by "color = 'red'", and arrangement of the selection can be done through +orderings+ (ORDER BY-part),
# such as by "last_name, first_name DESC". A maximum of returned objects can be specified in +limit+. Example:
# Project.find_all "category = 'accounts'", "last_accessed DESC", 15
- def find_all(conditions = nil, orderings = nil, limit = nil)
+ def find_all(conditions = nil, orderings = nil, limit = nil, joins = nil)
sql = "SELECT * FROM #{table_name} "
+ sql << "#{joins} " if joins
add_conditions!(sql, conditions)
sql << "ORDER BY #{orderings} " unless orderings.nil?
sql << "LIMIT #{limit} " unless limit.nil?
@@ -286,12 +287,18 @@
table_name_prefix + undecorated_table_name(class_name) + table_name_suffix
end

+ # Defines the primary key field -- can be overridden in subclasses.
+ def primary_key
+ "id"
+ end
+
# Turns the +table_name+ back into a class name following the reverse rules of +table_name+.
def class_name(table_name) # :nodoc:
# remove any prefix and/or suffix from the table name
class_name = table_name[table_name_prefix.length..-(table_name_suffix.length + 1)]

class_name = class_name.capitalize.gsub(/_(.)/) { |s| $1.capitalize }
+ class_name.gsub!(/.*::/, '')

if pluralize_table_names
if class_name[-3,3] == "ies"
@@ -342,7 +349,7 @@

# Guesses the table name, but does not decorate it with prefix and suffix information.
def undecorated_table_name(class_name = class_name_of_active_record_descendant(self))
- table_name = class_name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
+ table_name = class_name.gsub(/.*::/, '').gsub(/([a-z])([A-Z])/, '\1_\2').downcase

if pluralize_table_names
case table_name[-1,1]
@@ -393,11 +400,12 @@
# Every Active Record class must use "id" as their primary ID. This getter overwrites the native
# id method, which isn't being used in this context.
def id
- @attributes["id"]
+ @attributes[self.class.primary_key]
end

- def id=(value) # :nodoc:
- @attributes["id"] = value.to_i
+ # Sets the primary ID.
+ def id=(value)
+ @attributes[self.class.primary_key] = value
end

# Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet.
@@ -415,7 +423,7 @@
# Deletes the record in the database and freezes this instance to reflect that no changes should
# be made (since they can't be persisted).
def destroy
- connection.delete("DELETE FROM #{self.class.table_name} WHERE id = #{id}", "#{self.class.name} Destroy") unless new_record?
+ connection.delete("DELETE FROM #{self.class.table_name} WHERE #{self.class.primary_key} = '#{id}'", "#{self.class.name} Destroy") unless new_record?
freeze
end

@@ -457,7 +465,7 @@
connection.update(
"UPDATE #{self.class.table_name} " +
"SET #{comma_pair_list(attributes_with_quotes)} " +
- "WHERE id = #{id}",
+ "WHERE #{self.class.primary_key} = '#{id}'",
"#{self.class.name} Update"
)
end
@@ -577,4 +585,4 @@
hash.inject([]) { |list, pair| list << "#{pair.first} = #{pair.last}" }.join(", ")
end
end
-end
\ No newline at end of file
+end
Index: test/associations_test.rb
===================================================================
RCS file: /var/cvs/activerecord/activerecord/test/associations_test.rb,v
retrieving revision 1.16
diff -u -r1.16 associations_test.rb
--- test/associations_test.rb 3 Jun 2004 09:42:12 -0000 1.16
+++ test/associations_test.rb 4 Jun 2004 17:30:05 -0000
@@ -212,11 +212,12 @@
aridridel.add_projects([ Project.find(1), Project.find(2) ])
assert_equal 2, aridridel.projects_count
end
-
- def xtest_module_spanning_associations
+
+ def test_module_spanning_associations
assert MyApplication::Business::Firm.find_first.has_clients?
firm = MyApplication::Business::Firm.find_first
+ assert_nil firm.class.table_name.match('::')
assert_equal 2, MyApplication::Business::firm.clients_count
assert MyApplication::Billing::Account.find(1).has_firm?, "37signals account should be able to backtrack"
end
-end
\ No newline at end of file
+end

--=-gY1zHbz8GZv34OhNggLD--
 
D

David Heinemeier Hansson

One other question: Are these operations atomic? Does a transaction
get started before the cascading deletes occur and end when they are
finished? Because otherwise you could have orphaned rows if the
server gets hosed while you are still in the process of deleting
something's children.

They will be in 0.8.3 ;). It's in the CVS right now. I'll probably
follow the pattern of late and release 0.8.3 version tomorrow. Lots of
good new stuff in it.
--
David Heinemeier Hansson,
http://www.instiki.org/ -- A No-Step-Three Wiki in Ruby
http://www.basecamphq.com/ -- Web-based Project Management
http://www.loudthinking.com/ -- Broadcasting Brain
http://www.nextangle.com/ -- Development & Consulting Services
 
D

David Heinemeier Hansson

Attached is a patch that adds a primary key class method, and alters
the
code to use it:

Great patch, Ari! Experimentors beware, though. This patch has a few
bugs, but all the known ones have been ironed out in the CVS version of
Active Record. We're adding some more tests to make sure that
everything works as it should. All of this stuff will be in 0.8.3 as
well.
--
David Heinemeier Hansson,
http://www.instiki.org/ -- A No-Step-Three Wiki in Ruby
http://www.basecamphq.com/ -- Web-based Project Management
http://www.loudthinking.com/ -- Broadcasting Brain
http://www.nextangle.com/ -- Development & Consulting Services
 
A

Aredridel

Great patch, Ari! Experimentors beware, though. This patch has a few
bugs, but all the known ones have been ironed out in the CVS version of
Active Record. We're adding some more tests to make sure that
everything works as it should. All of this stuff will be in 0.8.3 as
well.

Yeah, sorry for the low quality of the patch -- it's from my development
copy, with the irrelevant changes half picked out by hand.
Cherry-picking from patches could be a lot easier than it is with CVS...

Ari
 
P

Peter C. Verhage

Couldn't the table meta data be used to find out which field(s) is / are
part of the primary key?

Regards,

Peter
 
B

Bauduin Raphael

Peter said:
Couldn't the table meta data be used to find out which field(s) is / are
part of the primary key?

That's what I meant by 'having the name of the "id column" extracted
from the database'. That would be the best solution IMHO, but do all
database drivers support that?

If it's possible, it would really be in the Active Record way of working
(from what I've seen): extract the most information possible directly
from the database.

Raph
 
K

Kirk Haines

On Sat, 5 Jun 2004 21:38:37 +0900, Bauduin Raphael wrote
That's what I meant by 'having the name of the "id column" extracted
from the database'. That would be the best solution IMHO, but do all
database drivers support that?

If it's possible, it would really be in the Active Record way of
working
(from what I've seen): extract the most information possible
directly from the database.

Most of them support it. That's how Kansas works.


Kirk Haines
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,769
Messages
2,569,579
Members
45,053
Latest member
BrodieSola

Latest Threads

Top