I have a MySQL database and a table tobjects where each record has its id, parameter, value (something like XML) and one can say that this parameter column determines the "type" of an object.
The objects are used in some other tables, depending on their types, so each of them should be handled in specific way.
Because "handling" is somewhat common (I use the same function) I created a TObject class (not abstract but could be) from which I inherit other classes; this inheritance method is very useful and that's the very reason I use object oriented programming. For example TObject has retrieve() method that gets from db all the necessary data, not those in tobjects table but others too, which are type dependent, so I override it in some classes.
The problem I encountered is that when I create an object I do not know what class should it be. Of course, I can SELECT Parameter FROM tobjects WHERE id=$id, and then (with switch) create object of the proper class, and use its retrieve() method (each class retrieves different data, only those from tobjects are common) to get data from the db, that causes me to run query two times and some part of work outside the class, which works, but is not gentle.
The best solution would be if I can create a TObject and then, upon retrieving, change the class of the object to the one I need and it would be TObject's descendant, but I'm almost sure it's not possible.
Is my solution, that I run the first query just to select one field from tobjects only to determine object's class right? Or is there a trick to change object's class in runtime?
If understand what you are doing correctly, here is the way I would approach this:
Passing PDO::FETCH_CLASS | PDO::FETCH_CLASSTYPE to the first argument of PDOStatement::fetch() will return an object of class PDOStatement::fetchColumn(0) - in other words, it determines the class name to instantiate from the value of the first column of the result set.
To leverage this, you would JOIN tobjects ON targetTable.objectType = tobjects.id and select tobjects.Parameter as the first column in the result set. If the Parameter column already holds a 1:1 mapping of database object types to class names, this is all you need to do, however I'm not sure whether this is the case, and it probably shouldn't be, because it makes it more difficult to substitute another class at a later date.
To overcome this limitation, I suggest you create a temporary table when you first connect the database, which maps Parameter values to class names, which you can JOIN onto the query to obtain the target class name.
So the flow would go something like this:
// Set up the connection
$db = new PDO('mysql:yourDSNhere');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
// Create a temp table to store the mapping
$db->query("
CREATE TEMPORARY TABLE `objectMappings` (
`Parameter` INT NOT NULL PRIMARY KEY,
`ClassName` VARCHAR(255)
) ENGINE=MEMORY
");
// A mapping of Parameter IDs to class names
$classMap = array(
1 => 'Class1',
2 => 'Class2',
3 => 'Class3',
// ...
);
// Build a query string and insert
$rows = array();
foreach ($classMap as $paramId => $className) {
// this data is hard-coded so it shouldn't need further sanitization
$rows[] = "($paramId, '$className')";
}
$db->query("
INSERT INTO `objectMappings`
(`Parameter`, `ClassName`)
VALUES
".implode(',
', $rows)."
");
// ...
// When you want to retrieve some data
$result = $db->query("
SELECT m.ClassName, t.*
FROM targetTable t
JOIN tobjects o ON t.objectType = o.id
JOIN objectMappings m ON o.Parameter = m.Parameter
WHERE t.someCol = 'some value'
");
while ($obj = $result->fetch(PDO::FETCH_CLASS | PDO::FETCH_CLASSTYPE)) {
// $obj now has the correct type, do stuff with it here
}
Related
I am using PHP Yii framework's Active Records to model a relation between two tables. The join involves a column and a literal, and could match 2+ rows but must be limited to only ever return 1 row.
I'm using Yii version 1.1.13, and MySQL 5.1.something.
My problem isn't the SQL, but how to configure the Yii model classes to work in all cases. I can get the classes to work sometimes (simple eager loading) but not always (never for lazy loading).
First I will describe the database. Then the goal. Then I will include examples of code I've tried and why it failed.
Sorry for the length, this is complex and examples are necessary.
The database:
TABLE sites
columns:
id INT
name VARCHAR
type VARCHAR
rows:
id name type
-- ------- -----
1 Site A foo
2 Site B bar
3 Site C bar
TABLE field_options
columns:
id INT
field VARCHAR
option_value VARCHAR
option_label VARCHAR
rows:
id field option_value option_label
-- ----------- ------------- -------------
1 sites.type foo Foo Style Site
2 sites.type bar Bar-Like Site
3 sites.type bar Bar Site
So sites has an informal a reference to field_options where:
field_options.field = 'sites.type' and
field_options.option_value = sites.type
The goal:
The goal is for sites to look up the relevant field_options.option_label to go with its type value. If there happens to be more than one matching row, pick only one (any one, doesn't matter which).
Using SQL this is easy, I can do it 2 ways:
I can join using a subquery:
SELECT
sites.id,
f1.option_label AS type_label
FROM sites
LEFT JOIN field_options AS f1 ON f1.id = (
SELECT id FROM field_options
WHERE
field_options.field = 'sites.type'
AND field_options.option_value = sites.type
LIMIT 1
)
Or I can use a subquery as a column reference in the select clause:
SELECT
sites.id,
(
SELECT id FROM field_options
WHERE
field_options.field = 'sites.type'
AND field_options.option_value = sites.type
LIMIT 1
) AS type_label
FROM sites
Either way works great. So how do I model this in Yii??
What I've tried so far:
1. Use "on" array key in relation
I can get a simple eager lookup to work with this code:
class Sites extends CActiveRecord
{
...
public function relations()
{
return array(
'type_option' => array(
self::BELONGS_TO,
'FieldOptions', // that's the class for field_options
'', // no normal foreign key
'on' => "type_option.id = (SELECT id FROM field_options WHERE field = 'sites.type' AND option_value = t.type LIMIT 1)",
),
);
}
}
This works when I load a set of Sites objects and force it to eager load type_label, e.g. Sites::model()->with('type_label')->findByPk(1).
It does not work if type_label is lazy-loaded.
$site = Sites::model()->findByPk(1);
$label = $site->type_option->option_label; // ERROR: column t.type doesn't exist
2. Force eager loading always
Building on #1 above, I tried forcing Yii to always to eager loading, never lazy loading:
class Sites extends CActiveRecord
{
public function relations()
{
....
}
public function defaultScope()
{
return array(
'with' => array( 'type_option' ),
);
}
}
Now everything always works when I load Sites, but it's no good because there are other models (not pictured here) that have relations that point to Sites, and those result in errors:
$site = Sites::model()->findByPk(1);
$label = $site->type_option->option_label; // works now
$other = OtherModel::model()->with('site_relation')->findByPk(1); // ERROR: column t.type doesn't exist, because 't' refers to OtherModel now
3. Make the reference to the base table somehow relative
If there was a way that I could refer to the base table, other than "t", that was guaranteed to point to the correct alias, that would work, e.g.
'on' => "type_option.id = (SELECT id FROM field_options WHERE field = 'sites.type' AND option_value = %%BASE_TABLE%%.type LIMIT 1)",
where %%BASE_TABLE%% always refers to the correct alias for table sites. But I know of no such token.
4. Add a true virtual database column
This way would be the best, if I could convince Yii that the table has an extra column, which should be loaded just like every other column, except the SQL is a subquery -- that would be awesome. But again, I don't see any way to mess with the column list, it's all done automatically.
So, after all that... does anyone have any ideas?
EDIT Mar 21/15: I just spent a long time investigating the possibility of subclassing parts of Yii to get the job done. No luck.
I tried creating a new type of relation based on BELONGS_TO (class CBelongsToRelation), to see if I could somehow add in context sensitivity so it could react differently depending on whether it was being lazy-loaded or not. But Yii isn't built that way. There is no place where I can hook in code during query buiding from inside a relation object. And there is also no way I can tell even what the base class is, relation objects have no link back to the parent model.
All of the code that assembles these queries for active records and their relations is locked up in a separate set of classes (CActiveFinder, CJoinQuery, etc.) that cannot be extended or replaced without replacing the entire AR system pretty much. So that's out.
I then tried to see if I can create "fake" database column entries that would actually be a subquery. Answer: no. I figured out how I could add additional columns to Yii's automatically generated schema data. But,
a) there's no way to define a column in such a way that it can be a derived value, Yii assumes it's a column name in way too many places for that; and
b) there also doesn't appear to be any way to avoid having it try to insert/update to those columns on save.
So it really is looking like Yii (1.x) just does not have any way to make this happen.
Limited solution provided by #eggyal in comments: #eggyal has a suggestion that will meet my needs. He suggests creating a MySQL view table to add extra columns for each label, using a subquery to look up the value. To allow editing, the view would have to be tied to a separate Yii class, so the downside is everywhere in my code I need to be aware of whether I'm loading a record for reading only (must use the view's class) or read/write (must use the base table's class, does not have the extra columns). That said, it is a workable solution for my particular case, maybe even the only solution -- although not an answer to this question as written, so I'm not going to put it in as an answer.
OK, after a lot of attempts, I have found a solution. Thanks to #eggyal for making me think about database views.
As a quick recap, my goal was:
link one Yii model (CActiveRecord) to another using a relation()
the table join is complex and could match more than one row
the relation must never join more than one row (i.e. LIMIT 1)
I got it to work by:
creating a view from the field_options base table, using SQL GROUP BY to eliminate duplicate rows
creating a separate Yii model (CActiveRecord class) for the view
using the new model/view for the relation(), not the original table
Even then there were some wrinkles (maybe a Yii bug?) I had to work around.
Here are all the details:
The SQL view:
CREATE VIEW field_options_distinct AS
SELECT
field,
option_value,
option_label
FROM
field_options
GROUP BY
field,
option_value
;
This view contains only the columns I care about, and only ever one row per field/option_value pair.
The Yii model class:
class FieldOptionsDistinct extends CActiveRecord
{
public function tableName()
{
return 'field_options_distinct'; // the view
}
/*
I found I needed the following to override Yii's default table data.
The view doesn't have a primary key, and that confused Yii's AR finding system
and resulted in a PHP "invalid foreach()" error.
So the code below works around it by diving into the Yii table metadata object
and manually setting the primary key column list.
*/
private $bMetaDataSet = FALSE;
public function getMetaData()
{
$oMetaData = parent::getMetaData();
if (!$this->bMetaDataSet) {
$oMetaData->tableSchema->primaryKey = array( 'field', 'option_value' );
$this->bMetaDataSet = TRUE;
}
return $oMetaData;
}
}
The Yii relation():
class Sites extends CActiveRecord
{
// ...
public function relations()
{
return (
'type_option' => array(
self::BELONGS_TO,
'FieldOptionsDistinct',
array(
'type' => 'option_value',
),
'on' => "type_option.field = 'sites.type'",
),
);
}
}
And all that does the trick. Easy, right?!?
I have model A and model B which lie in two different databases.
Now I have a pivot_table called a_bs in the same database as model A.
I've setup the belongsToMany relatinoship like this in model A
public function bs()
{
return $this->belongsToMany('B', 'a_bs', 'a_id', 'b_id');
}
When I try to access this relationship like so:
$a = A::find($id);
print_r($a->bs->lists('id'));
I get an error that my pivot table doesn't exist in model B's database. Which is obviously correct since the pivot table is in model A's database. How can I let Laravel know that?
Do not suggest to put the pivot table in model B's database
Very simply:
public function bs()
{
$database = $this->getConnection()->getDatabaseName();
return $this->belongsToMany('B', "$database.a_bs", 'a_id', 'b_id');
}
I'm obtaining the database name dynamically because my connection is configured based off an environment variable. Laravel seems to assume the pivot table to exist in the same database as the target relation, so this will force it to look instead to the database corresponding to the model that this method is in, your 'A' realm.
If you're not worried about SQLite databases, i.e. in the scope of a unit-test, that's all you need. But if you are, keep reading.
Firstly, the previous example isn't sufficient on its own. The value of $database would end up being a file-path, so you need to alias it to something that won't break an SQL statement, and make it accessible to the current connection. "ATTACH DATABASE '$database' AS $name" is how you do that:
public function bs()
{
$database = $this->getConnection()->getDatabaseName();
if (is_file($database)) {
$connection = app('B')->getConnection()->getName();
$name = $this->getConnection()->getName();
\Illuminate\Support\Facades\DB::connection($connection)->statement("ATTACH DATABASE '$database' AS $name");
$database = $name;
}
return $this->belongsToMany('B', "$database.a_bs", 'a_id', 'b_id');
}
Warning: Transactions muck this up: If the current connection is using transactions, the ATTACH DATABASE statement will fail. You can use transactions on it after executing that statement though.
Whereas, if the related connection uses transactions, the resulting data will be silently rendered invisible to the current one. This drove me nuts for longer than I'd care to admit, because my queries ran without error, but kept coming up empty. It seems only data truly written to the attached database is actually accessible to the one it's attached to.
So, after being forced to write to your attached database, you may still want your test to clean up after itself. A simple solution there would be to just use $this->artisan('migrate:rollback', ['--database' => $attachedConnectionName]);. But if you have multiple tests that need the same tables, this is not very efficient, as it forces them to have to rebuild them each time.
A better option would be to truncate the tables, but leave their structure in tact:
//Get all tables within the attached database
collect(DB::connection($database)->select("SELECT name FROM sqlite_master WHERE type = 'table'"))->each(function ($table) use ($name) {
//Clear all entries for the table
DB::connection($database)->delete("DELETE FROM '$table->name'");
//Reset any auto-incremented index value
DB::connection($database)->delete("DELETE FROM sqlite_sequence WHERE name = '$table->name'");
});
}
This will wipe all data from that connection, but there's no reason you couldn't apply some kind filter to that however you see fit. Alternatively, you could take advantage of the fact that SQLite DBs are easily-accessible files, and just copy the attached one to a temp file, and use it to overwrite the source after the test is done executing. The result would be functionally identical to a transaction.
You can set the database of the table in the model class:
protected $table = 'A.a_s';
And You have to use singular form when create a pivot table.
/app/model/A.php
class A extends Eloquent {
// Set table name (plural) with database name
protected $table = 'A.a_s';
// Many to many relation
public function b_s() {
return $this->belongsToMany('B');
}
}
/app/model/B.php
class B extends Eloquent {
// Set table name (plural) with database name
protected $table = 'B.b_s';
}
Query
print_r(A::with('b_s')->where('id', 1)->get()->toArray());
MySQL
CREATE TABLE `A`.`a_s` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
);
CREATE TABLE `B`.`b_s` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
);
CREATE TABLE `A`.`a_b` (
`a_id` int(10) unsigned NOT NULL,
`b_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`a_id`,`b_id`)
) ENGINE=InnoDB;
INSERT INTO A.a_s VALUES (NULL);
INSERT INTO A.a_s VALUES (NULL);
INSERT INTO B.b_s VALUES (NULL);
INSERT INTO A.a_b VALUES (1,1);
INSERT INTO A.a_b VALUES (1,2);
If the databases/schemas are on the same host server like #NiRR said just do this to override the default schema of the second connection:
return $this->belongsToMany('B', 'real-schema-name.a_bs');
or alternatively
return $this->belongsToMany('A', 'real-schema-name.a_bs');
Depending on which model (A or B) is defined with the connection that isn't using the default schema.
Remember that it's impossible to do a join query that spans across two servers; which server is it going to get executed on? Each is missing some of the needed data needed to preform the request.
This is only possible if the two databases are on the same connection (server).
Its not possible to do this on two different connections (servers) since you'll need all three tables on a single server that will perform the join command.
Explicitly setting or defining the connection for both models works just fine.
protected $connection = 'connection1'; //inside model A
protected $connection = 'connection2'; //inside model B
EDIT
When Laravel is fetching a model from the DB, provided a database/connection has been defined (as a property) within the model, Laravel will use the database name for that connection when constructing the SQL. So when working with multiple connections, it is best to define the connection for every model.
let me at first state that we use php and postgre database. In our project we have decided not to use any ORM due to its overload of sql queries and we are taking the oposite way.
Imagine you have a select from several tables, lets say joined on id columns. For instance:
tables: users(id, name), items(id, name, description), comments(user_id, item_id, text, rating)
So basically you have a table of users, a table of some items and a table of comments which are related to one user and one item.
You create two objects - user and item representing their table row. And then you want to create a comment object. In an ORM it would contain objects user and item and they would load themselves with their queries, but that would be two queries and you re thinking...hm but I can select that data with a single query...but how?
Imagine that you have this select:
SELECT * FROM comments JOIN users ON comments.user_id = users.id JOIN items ON comments.item_id = items.id
(you can also imagine a WHERE clause with specified item id or user id etc.)
So how would you split the result of such a select into this class structure, lets say you want a list of comment objects:
user
item
comment (contains references to user and item object)
So far our theoretical solution was to prefix name of the columns with fixed prefixes :) and then propagating the result into the object structure and each objects takes what it needs from the select. Any other solutions? Lets say more sophisticated?
Thanks for any ideas
PS: obviously I have used a very simple example, but try to imagine that the problem is far larger and the structure far more complex
First of all, you might benefit from looking at the Data Mapper pattern. A simple use-case with would look like this:
$user = new User;
$mapper = new UserMapper( $db );
$user->setName('foobar');
$mapper->fetch( $user );
if ( $user->isBanned() )
{
throw new Exception('get out !');
}
$user->setLastActive( time() );
$mapper->store( $user );
As for the single query with data: that's not the important part. You just ALIAS it as required (oh .. and i hope you are not using the * for selecting rows). The important bit is creating an object graph from selected data. That where you use builders/factories.
//the rest of PDO-related code
$data = $statement->fecth(PDO::FETCH_ASSOC);
$comment = $commentFactory->build($data);
Where $commentFactory is instance of CommentFactory:
class CommentFactory
{
public function build( $params )
{
$author = new User;
$subject = new Item;
$comment = new Comment( $author, $subject );
$author->setId( $params['user_id']);
$author->setName( $params['user_name']);
$subject->setId( $param['item_id']);
$comment->setContent( $param['content']);
return $comment;
}
}
Additionally with setup like this, you can easily change how $comment is made, just by changing what class is the $commentFactory an instance of.
The following code will return an array of PHP Activerecord Objects:
$book = Book::find('all');
Assuming the program is aware of the order of books I can continue and update the attributes of the books and save them to the database as follows:
$book[0]->title = 'my first book';
$book[0]->author = 'Danny DeVito';
$book[4]->title = 'Nice Title';
in order to save the above I would have to invoke the ->save() method on each object
$book[0]->save();
$book[4]->save();
Is there a better way to do this? built-in PHP ActiveRecord function
that saves all members of a given array of objects, or based on an
association?
Assuming the original title of $book[4] above was already 'Nice
Title', would the ->save() method consider $book[4]changed and
continue with the database save?
Try using update all insted
$update = array();
$update['title'] = 'my first book';
$update['author'] = 'Danny DeVito' ;
$book[0]->update_all(array('set' =>$update));
$book[4]->update_all(array('set' =>array("title"=>"Nice Title"));
I think this should be cleaner
After much research I decided to post my conclusions/answers:
There is no such ActiveRecord library function that can update an
array of objects with unique values.
Assuming Activerecord would shoot one update request it would look like this:
UPDATE books
SET title = CASE id
WHEN 0 THEN 'my first book'
WHEN 4 THEN 'Nice Title'
END,
author = CASE id
WHEN 0 THEN 'Danny DeVito'
END
WHERE id IN (0,4)
The same question as "how would I update multiple rows with different values at once". This would go against the design of an Activerecord model, as an Object represents a row, and maps rows across tables. An obvious limitation for having such an easy model to work with.
Any assignment to an Object's attributes triggers a 'dirty' flag on
that attribute, and any subsequent call to update/save that
object will trigger a query even if the assigned attribute value is
the same as the database/model's previous value. Invoking the
save() method when no assignments were made does not trigger this
query.
I am wondering about using the abstraction in Zend Db RowSet instead of joins, is it possible
for instance I am able to get some info from parent table as in here
/**
* Get default photo info (path , description)
*/
public function getDefaultPhotoInfo($userdId){
$select = $this->select($this)
->where('id=?', $userdId);
$rowset = $this->fetchAll($select);
$current = $rowset->current();
$res = $current->findParentRow('UserPhotos', 'Avatar');
if(isset($res)){
return $res->toArray();
}
}
How can I use Rowset abstraction to get this logic working
table( id, pic_path,) table_translation(id, table_id, lang_id, pic_title);
the above is representation of two tables , the idea is to get the info from both table specifying the lang_id , it is easy with joins but can I do it with the abstraction of Db Rowset ?
Just for clarification: when doing fetchAll on a Zend_Db_Table instance, you get a Zend_Db_Table_Rowset, which implements the Iterator interface. Thus, calling current() on the Rowset instance, will return a Zend_Db_Table_Row instance.
As of ZF1.10, you define relationships between tables in a Zend_Db_Table_Defintion instance or on a concrete table instance like described in the reference guide for Zend_Db_Table Relationships. Since the guide is rather detailed, I won't reproduce this here.
Once you defined relationships, you can fetch them from a row with (example 1 from guide)
$accountsTable = new Accounts();
$accountsRowset = $accountsTable->find(1234);
$user1234 = $accountsRowset->current();
$bugsReportedByUser = $user1234->findDependentRowset('Bugs');
or by the magic finder methods.
The findParentRow() method is somewhat different to that, as it return the full row of a dependent rowset from it's parent row.
Ex5: This example shows getting a Row object from the table Bugs (for example one of those bugs with status 'NEW'), and finding the row in the Accounts table for the user who reported the bug.
$bugsTable = new Bugs();
$bugsRowset = $bugsTable->fetchAll(array('bug_status = ?' => 'NEW'));
$bug1 = $bugsRowset->current();
$reporter = $bug1->findParentRow('Accounts');
When using table relations, keep in mind that these will result in one additional query per fetched dependent table, whereas a Join does it all in one.
Also see this related questions:
Modeling objects with multiple table relationships in Zend Framework