summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitattributes1
-rw-r--r--demos/quickstart/protected/pages/Database/ActiveRecord.page280
-rw-r--r--demos/quickstart/protected/pages/Database/SqlMap.page236
-rw-r--r--framework/Data/ActiveRecord/Exceptions/messages.txt4
-rw-r--r--framework/Data/ActiveRecord/TActiveRecord.php33
-rw-r--r--framework/Data/ActiveRecord/TActiveRecordGateway.php15
-rw-r--r--framework/Data/ActiveRecord/Vendor/TDbMetaData.php92
-rw-r--r--framework/Data/ActiveRecord/Vendor/TDbMetaDataCommon.php26
-rw-r--r--tests/simple_unit/ActiveRecord/FindByPksTestCase.php58
-rw-r--r--tests/test_tools/simpletest/errors.php2
10 files changed, 441 insertions, 306 deletions
diff --git a/.gitattributes b/.gitattributes
index 84c9874b..d43a248b 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -2352,6 +2352,7 @@ tests/simple_unit/ActiveRecord/ActiveRecordRegistryTestCase.php -text
tests/simple_unit/ActiveRecord/BaseActiveRecordTestCase.php -text
tests/simple_unit/ActiveRecord/CountRecordsTestCase.php -text
tests/simple_unit/ActiveRecord/DeleteByPkTestCase.php -text
+tests/simple_unit/ActiveRecord/FindByPksTestCase.php -text
tests/simple_unit/ActiveRecord/FindBySqlTestCase.php -text
tests/simple_unit/ActiveRecord/SqliteTestCase.php -text
tests/simple_unit/ActiveRecord/UserRecordTestCase.php -text
diff --git a/demos/quickstart/protected/pages/Database/ActiveRecord.page b/demos/quickstart/protected/pages/Database/ActiveRecord.page
index e3da53c0..a1337ba3 100644
--- a/demos/quickstart/protected/pages/Database/ActiveRecord.page
+++ b/demos/quickstart/protected/pages/Database/ActiveRecord.page
@@ -2,53 +2,53 @@
<!-- $Id $ -->
<h1>Active Record</h1>
<p>Active Records are objects that wrap a row in a database table or view,
- encapsulates the database access and adds domain logic on that data.
- The basics of an Active Record is a business object class, e.g., a
- <tt>Products</tt> class, that match very closely the record structure
- of an underlying database table. Each Active Record will be responsible for
- saving and loading data to and from the database. </p>
+ encapsulates the database access and adds domain logic on that data.
+ The basics of an Active Record is a business object class, e.g., a
+ <tt>Products</tt> class, that match very closely the record structure
+ of an underlying database table. Each Active Record will be responsible for
+ saving and loading data to and from the database. </p>
<div class="info"><b class="note">Info:</b>
- The data structure of an Active Record should match exactly that of a table
- in the database.
- Each field in the class must correspond to one column in the table.
+ The data structure of an Active Record should match exactly that of a table
+ in the database.
+ Each field in the class must correspond to one column in the table.
</div>
<h2>When to Use It</h2>
<p>Active Record is a good choice for domain logic that isn't too complex,
- such as creates, reads, updates, and deletes. Derivations and validations
- based on a single record work well in this structure. Active Record has the
- primary advantage of simplicity. It's easy to build
- Active Records, and they are easy to understand.</p>
+ such as creates, reads, updates, and deletes. Derivations and validations
+ based on a single record work well in this structure. Active Record has the
+ primary advantage of simplicity. It's easy to build
+ Active Records, and they are easy to understand.</p>
- <p>However, as your business logic grows in complexity, you'll soon want
- to use your object's direct relationships, collections, inheritance, and so
+ <p>However, as your business logic grows in complexity, you'll soon want
+ to use your object's direct relationships, collections, inheritance, and so
forth. These don't map easily onto Active Record, and adding them piecemeal
- gets very messy.
- Another argument against Active Record is the fact that it couples the object
- design to the database design. This makes it more difficult to refactor as a project goes forward.</p>
-
- <p>The alternative is to use a Data Mapper that separates the roles of the
- business object and how these objects are stored.
- Prado provides a complimentary choice between Active Record and
- <a href="?page=Database.SqlMap">SqlMap Data Mapper</a>.
- A SqlMap Data Mapper can be used to load Active Record objects, in turn, these
- Active Record objects can be used to update the database.
- The "relationship" between Active Records and <a href="?page=Database.SqlMap">SqlMap</a> is illustrated in the
- following diagram. More details regarding the SqlMap Data Mapper can be found in
- the <a href="http://www.pradosoft.com/demos/sqlmap/">SqlMap Manual</a>.
- <img src=<%~ sqlmap_active_record.png %> alt="Active Records and SqlMap DataMapper" id="fig:diagram.png" class="figure"/>
- </p>
-
- <p>
- The Active Record class has methods that do the following:
- <ul>
- <li>Construct an instance of the Active Record from a SQL result set row.</li>
- <li>Construct a new instance for later insertion into the table.</li>
- <li>Finder methods to wrap commonly used SQL queries and return Active Record objects.</li>
- <li>Update existing records and insert new records into the database.</li>
- </ul>
- </p>
-The Active Record implementation utilizes the <a href="?page=Database.DAO">Prado DAO</a> classes for data access.
+ gets very messy.
+ Another argument against Active Record is the fact that it couples the object
+ design to the database design. This makes it more difficult to refactor as a project goes forward.</p>
+
+ <p>The alternative is to use a Data Mapper that separates the roles of the
+ business object and how these objects are stored.
+ Prado provides a complimentary choice between Active Record and
+ <a href="?page=Database.SqlMap">SqlMap Data Mapper</a>.
+ A SqlMap Data Mapper can be used to load Active Record objects, in turn, these
+ Active Record objects can be used to update the database.
+ The "relationship" between Active Records and <a href="?page=Database.SqlMap">SqlMap</a> is illustrated in the
+ following diagram. More details regarding the SqlMap Data Mapper can be found in
+ the <a href="http://www.pradosoft.com/demos/sqlmap/">SqlMap Manual</a>.
+ <img src=<%~ sqlmap_active_record.png %> alt="Active Records and SqlMap DataMapper" id="fig:diagram.png" class="figure"/>
+ </p>
+
+ <p>
+ The Active Record class has methods that do the following:
+ <ul>
+ <li>Construct an instance of the Active Record from a SQL result set row.</li>
+ <li>Construct a new instance for later insertion into the table.</li>
+ <li>Finder methods to wrap commonly used SQL queries and return Active Record objects.</li>
+ <li>Update existing records and insert new records into the database.</li>
+ </ul>
+ </p>
+The Active Record implementation utilizes the <a href="?page=Database.DAO">Prado DAO</a> classes for data access.
The current Active Record implementation supports
<a href="http://www.mysql.com">MySQL</a>,
<a href="http://www.postgres.com">Postgres SQL</a> and
@@ -56,14 +56,14 @@ The current Active Record implementation supports
Support for other databases can be provided when there are sufficient demand.
<h2>Defining an Active Record</h2>
<p>Let us
- consider the following "users" table that contains two columns named "username" and "email",
- where "username" is also the primary key.
+ consider the following "users" table that contains two columns named "username" and "email",
+ where "username" is also the primary key.
<com:TTextHighlighter Language="sql" CssClass="source">
CREATE TABLE users
(
- username VARCHAR( 20 ) NOT NULL ,
- email VARCHAR( 200 ) ,
- PRIMARY KEY ( username )
+ username VARCHAR( 20 ) NOT NULL ,
+ email VARCHAR( 200 ) ,
+ PRIMARY KEY ( username )
);
</com:TTextHighlighter>
</p>
@@ -71,91 +71,98 @@ CREATE TABLE users
<com:TTextHighlighter Language="php" CssClass="source">
class UserRecord extends TActiveRecord
{
- public $username; //the column named "username" in the "users" table
- public $email;
-
- public static $_tablename='users'; //table name
-
- /**
- * @return TActiveRecord active record finder instance
- */
- public static function finder()
- {
- return self::getRecordFinder('UserRecord');
- }
+ public $username; //the column named "username" in the "users" table
+ public $email;
+
+ public static $_tablename='users'; //table name
+
+ /**
+ * @return TActiveRecord active record finder instance
+ */
+ public static function finder()
+ {
+ return self::getRecordFinder('UserRecord');
+ }
}
</com:TTextHighlighter>
</p>
<p>Each property of the <tt>UserRecord</tt> class must correspond to a
- column with the same name in the "users" table. The static class variable
- <tt>$_tablename</tt> (must be public) is optional when the class name is the same as
- the table name in the database, otherwise <tt>$_tablename</tt> must
- specify the table name that corresponds to your Active Record class.
+ column with the same name in the "users" table. The static class variable
+ <tt>$_tablename</tt> (must be public) is optional when the class name is the same as
+ the table name in the database, otherwise <tt>$_tablename</tt> must
+ specify the table name that corresponds to your Active Record class.
</p>
<div class="tip"><b class="note">Tip:</b>
- Since <tt>TActiveRecord</tt> extends <tt>TComponent</tt>, setter and
- getter methods can be defined to allow control over how variables
- are set and returned. For example, adding a <tt>$level</tt> property to the UserRecord class:
+ Since <tt>TActiveRecord</tt> extends <tt>TComponent</tt>, setter and
+ getter methods can be defined to allow control over how variables
+ are set and returned. For example, adding a <tt>$level</tt> property to the UserRecord class:
<com:TTextHighlighter Language="php" CssClass="source">
class UserRecord extends TActiveRecord {
- ... //existing definitions as above
-
- private $_level;
- public function setLevel($value) {
- $this->_level=TPropertyValue::ensureInteger($value,0);
- }
- public function getLevel($value){
- return $this->_level;
- }
+ ... //existing definitions as above
+
+ private $_level;
+ public function setLevel($value) {
+ $this->_level=TPropertyValue::ensureInteger($value,0);
+ }
+ public function getLevel($value){
+ return $this->_level;
+ }
}
</com:TTextHighlighter>
</div>
+<div class="note"><b class="note">Note:</b>
+<tt>TActiveRecord</tt> can also work with database views by specifying the value <tt>$_tablename</tt>
+corresponding to the view name. However, objects returned
+from views are read-only, calling the <tt>save()</tt> or <tt>delete()</tt> method
+will raise an exception.
+</div>
+
<p>
- The static method <tt>finder()</tt> returns an <tt>UserRecord</tt> instance
- that can be used to load records from the database. The loading of records
- using the finer methods is discuss a little later. The <tt>TActiveRecord::getRecordFinder()</tt>
- static method takes the name of the current Active Record class as parameter.
+ The static method <tt>finder()</tt> returns an <tt>UserRecord</tt> instance
+ that can be used to load records from the database. The loading of records
+ using the finer methods is discuss a little later. The <tt>TActiveRecord::getRecordFinder()</tt>
+ static method takes the name of the current Active Record class as parameter.
</p>
<h2>Setting up a database connection</h2>
<p>
- A default database connection for Active Record can be set as follows.
- See <a href="?page=Database.DAO">Establishing Database Connection</a> for
- futher details regarding creation of database connection in general.
+ A default database connection for Active Record can be set as follows.
+ See <a href="?page=Database.DAO">Establishing Database Connection</a> for
+ futher details regarding creation of database connection in general.
<com:TTextHighlighter Language="php" CssClass="source">
//create a connection and give it to the ActiveRecord manager.
$dsn = 'pgsql:host=localhost;dbname=test'; //Postgres SQL
$conn = new TDbConnection($dsn, 'dbuser','dbpass');
TActiveRecordManager::getInstance()->setDbConnection($conn);
-</com:TTextHighlighter>
+</com:TTextHighlighter>
</p>
<p>
- The default database connection can also be configured using a <tt>&lt;module&gt;</tt>
- tag in the <a href="?page=Configurations.AppConfig">application.xml</a>
- or <a href="?page=Configurations.PageConfig">config.xml</a> as follows.
+ The default database connection can also be configured using a <tt>&lt;module&gt;</tt>
+ tag in the <a href="?page=Configurations.AppConfig">application.xml</a>
+ or <a href="?page=Configurations.PageConfig">config.xml</a> as follows.
<com:TTextHighlighter Language="xml" CssClass="source">
<modules>
<module class="System.Data.ActiveRecord.TActiveRecordConfig" EnableCache="true">
<database ConnectionString="pgsql:host=localhost;dbname=test"
Username="dbuser" Password="dbpass" />
</module>
-</modules>
-</com:TTextHighlighter>
+</modules>
+</com:TTextHighlighter>
<div class="tip"><b class="note">Tip:</b>
- The <tt>EnableCache</tt> attribute when set to "true" will cache the table
- meta data, that is, the table columns names, indexes and constraints are
- saved in the cache and reused. You must clear or disable the cache if you
- wish to see chanages made to your table definitions. A <a href="?page=Advanced.Performance#6402">cache
- module</a> must also be defined for the cache to function.
+ The <tt>EnableCache</tt> attribute when set to "true" will cache the table
+ meta data, that is, the table columns names, indexes and constraints are
+ saved in the cache and reused. You must clear or disable the cache if you
+ wish to see chanages made to your table definitions. A <a href="?page=Advanced.Performance#6402">cache
+ module</a> must also be defined for the cache to function.
</div>
</p>
<p>A <tt>ConnectionID</tt> property can be specified with value corresponding
- to another <tt>TDataSourceConfig</tt> module configuration's ID value. This allows
- the same database connection to be used in other modules such as <a href="?page=Database.SqlMap">SqlMap</a>.
+ to another <tt>TDataSourceConfig</tt> module configuration's ID value. This allows
+ the same database connection to be used in other modules such as <a href="?page=Database.SqlMap">SqlMap</a>.
<com:TTextHighlighter Language="xml" CssClass="source">
<modules>
<module class="System.Data.TDataSourceConfig" ID="db1">
@@ -168,32 +175,52 @@ TActiveRecordManager::getInstance()->setDbConnection($conn);
<module class="System.Data.SqlMap.TSqlMapConfig"
ConnectionID="db1" ... />
-</modules>
-</com:TTextHighlighter>
+</modules>
+</com:TTextHighlighter>
</p>
<h2>Loading data from the database</h2>
<p>
- The <tt>TActiveRecord</tt> class provides many convenient methods to find
- records from the database. The simplest is finding records by matching primary keys.
- See the <com:DocLink ClassPath="System.Data.ActiveRecord.TActiveRecord" /> for
- more details.
+ The <tt>TActiveRecord</tt> class provides many convenient methods to find
+ records from the database. The simplest is finding records by matching primary keys.
+ See the <com:DocLink ClassPath="System.Data.ActiveRecord.TActiveRecord" /> for
+ more details.
</p>
- <h3><tt>findByPk()</tt></h3>
- <p>Finds one record using only the primary key or composite primary keys.
+ <h3><tt>findByPk()</tt></h3>
+ <p>Finds one record using only the primary key or composite primary keys.
<com:TTextHighlighter Language="php" CssClass="source">
$finder = UserRecord::finder();
$user = $finder->findByPk($primaryKey);
//when the table uses composite keys
-$record = $finder->findByPk($key1, $key2, ...); //for composite keys
-$record = $finder->findByPk(array($key1, $key2,...)); //same as above
+$record = $finder->findByPk($key1, $key2, ...);
+$record = $finder->findByPk(array($key1, $key2,...));
</com:TTextHighlighter>
</p>
+ <h3><tt>findAllByPks()</tt></h3>
+ <p>Finds multiple records using a list of primary keys or composite primary keys.
+The following are equivalent for scalar primary keys (primary key consisting of only one column/field).
+<com:TTextHighlighter Language="php" CssClass="source">
+$finder = UserRecord::finder();
+$users = $finder->findAllByPk($key1, $key2, ...);
+$users = $finder->findAllByPk(array($key1, $key2, ...));
+</com:TTextHighlighter>
+The following are equivalent for composite keys.
+<com:TTextHighlighter Language="php" CssClass="source">
+//when the table uses composite keys
+$record = $finder->findAllByPks(array($key1, $key2), array($key3, $key4), ...);
+
+$keys = array( array($key1, $key2), array($key3, $key4), ... );
+$record = $finder->findAllByPks($keys);
+
+</com:TTextHighlighter>
+</p>
+
+
<h3><tt>find()</tt></h3>
<p>Finds <b>one single record</b> that matches the criteria. The criteria
- can be a partial SQL string or a <tt>TActiveRecordCriteria</tt> object.
+ can be a partial SQL string or a <tt>TActiveRecordCriteria</tt> object.
<com:TTextHighlighter Language="php" CssClass="source">
$finder = UserRecord::finder();
@@ -212,13 +239,13 @@ $finder->find($criteria); //the 2nd parameter for find() is ignored.
</p>
<p>The <tt>TActiveRecordCriteria</tt> class has the following properties:
- <ul>
- <li><tt>Parameters</tt> -- name value parameter pairs.</li>
- <li><tt>OrderBy</tt> -- column name and ordering pairs.</li>
- <li><tt>Condition</tt> -- parts of the WHERE SQL conditions.</li>
- <li><tt>Limit</tt> -- maximum number of records to return.</li>
- <li><tt>Offset</tt> -- record offset in the table.</li>
- </ul>
+ <ul>
+ <li><tt>Parameters</tt> -- name value parameter pairs.</li>
+ <li><tt>OrderBy</tt> -- column name and ordering pairs.</li>
+ <li><tt>Condition</tt> -- parts of the WHERE SQL conditions.</li>
+ <li><tt>Limit</tt> -- maximum number of records to return.</li>
+ <li><tt>Offset</tt> -- record offset in the table.</li>
+ </ul>
</p>
<com:TTextHighlighter Language="php" CssClass="source">
@@ -258,8 +285,8 @@ $finder->find('Username = ? AND Password = ?', $name, $pass);
$finder->findAllByAge($age);
$finder->findAll('Age = ?', $age);
</com:TTextHighlighter>
-</p>
-
+</p>
+
<h3><tt>findBySql()</tt></h3>
<p>Finds records using full SQL, returns corresponding array of record objects.</p>
@@ -291,7 +318,7 @@ incremented values.</div>
<p>
To update a record in the database, just change one or more properties of
the Active Record object that has been loaded from the database and then
-call the <tt>save()</tt> method.
+call the <tt>save()</tt> method.
<com:TTextHighlighter Language="php" CssClass="source">
$user = UserRecord::finder()->findByName('admin');
@@ -303,14 +330,23 @@ $user->save(); //update it.
<p>
Active Record objects have a simple life-cycle illustrated in the following diagram.
<img src=<%~ object_states.png %> alt="Active Records Life Cycle" id="fig:cycle.png" class="figure"/>
+We see that new ActiveRecord objects are created by either using one of the <tt>find*()</tt>
+methods or using creating a new instance by using PHP's <tt>new</tt> keyword. Objects
+created by a <tt>find*()</tt> method starts with <tt>clean</tt> state. New instance of
+ActiveRecords created other than by a <tt>find*()</tt> method starts with <tt>new</tt> state.
+When ever you
+call the <tt>save()</tt> method on the ActiveRecord object, the object enters the <tt>clean</tt>
+state. Objects in the <tt>clean</tt> becomes <tt>dirty</tt> whenever one of more of its
+internal states are changed. Calling the <tt>delete()</tt> method on the object
+ends the object life-cycle, no futher actions can be performed on the object.
</p>
<h2>Deleting existing records</h2>
<p>
- To delete an existing record that is already loaded, just call the <tt>delete()</tt> method.
- You can also delete records in the database by primary keys without
- loading any records using the <tt>deleteByPk()</tt> method.
- For example, to delete one or records with tables having a scalar primary key.
+ To delete an existing record that is already loaded, just call the <tt>delete()</tt> method.
+ You can also delete records in the database by primary keys without
+ loading any records using the <tt>deleteByPk()</tt> method.
+ For example, to delete one or records with tables having a scalar primary key.
<com:TTextHighlighter Language="php" CssClass="source">
$finder->deleteByPk($primaryKey); //delete 1 record
$finder->deleteByPk($key1,$key2,...); //delete multiple records
@@ -333,7 +369,7 @@ $finder->deleteByPk(array( array($key1,$key2), array($key3,$key4), .. ));
<h2>Transactions</h2>
<p>All Active Record objects contains the property <tt>DbConnection</tt>
- that can be used to obtain a transaction object.
+ that can be used to obtain a transaction object.
<com:TTextHighlighter Language="php" CssClass="source">
$finder = UserRecord::finder();
@@ -345,7 +381,7 @@ try
$user->save();
$transaction->commit();
}
-catch(Exception $e) // an exception is raised if a query fails will be raised
+catch(Exception $e) // an exception is raised if a query fails
{
$transaction->rollBack();
}
@@ -353,8 +389,8 @@ catch(Exception $e) // an exception is raised if a query fails will be raised
<h2>References</h2>
<ul>
- <li>Fowler et. al. <i>Patterns of Enterprise Application Architecture</i>,
- Addison Wesley, 2002.</li>
+ <li>Fowler et. al. <i>Patterns of Enterprise Application Architecture</i>,
+ Addison Wesley, 2002.</li>
</ul>
</com:TContent> \ No newline at end of file
diff --git a/demos/quickstart/protected/pages/Database/SqlMap.page b/demos/quickstart/protected/pages/Database/SqlMap.page
index 2b70e8f5..4b462168 100644
--- a/demos/quickstart/protected/pages/Database/SqlMap.page
+++ b/demos/quickstart/protected/pages/Database/SqlMap.page
@@ -3,78 +3,78 @@
<h1>Data Mapper</h1>
<p>Data Mappers moves data between objects and a database while keeping them
- independent of each other and the mapper itself. If you started with
- <a href="?page=Database.ActiveRecord">Active Records</a>, you may eventually
- faced with more complex business
- objects as your project progresses. When you build an object model with a
- lot of business logic it's valuable to use these mechanisms to better organize
- the data and the behavior that goes with it. Doing so leads to variant schemas;
- that is, the object schema and the relational schema don't match up.
+ independent of each other and the mapper itself. If you started with
+ <a href="?page=Database.ActiveRecord">Active Records</a>, you may eventually
+ faced with more complex business
+ objects as your project progresses. When you build an object model with a
+ lot of business logic it's valuable to use these mechanisms to better organize
+ the data and the behavior that goes with it. Doing so leads to variant schemas;
+ that is, the object schema and the relational schema don't match up.
</p>
<p>The Data Mapper separates the in-memory objects from the database. Its responsibility
- is to transfer data between the two and also to isolate them from each other.
- With Data Mapper the in-memory objects needn't know even that there's a database
- present; they need no SQL interface code, and certainly no knowledge of the
- database schema. (The database schema is always ignorant of the objects that use it.)
+ is to transfer data between the two and also to isolate them from each other.
+ With Data Mapper the in-memory objects needn't know even that there's a database
+ present; they need no SQL interface code, and certainly no knowledge of the
+ database schema. (The database schema is always ignorant of the objects that use it.)
</p>
<h2>When to Use It</h2>
<p>The primary occasion for using Data Mapper is when you want the database schema
- and the object model to evolve independently. Data Mapper's primary benefit is
- that when working on the business (or domain) objects you can ignore the database, both in
- design and in the build and testing process. The domain objects have no idea
- what the database structure is, because all the correspondence is done by the mappers.
+ and the object model to evolve independently. Data Mapper's primary benefit is
+ that when working on the business (or domain) objects you can ignore the database, both in
+ design and in the build and testing process. The domain objects have no idea
+ what the database structure is, because all the correspondence is done by the mappers.
</p>
<p>This helps you in the code because you can understand and work with the domain objects
- without having to understand how they're stored in the database. You can modify the
- business models or the database without having to alter either. With complicated
- mappings, particularly those involving <b>existing databases</b>, this is very valuable.
+ without having to understand how they're stored in the database. You can modify the
+ business models or the database without having to alter either. With complicated
+ mappings, particularly those involving <b>existing databases</b>, this is very valuable.
</p>
<p>The price, of course, is the extra layer that you don't get with
- <a href="?page=Database.ActiveRecord">Active Record</a>,
- so the test for using these patterns is the complexity of the business logic.
- If you have fairly simple business logic, an <a href="?page=Database.ActiveRecord">Active Record</a>
- will probably work.
- For more complicated logic a Data Mapper may be more suitable.
+ <a href="?page=Database.ActiveRecord">Active Record</a>,
+ so the test for using these patterns is the complexity of the business logic.
+ If you have fairly simple business logic, an <a href="?page=Database.ActiveRecord">Active Record</a>
+ will probably work.
+ For more complicated logic a Data Mapper may be more suitable.
</p>
<h2>SqlMap Data Mapper</h2>
<p>The SqlMap DataMapper framework makes it easier to use a database with a PHP application.
- SqlMap DataMapper couples objects with stored procedures or SQL statements using
- a XML descriptor. Simplicity is the biggest advantage of the SqlMap DataMapper over
- object relational mapping tools. To use SqlMap DataMapper you rely on your own objects,
- XML, and SQL. There is little to learn that you don't already know.
- With SqlMap DataMapper you have the full power of both SQL and stored procedures at
- your fingertip
+ SqlMap DataMapper couples objects with stored procedures or SQL statements using
+ a XML descriptor. Simplicity is the biggest advantage of the SqlMap DataMapper over
+ object relational mapping tools. To use SqlMap DataMapper you rely on your own objects,
+ XML, and SQL. There is little to learn that you don't already know.
+ With SqlMap DataMapper you have the full power of both SQL and stored procedures at
+ your fingertip
</p>
<p>
- <img src=<%~ diagram.png %> alt="SqlMap Data Mapper Overview" id="fig:sqlmap.png" class="figure"/>
-
- Here's a high level description of the work flow illustrated in the figure abov.
- Provide a parameter, either as an object or a primitive type. The parameter can be
- used to set runtime values in your SQL statement or stored procedure. If a runtime value
- is not needed, the parameter can be omitted.
+ <img src=<%~ diagram.png %> alt="SqlMap Data Mapper Overview" id="fig:sqlmap.png" class="figure"/>
+
+ Here's a high level description of the work flow illustrated in the figure abov.
+ Provide a parameter, either as an object or a primitive type. The parameter can be
+ used to set runtime values in your SQL statement or stored procedure. If a runtime value
+ is not needed, the parameter can be omitted.
</p>
<p>Execute the mapping by passing the parameter and the name you gave the statement or
- procedure in your XML descriptor. This step is where the magic happens. The framework
- will prepare the SQL statement or stored procedure, set any runtime values using your
- parameter, execute the procedure or statement, and return the result.
+ procedure in your XML descriptor. This step is where the magic happens. The framework
+ will prepare the SQL statement or stored procedure, set any runtime values using your
+ parameter, execute the procedure or statement, and return the result.
</p>
<p>In the case of an update, the number of rows affected is returned. In the case of a
- query, a single object, or a collection of objects is returned. Like the parameter,
- the result object, or collection of objects, can be a plain-old object or a primitive PHP type.
+ query, a single object, or a collection of objects is returned. Like the parameter,
+ the result object, or collection of objects, can be a plain-old object or a primitive PHP type.
</p>
<h2>Setting up a database connection and initializing the SqlMap</h2>
<p>
- A database connection for SqlMap can be set as follows.
- See <a href="?page=Database.DAO">Establishing Database Connection</a> for
- futher details regarding creation of database connection in general.
+ A database connection for SqlMap can be set as follows.
+ See <a href="?page=Database.DAO">Establishing Database Connection</a> for
+ futher details regarding creation of database connection in general.
<com:TTextHighlighter Language="php" CssClass="source">
//create a connection and give it to the SqlMap manager.
$dsn = 'pgsql:host=localhost;dbname=test'; //Postgres SQL
@@ -82,21 +82,21 @@ $conn = new TDbConnection($dsn, 'dbuser','dbpass');
$manager = new TSqlMapManager($conn);
$manager->configureXml('my-sqlmap.xml');
$sqlmap = $manager->getSqlMapGateway();
-</com:TTextHighlighter>
+</com:TTextHighlighter>
</p>
<p>
- The <tt>TSqlMapManager</tt> is responsible for setting up the database connection
- and configuring the SqlMap with given XML file(s). The <tt>configureXml()</tt>
- method accepts a string that points to a SqlMap XML configuration file. Once
- configured, call the <tt>getSqlMapGateway()</tt> method to obtain an instance
- of the SqlMap gateway interface (use this object to insert/delete/find records).
+ The <tt>TSqlMapManager</tt> is responsible for setting up the database connection
+ and configuring the SqlMap with given XML file(s). The <tt>configureXml()</tt>
+ method accepts a string that points to a SqlMap XML configuration file. Once
+ configured, call the <tt>getSqlMapGateway()</tt> method to obtain an instance
+ of the SqlMap gateway interface (use this object to insert/delete/find records).
</p>
<p>
- SqlMap database connection can also be configured using a <tt>&lt;module&gt;</tt>
- tag in the <a href="?page=Configurations.AppConfig">application.xml</a>
- or <a href="?page=Configurations.PageConfig">config.xml</a> as follows.
+ SqlMap database connection can also be configured using a <tt>&lt;module&gt;</tt>
+ tag in the <a href="?page=Configurations.AppConfig">application.xml</a>
+ or <a href="?page=Configurations.PageConfig">config.xml</a> as follows.
<com:TTextHighlighter Language="xml" CssClass="source">
<modules>
<module id="my-sqlmap" class="System.Data.SqlMap.TSqlMapConfig"
@@ -104,66 +104,66 @@ $sqlmap = $manager->getSqlMapGateway();
<database ConnectionString="pgsql:host=localhost;dbname=test"
Username="dbuser" Password="dbpass" />
</module>
-</modules>
-</com:TTextHighlighter>
+</modules>
+</com:TTextHighlighter>
</p>
<p>
- The <tt>ConfigFile</tt> attribute should point to a SqlMap configuration file
- (to be detailed later) either using absolute path, relative path or the
- Prado's namespace dot notation path (must omit the ".xml" extension).
-
- <div class="tip"><b class="note">Tip:</b>
- The <tt>EnableCache</tt> attribute when set to "true" will cache the
- parsed configuration. You must clear or disable the cache if you
- make chanages your configuration file.
- A <a href="?page=Advanced.Performance#6402">cache
- module</a> must also be defined for the cache to function.
- </div>
+ The <tt>ConfigFile</tt> attribute should point to a SqlMap configuration file
+ (to be detailed later) either using absolute path, relative path or the
+ Prado's namespace dot notation path (must omit the ".xml" extension).
+
+ <div class="tip"><b class="note">Tip:</b>
+ The <tt>EnableCache</tt> attribute when set to "true" will cache the
+ parsed configuration. You must clear or disable the cache if you
+ make chanages your configuration file.
+ A <a href="?page=Advanced.Performance#6402">cache
+ module</a> must also be defined for the cache to function.
+ </div>
</p>
<p>To obtain the SqlMap gateway interface from the &lt;module&gt; configuration, simply
- do, for example,
+ do, for example,
<com:TTextHighlighter Language="php" CssClass="source">
class MyPage extends TPage
{
- public function onLoad($param)
- {
- parent::onLoad($param);
- $sqlmap = $this->Application->Modules['my-sqlmap']->Client;
- $sqlmap->queryForObject(...); //query for some object
- }
+ public function onLoad($param)
+ {
+ parent::onLoad($param);
+ $sqlmap = $this->Application->Modules['my-sqlmap']->Client;
+ $sqlmap->queryForObject(...); //query for some object
+ }
}
</com:TTextHighlighter>
</p>
<h2>A quick example</h2>
<p>Let us
- consider the following "users" table that contains two columns named "username" and "email",
- where "username" is also the primary key.
+ consider the following "users" table that contains two columns named "username" and "email",
+ where "username" is also the primary key.
<com:TTextHighlighter Language="sql" CssClass="source">
CREATE TABLE users
(
- username VARCHAR( 20 ) NOT NULL ,
- email VARCHAR( 200 ) ,
- PRIMARY KEY ( username )
+ username VARCHAR( 20 ) NOT NULL ,
+ email VARCHAR( 200 ) ,
+ PRIMARY KEY ( username )
);
</com:TTextHighlighter>
</p>
<p>Next we define our plain <tt>User</tt> class as follows. Notice that
- the <tt>User</tt> is very simple.
+ the <tt>User</tt> is very simple.
<com:TTextHighlighter Language="php" CssClass="source">
class User
{
- public $username;
- public $email;
+ public $username;
+ public $email;
}
</com:TTextHighlighter>
</p>
</p>
<p>Next, we need to define a SqlMap XMl configuration file, lets name
- the file as <tt>my-sqlmap.xml</tt>
+ the file as <tt>my-sqlmap.xml</tt>
<com:TTextHighlighter Language="xml" CssClass="source">
<?xml version="1.0" encoding="utf-8" ?>
<sqlMapConfig>
@@ -174,10 +174,10 @@ class User
</com:TTextHighlighter>
</p>
<p>The &lt;select&gt; tag returns defines an SQL statement. The <tt>id</tt>
- attribute will be used as the identifier for the query. The <tt>resultClass</tt>
- attribute value is the name of the class the the objects to be returned.
- We can now query the objects as follows:
-
+ attribute will be used as the identifier for the query. The <tt>resultClass</tt>
+ attribute value is the name of the class the the objects to be returned.
+ We can now query the objects as follows:
+
<com:TTextHighlighter Language="php" CssClass="source">
//assume that $sqlmap is an TSqlMapGateway instance
$userList = $sqlmap->queryForList("SelectUsers");
@@ -188,49 +188,49 @@ $user = $sqlmap->queryForObject("SelectUsers");
</p>
<p>The above example shows demonstrates only a fraction of the capabilities
- of the SqlMap Data Mapper. Further details can be found in the
- <a href="http://www.pradosoft.com/demo/sqlamp/">SqlMap Manual</a>.
+ of the SqlMap Data Mapper. Further details can be found in the
+ <a href="http://www.pradosoft.com/demo/sqlamp/">SqlMap Manual</a>.
</p>
<h2>Combining SqlMap with Active Records</h2>
<p>The above example may seem trival and it also seems that there is
- alot work just to retrieve some data. However, notice that the <tt>User</tt>
- class is totally unware of been stored in the database, and the database is
- unware of the <tt>User</tt> class.
+ alot work just to retrieve some data. However, notice that the <tt>User</tt>
+ class is totally unware of been stored in the database, and the database is
+ unware of the <tt>User</tt> class.
</p>
<p>
- One of advantages of SqlMap is the
- ability to map complex object relationship, collections from an existing
- database. On the other hand, <a href="?page=Database.ActiveRecord">Active Record</a>
- provide a very simple way
- to interact with the underlying database but unable to do more complicated
- relationship or collections. A good compromise is to use SqlMap to retrieve
- complicated relationships and collections as Active Record objects and then using
- these Active Records to do the updates, inserts and deletes.
+ One of advantages of SqlMap is the
+ ability to map complex object relationship, collections from an existing
+ database. On the other hand, <a href="?page=Database.ActiveRecord">Active Record</a>
+ provide a very simple way
+ to interact with the underlying database but unable to do more complicated
+ relationship or collections. A good compromise is to use SqlMap to retrieve
+ complicated relationships and collections as Active Record objects and then using
+ these Active Records to do the updates, inserts and deletes.
</p>
<p>Continuing with the previous example, we change the definition of the
- <tt>User</tt> class to become an Active Record.
+ <tt>User</tt> class to become an Active Record.
<com:TTextHighlighter Language="php" CssClass="source">
class UserRecord extends TActiveRecord
{
- public $username; //the column named "username" in the "users" table
- public $email;
-
- private static $_tablename='users'; //table name
-
- /**
- * @return TActiveRecord active record finder instance
- */
- public static function finder()
- {
- return self::getRecordFinder('UserRecord');
- }
+ public $username; //the column named "username" in the "users" table
+ public $email;
+
+ private static $_tablename='users'; //table name
+
+ /**
+ * @return TActiveRecord active record finder instance
+ */
+ public static function finder()
+ {
+ return self::getRecordFinder('UserRecord');
+ }
}
</com:TTextHighlighter>
</p>
<p>We also need to change the definition of the SqlMap XML configuration. We
- just need to change the value of <tt>resultClass</tt> attribute to <tt>UserRecord</tt>.
+ just need to change the value of <tt>resultClass</tt> attribute to <tt>UserRecord</tt>.
<com:TTextHighlighter Language="xml" CssClass="source">
<?xml version="1.0" encoding="utf-8" ?>
<sqlMapConfig>
@@ -243,8 +243,8 @@ class UserRecord extends TActiveRecord
<p>The PHP code for retrieving the users remains the same, but SqlMap
- returns Active Records instead, and we can take advantage of the Active Record methods.
-
+ returns Active Records instead, and we can take advantage of the Active Record methods.
+
<com:TTextHighlighter Language="php" CssClass="source">
//assume that $sqlmap is an TSqlMapGateway instance
$user = $sqlmap->queryForObject("SelectUsers");
@@ -256,10 +256,10 @@ $user->save(); //save it using Active Record
<h2>References</h2>
<ul>
- <li>Fowler et. al. <i>Patterns of Enterprise Application Architecture</i>,
- Addison Wesley, 2002.</li>
- <li>xxxx. <i>iBatis Data Mapper</i>,
- <a href="http://www.apache.org/ibatis">http://www.apache.org/ibatis</a>.</li>
+ <li>Fowler et. al. <i>Patterns of Enterprise Application Architecture</i>,
+ Addison Wesley, 2002.</li>
+ <li>xxxx. <i>iBatis Data Mapper</i>,
+ <a href="http://www.apache.org/ibatis">http://www.apache.org/ibatis</a>.</li>
</ul>
</com:TContent> \ No newline at end of file
diff --git a/framework/Data/ActiveRecord/Exceptions/messages.txt b/framework/Data/ActiveRecord/Exceptions/messages.txt
index 92bdb30f..774c0275 100644
--- a/framework/Data/ActiveRecord/Exceptions/messages.txt
+++ b/framework/Data/ActiveRecord/Exceptions/messages.txt
@@ -10,4 +10,6 @@ ar_primary_key_is_scalar = Primary key '{1}' in table '{0}' is NOT a composi
ar_invalid_db_connection = Missing or invalid default database connection for ActiveRecord class '{0}', default connection is set by the DbConnection property of TActiveRecordManager.
ar_mismatch_args_exception = ActiveRecord finder method '{0}' expects {1} parameters but found only {2} parameters instead.
ar_invalid_tablename_property = ActiveRecord tablename property '{0}::${1}' must be static and not null.
-ar_value_must_not_be_null = Property '{0}::${2}' must not be null as defined by column '{2}' in table '{1}'. \ No newline at end of file
+ar_value_must_not_be_null = Property '{0}::${2}' must not be null as defined by column '{2}' in table '{1}'.
+ar_missing_pk_values = Missing primary key values in forming IN(key1, key2, ...) for table '{0}'.
+ar_pk_value_count_mismatch = Composite key value count mismatch in forming IN( (key1, key2, ..), (key3, key4, ..)) for table '{0}'. \ No newline at end of file
diff --git a/framework/Data/ActiveRecord/TActiveRecord.php b/framework/Data/ActiveRecord/TActiveRecord.php
index 68d63a23..d3e25dcf 100644
--- a/framework/Data/ActiveRecord/TActiveRecord.php
+++ b/framework/Data/ActiveRecord/TActiveRecord.php
@@ -38,7 +38,7 @@ Prado::using('System.Data.ActiveRecord.TActiveRecordCriteria');
* public $username; //corresponds to the fieldname in the table
* public $email;
*
- * private static final $_tablename='users'; //optional table name.
+ * public static final $_tablename='users'; //optional table name.
*
* //returns active record finder instance
* public static function finder()
@@ -332,7 +332,7 @@ abstract class TActiveRecord extends TComponent
*/
public function findByPk($keys)
{
- if(func_num_args() > 1 && !is_array($keys))
+ if(func_num_args() > 1)
$keys = func_get_args();
$gateway = $this->getRecordManager()->getRecordGateway();
$data = $gateway->findRecordByPK($this,$keys);
@@ -340,6 +340,35 @@ abstract class TActiveRecord extends TComponent
}
/**
+ * Find multiple records matching a list of primary or composite keys.
+ *
+ * For scalar primary keys:
+ * <code>
+ * $finder->findAllByPk($key1, $key2, ...);
+ * $finder->findAllByPk(array($key1, $key2, ...));
+ * </code>
+ *
+ * For composite keys:
+ * <code>
+ * $finder->findAllByPk(array($key1, $key2), array($key3, $key4), ...);
+ * $finder->findAllByPk(array(array($key1, $key2), array($key3, $key4), ...));
+ * </code>
+ * @param mixed primary keys
+ * @return array matching ActiveRecords
+ */
+ public function findAllByPks($keys)
+ {
+ if(func_num_args() > 1)
+ $keys = func_get_args();
+ $gateway = $this->getRecordManager()->getRecordGateway();
+ $results = array();
+ $class = get_class($this);
+ foreach($gateway->findRecordsByPks($this,(array)$keys) as $data)
+ $results[] = $this->populateObject($class,$data);
+ return $results;
+ }
+
+ /**
* Find records using full SQL, returns corresponding record object.
* @param string select SQL
* @param array $parameters
diff --git a/framework/Data/ActiveRecord/TActiveRecordGateway.php b/framework/Data/ActiveRecord/TActiveRecordGateway.php
index 1cb1c79f..7bcd0eb2 100644
--- a/framework/Data/ActiveRecord/TActiveRecordGateway.php
+++ b/framework/Data/ActiveRecord/TActiveRecordGateway.php
@@ -147,6 +147,21 @@ class TActiveRecordGateway extends TComponent
}
/**
+ * Returns records matching the list of given primary keys.
+ * @param TActiveRecord active record instance.
+ * @param array list of primary name value pairs
+ * @return array matching data.
+ */
+ public function findRecordsByPks(TActiveRecord $record, $keys)
+ {
+ $meta = $this->getMetaData($record);
+ $command = $meta->getFindInPksCommand($record->getDbConnection(), $keys);
+ $this->raiseCommandEvent(TActiveRecordStatementType::Select,$command,$record,$keys);
+ return $meta->postQuery($command->query());
+ }
+
+
+ /**
* Returns record data matching the given critera. If $iterator is true, it will
* return multiple rows as TDbDataReader otherwise it returns the <b>first</b> row data.
* @param TActiveRecord active record finder instance.
diff --git a/framework/Data/ActiveRecord/Vendor/TDbMetaData.php b/framework/Data/ActiveRecord/Vendor/TDbMetaData.php
index efb7c467..38a82aef 100644
--- a/framework/Data/ActiveRecord/Vendor/TDbMetaData.php
+++ b/framework/Data/ActiveRecord/Vendor/TDbMetaData.php
@@ -137,6 +137,43 @@ abstract class TDbMetaData extends TComponent
}
/**
+ * Construct a "pk IN ('key1', 'key2', ...)" criteria.
+ * @param TDbConnection database connection.
+ * @param array values for IN predicate
+ * @param string SQL string for primary keys IN a list.
+ */
+ protected function getCompositeKeysCriteria($conn, $values)
+ {
+ $count = count($this->getPrimaryKeys());
+ if($count===0)
+ throw new TActiveRecordException('ar_no_primary_key_found',$this->getTableName());
+ if(!is_array($values) || count($values) === 0)
+ throw new TActiveRecordException('ar_missing_pk_values', $this->getTableName());
+ if($count>1 && !is_array($values[0]))
+ $values = array($values);
+ if($count > 1 && count($values[0]) !== $count)
+ throw new TActiveRecordException('ar_pk_value_count_mismatch', $this->getTableName());
+
+ $columns = array();
+ foreach($this->getPrimaryKeys() as $key)
+ $columns[] = $this->getColumn($key)->getName();
+ return '('.implode(', ',$columns).') IN '.$this->quoteTuple($conn, $values);
+ }
+
+ /**
+ * @param TDbConnection database connection.
+ * @param array values
+ * @return string quoted recursive tuple values, e.g. "('val1', 'val2')".
+ */
+ protected function quoteTuple($conn, $array)
+ {
+ $data = array();
+ foreach($array as $k=>$v)
+ $data[] = is_array($v) ? $this->quoteTuple($conn, $v) : $conn->quoteString($v);
+ return '('.implode(', ', $data).')';
+ }
+
+ /**
* Bind a list of variables in the command. The named parameters is taken
* from the values of the $keys parameter. The bind value is taken from the
* $values parameter using the index taken from the each value of $keys array.
@@ -308,60 +345,5 @@ x * @param array name value pairs of columns for update.
return implode(', ', $fields);
}
- /**
- * @param TDbConnection database connection
- * @param array primary key values.
- * @return string delete criteria for multiple scalar primary keys.
- */
- protected function getDeleteInPkCriteria($conn, $keys)
- {
- $pk = $this->getPrimaryKeys();
- $column = $this->getColumn($pk[0])->getName();
- $values = array();
- foreach($keys as $key)
- {
- if(is_array($key))
- {
- throw new TActiveRecordException('ar_primary_key_is_scalar',
- $this->getTableName(),$column,'array('.implode(', ',$key).')');
- }
- $values[] = $conn->quoteString($key);
- }
- $pks = implode(', ', $values);
- return "$column IN ($pks)";
- }
-
- /**
- * @param TDbConnection database connection
- * @param array primary key values.
- * @return string delete criteria for multiple composite primary keys.
- */
- protected function getDeleteMultiplePkCriteria($conn,$pks)
- {
- //check for 1 set composite keys
- if(count($pks)>0 && !is_array($pks[0]))
- $pks = array($pks);
- $conditions=array();
- foreach($pks as $keys)
- $conditions[] = $this->getDeleteCompositeKeyCondition($conn,$keys);
- return implode(' OR ', $conditions);
- }
-
- /**
- * @return string delete criteria for 1 composite key.
- */
- protected function getDeleteCompositeKeyCondition($conn,$keys)
- {
- $condition=array();
- $index = 0;
- foreach($this->getPrimarykeys() as $pk)
- {
- $name = $this->getColumn($pk)->getName();
- $value = isset($keys[$pk]) ? $keys[$pk] : $keys[$index];
- $condition[] = "$name = ".$conn->quoteString($value);
- $index++;
- }
- return '('.implode(' AND ', $condition).')';
- }
}
?> \ No newline at end of file
diff --git a/framework/Data/ActiveRecord/Vendor/TDbMetaDataCommon.php b/framework/Data/ActiveRecord/Vendor/TDbMetaDataCommon.php
index fffdb6fb..7f7dad8b 100644
--- a/framework/Data/ActiveRecord/Vendor/TDbMetaDataCommon.php
+++ b/framework/Data/ActiveRecord/Vendor/TDbMetaDataCommon.php
@@ -41,6 +41,24 @@ abstract class TDbMetaDataCommon extends TDbMetaData
}
/**
+ * SQL database command for finding records by a list of primary keys.
+ * @param TDbConnection database connection.
+ * @param array list of primary keys to match.
+ * @return TDbCommand find by list of primary keys command.
+ */
+ public function getFindInPksCommand($conn, $keys)
+ {
+ $conn->setActive(true);
+ $columns = $this->getSelectionColumns();
+ $table = $this->getTableName();
+ $criteria = $this->getCompositeKeysCriteria($conn,$keys);
+ $sql = "SELECT {$columns} FROM {$table} WHERE {$criteria}";
+ $command = $conn->createCommand($sql);
+ $command->prepare();
+ return $command;
+ }
+
+ /**
* SQL database command for finding records using a criteria object.
* @param TDbConnection database connection.
* @param TActiveRecordCriteria criteria object
@@ -160,14 +178,8 @@ abstract class TDbMetaDataCommon extends TDbMetaData
public function getDeleteByPkCommand($conn,$keys)
{
$conn->setActive(true);
- $numKeys = count($this->getPrimaryKeys());
$table = $this->getTableName();
- if($numKeys===0)
- throw new TActiveRecordException('ar_no_primary_key_found',$table);
- if($numKeys===1)
- $criteria = $this->getDeleteInPkCriteria($conn,$keys);
- else
- $criteria = $this->getDeleteMultiplePkCriteria($conn,$keys);
+ $criteria = $this->getCompositeKeysCriteria($conn, $keys);
$sql = "DELETE FROM {$table} WHERE {$criteria}";
$command = $conn->createCommand($sql);
$command->prepare();
diff --git a/tests/simple_unit/ActiveRecord/FindByPksTestCase.php b/tests/simple_unit/ActiveRecord/FindByPksTestCase.php
new file mode 100644
index 00000000..e7f3e3d0
--- /dev/null
+++ b/tests/simple_unit/ActiveRecord/FindByPksTestCase.php
@@ -0,0 +1,58 @@
+<?php
+
+Prado::using('System.Data.ActiveRecord.TActiveRecord');
+require_once(dirname(__FILE__).'/records/DepartmentRecord.php');
+require_once(dirname(__FILE__).'/records/DepSections.php');
+
+class FindByPksTestCase extends UnitTestCase
+{
+ function setup()
+ {
+ $conn = new TDbConnection('pgsql:host=localhost;dbname=test', 'test','test');
+ TActiveRecordManager::getInstance()->setDbConnection($conn);
+ }
+
+ function test_find_by_1pk()
+ {
+ $dep = DepartmentRecord::finder()->findByPk(1);
+ $this->assertNotNull($dep);
+ $this->assertEqual($dep->department_id, 1);
+ }
+
+ function test_find_by_pks()
+ {
+ $deps = DepartmentRecord::finder()->findAllByPks(1,2,4);
+ $this->assertEqual(count($deps), 3);
+
+ $this->assertEqual($deps[0]->department_id, 1);
+ $this->assertEqual($deps[1]->department_id, 2);
+ $this->assertEqual($deps[2]->department_id, 4);
+ }
+
+ function test_find_by_pks_with_invalid()
+ {
+ $deps = DepartmentRecord::finder()->findAllByPks(4,2,14);
+ $this->assertEqual(count($deps), 2);
+
+ $this->assertEqual($deps[0]->department_id, 2);
+ $this->assertEqual($deps[1]->department_id, 4);
+ }
+
+ function test_find_by_composite_pks()
+ {
+ $ds = DepSections::finder()->findAllByPks(array(1,1), array(2,5));
+ $this->assertEqual(count($ds), 2);
+
+ $this->assertIsDepSection($ds[0], 1, 1);
+ $this->assertIsDepSection($ds[1], 2, 5);
+ }
+
+ function assertIsDepSection($dep, $dep_id, $sec_id)
+ {
+ $this->assertTrue($dep instanceof DepSections);
+ $this->assertEqual($dep->department_id, $dep_id);
+ $this->assertEqual($dep->section_id, $sec_id);
+ }
+}
+
+?> \ No newline at end of file
diff --git a/tests/test_tools/simpletest/errors.php b/tests/test_tools/simpletest/errors.php
index e2f6eb04..729883e9 100644
--- a/tests/test_tools/simpletest/errors.php
+++ b/tests/test_tools/simpletest/errors.php
@@ -150,7 +150,7 @@
E_COMPILE_WARNING => 'E_COMPILE_WARNING',
E_USER_ERROR => 'E_USER_ERROR',
E_USER_WARNING => 'E_USER_WARNING',
- E_USER_NOTICE => 'E_USER_NOTICE');
+ E_USER_NOTICE => 'E_USER_NOTICE', 4096 => 'E??');
return $map[$severity];
}
}