I have two tables (which happen to be in two different databases). "clients" and "domains", clients can have multiple domains.
This is the code i am using:
$this->Domain->find('all', array(
'order' => 'domain ASC',
'fields' => array(
'Domain.id',
'Domain.domain',
'Server.name',
'Client.id',
'Client.name'
)
));
When i return all the fields by not using the 'fields' => array() everything works fine, as soon as i ask for specific fields, it says:
SQL Error: 1054: Unknown column
'Client.id' in 'field list'
Everything also works fine if i just remove the two Client columns (The Client model is the only model which is on another database.
If clients hasMany domains, So your models should be called like $this->Domain->find('all'); explicitly passing fields Client.id will show error, as its not the part of domains table, enable sql dumping using debug=2 and see how queries are being run.
You models should be
// in client.php model - having structure - id, name
$hasMany = 'Domain';
// in domain.php model - having structure - id, name, client_id
$belongsTo = 'Client';
This should work like this
$this->Domain->recursive = 1;
$data = $this->Domain->find('all');
// $data = Array ( 'Domain' => ********, 'Client' => ****** )
If your two tables are in different databases, you're really making your life difficult. AFAIK, Cake doesn't support joining two tables (or enforcing relationships) between two different databases. Why do you have the client table in a separate db?
If you can't move your table, I think you're going to have to write some custom code inside your Domain model, so that it will use the (default) db connection string for domains, but will instantiate and connect another db resource to the other database. See http://bakery.cakephp.org/articles/doze/2010/03/12/use-multiple-databases-in-one-app-based-on-requested-url for how to do it -- skip ahead to the section entitled, "Select Correct Database Dynamically".
HTH,
Travis
Related
I need to make an import method that takes the CSV file and imports everything in the database.
I've done the parsing with one of Laravel's CSV addons and it works perfectly giving me a big array of values set as:
[
'col1_name' => 'col1 value',
'col2_name' => 'col2 value',
'col3_name' => 'col3 value,
'...' => '...'
]
This is also perfect since all the column names fit my model which makes the database inserts a breeze.
However - a lot of column values are strings that i'd like to set as separate tables/relations. For example, one column contains the name of the item manufacturer, and i have the manufacturer table set in my database.
My question is - what's the easy way to go through the imported CSV and swap the strings with the corresponding ID from the relationship table, making it compatible with my database design?
Something that would make the imported line:
[
'manufacturer' => 'Dell',
]
into:
[
'manufacturer' => '32',
]
I know i could just do a foreach loop comparing the needed values with values from the relationship models but I'm sure there's an easier and more clean way of doing it.
I don't think theres any "nice" way to do this - you'll need to look up each value for "manufacturer" - the question is, how many queries will you run to do so?
A consideration you need to make here is how many rows you will be importing from your CSV file.
You have a couple of options.
1) Querying 1 by 1
I'm assuming you're going to be looping through every line of the CSV file anyway, and then making a new model? In which case, you can add an extra database call in here;
$model->manufacturer_id = Manufacturer::whereName($colXValue)->first()->id;
(You'd obviously need to put in your own checks etc. here to make sure manufacturers exist)
This method is fine relatively small datsets, however, if you're importing lots and lots of rows, it might end up sluggish with alot of arguably unnecessary database calls.
2) Mapping ALL your Manufacturers
Another option would be to create a local map of all your Manufacturers before you loop through your CSV lines;
$mappedManufacturers = Manufacturer::all()->pluck('id', 'name');
This will make $mappedManufacturers an array of manufacturers that has name as a key, id as a value. This way, when you're building your model, you can do;
$model->manufacturer_id = $mappedManufacturers[$colXValue];
This method is also fine, unless you have tens of thousands of Manufacturers!
3) Where in - then re-looping
Another option would be to build up a list of manufacturer names when looping through your CSV lines, going to the database with 1 whereIn query and then re-looping through your models to populate the manufacturer ID.
So in your initial loop through your CSV, you can temporarily set a property to store the name of the manufacturer, whilst adding it to another array;
$models = collect();
$model->..... = ....;
$model->manufacturer = $colXValue;
$models->push($colXValue);
Then you'll end up with a collection of models. You then query the database for ONLY manufacturers which have appeared:
$manufacturers = Manufacturer::whereIn('name', $models->lists('manufacturer'))->get()->keyBy('name')->toArray();
This will give you array of manufacturers, keyed by their name.
You then loop through your $models collection again, assigning the correct manufacturer id using the map;
$model->manufacturer_id = $manufacturers[$model->manufacturer];
Hopefully this will give you some ideas of how you can achieve this. I'd say the solution mostly depends on your use case - if this was going to be a heavy duty ask - I'd definitely Queue it and be tempted to use Option 1! :P
I'm using Yii2's ActiveRecord implementation in (hopefully) exactly the way it should be used, according to the docs.
Problem
In a quite simple setup with simple relations betweens the tables, fetching 10 results is fast, 100 is slow. 1000 is impossible. The database is extremely small and indexed perfectly. The problem is definitly Yii2's way to request data, not the db itself.
I'm using a standard ActiveDataProvider like:
$provider = new ActiveDataProvider([
'query' => Post::find(),
'pagination' => false // to get all records
]);
What I suspect
Debugging with the Yii2 toolbar showed thousands of single SELECTs for a simple request that should just get 50 rows from table A with some simple "JOINs" to table B to table C. In plain SQL everybody would solve this with one SQL statement and two joins. Yii2 however fires a SELECT for every relation in every row (which makes sense to keep the ORM clean). Resulting in (more or less) 1 * 50 * 30 = 1500 queries for just getting two relations of each row.
Question
Why is Yii2 using so many single SELECTs, or is this a mistake on my side ?
Addionally, does anybody know how to "fix" this ?
As this is a very important issue for me I'll provide 500 bounty on May 14th.
By default, Yii2 uses lazy loading for better performance. The effect of this is that any relation is only fetched when you access it, hence the thousands of sql queries. You need to use eager loading. You can do this with \yii\db\ActiveQuery::with() which:
Specifies the relations with which this query should be performed
Say your relation is comments, the solution is as follows:
'query' => Post::find()->with('comments'),
From the guide for Relations, with will perform an extra query to get the relations i.e:
SELECT * FROM `post`;
SELECT * FROM `comment` WHERE `postid` IN (....);
To use proper joining, use joinWith with the eagerLoading parameter set to true instead:
This method allows you to reuse existing relation definitions to perform JOIN queries. Based on the definition of the specified relation(s), the method will append one or multiple JOIN statements to the current query.
So
'query' => Post::find()->joinWith('comments', true);
will result in the following queries:
SELECT `post`.* FROM `post` LEFT JOIN `comment` comments ON post.`id` = comments.`post_id`;
SELECT * FROM `comment` WHERE `postid` IN (....);
From #laslov's comment and https://github.com/yiisoft/yii2/issues/2379
it's important to realise that using joinWith() will not use the JOIN query to eagerly load the related data. For various reasons, even with the JOIN, the WHERE postid IN (...) query will still be executed to handle the eager loading. Thus, you should only use joinWith() when you specifically need a JOIN, e.g. to filter or order on one of the related table's columns
TLDR:
joinWith = with plus an actual JOIN (and therefore the ability to filter/order/group etc by one of the related columns)
In order to use relational AR, it is recommended that primary-foreign key constraints are declared for tables that need to be joined. The constraints will help to keep the consistency and integrity of the relational data.
Support for foreign key constraints varies in different DBMS. SQLite 3.6.19 or prior does not support foreign key constraints, but you can still declare the constraints when creating tables. MySQL’s MyISAM engine does not support foreign keys at all.
In AR, there are four types of relationships:
BELONGS_TO: if the relationship between table A and B is one-to-many, then B belongs to A (e.g. Post belongs to User);
HAS_MANY: if the relationship between table A and B is one-to-many, then A has many B (e.g. User has many Post);
HAS_ONE: this is special case of HAS_MANY where A has at most one B (e.g. User has at most one Profile);
MANY_MANY: this corresponds to the many-to-many relationship in database. An associative table is needed to break a many-to-many relationship into one-to-many relationships, as most DBMS do not support many-to-many relationship directly. In our example database schema, the tbl_post_category serves for this purpose. In AR terminology, we can explain MANY_MANY as the combination of BELONGS_TO and HAS_MANY. For example, Post belongs to many Category and Category has many Post.
The following code shows how we declare the relationships for the User and Post classes.
class Post extends CActiveRecord
{
......
public function relations()
{
return array(
'author'=>array(self::BELONGS_TO, 'User', 'author_id'),
'categories'=>array(self::MANY_MANY, 'Category',
'tbl_post_category(post_id, category_id)'),
);
}
}
class User extends CActiveRecord
{
......
public function relations()
{
return array(
'posts'=>array(self::HAS_MANY, 'Post', 'author_id'),
'profile'=>array(self::HAS_ONE, 'Profile', 'owner_id'),
);
}
}
The query result will be saved to the property as instance(s) of the related AR class. This is known as the lazy loading approach, i.e., the relational query is performed only when the related objects are initially accessed. The example below shows how to use this approach:
// retrieve the post whose ID is 10
$post=Post::model()->findByPk(10);
// retrieve the post's author: a relational query will be performed here
$author=$post->author;
You are somehow doing it the wrong please go through from the documentaion here http://www.yiiframework.com/doc/guide/1.1/en/database.arr
This is a two part question. I found something similar, but it was more complicated with more tables and used joins making it much more complex and difficult to translate to my more simplified situation. Also, it doesn't cover the second part of my question.
This takes place in my OrdersController. I have also have ArchivedordersControler and ArchivedOrder model
I'm trying to search for an email address in two different tables(orders and archived_orders). I don't need to join anything (at least I don't think I have to). Both tables have the exact same structure, one is just for archived values.
With MySQL I'd just do something like
select * from orders where orders.email = '$email'
Union
select * from archived_orders where archived_orders.email = '$email'
How can I add some sort of identifier to know which table it was selected from? The email can appear in both tables but the options displayed based on which table it was pulled from will be different.
You'll want to create a Model for both tables, Order and ArchivedOrder. That way you can easily find the data you are looking for in both tables by:
// From OrdersController
$this->Order->find('first', array('conditions' => array('email' => $email)));
// From ArchivedOrdersController
$this->ArchivedOrder->find('first', array('conditions' => array('email' => $email)));
If you want to fetch the archived data from the original OrdersController, you can load the model from there as well, like:
$this->loadModel('ArchivedOrder');
$this->ArchivedOrder->find('first', array('conditions' => array('email' => $email)));
That way, you don't need a separate controller for it. It will return the data as an array that looks like:
array(
'Order' => array(
'id' => 12,
'email' => 'customer#example.com'
// And other data...
)
)
So from the Order you can tell it was selected from the original table. Otherwise, it will be ArchivedOrder.
convert following query into cakephpquery.
"SELECT * FROM user1.user_favourites,esl.esl_lyrics
WHERE
esl_lyrics.id=user_favourites.fav_recordID
AND user_favourites.fav_userID=".$user_id."
AND user_favourites.fav_widgetID=$wid_id";
Models files are esl.php and userFavourite.php
DB are user1 and esl.
DB tables are user_favourites in user1 and esl_lyrics in esl DB.
plz give details.what changes are do in esl.php and userFavourite.php
please Help me...
It will be difficult to determine what to write until you can provide a data map for the various IDs you are using. Since you are not conforming to standards in your SQL, the fields are all named different in the various tables. But here is what I understand so far:
esl_lyrics.id = user_favorites.fav_recordID
From your description there are two models. So you will need to make sure you have relationships between the two. This will require that you determine if it is belongsTo, hasOne, hasMany, etc.
It also appears you are using multiple databases (schemas), so you will need to configure the database.php so that you can access each one.
Once everything is configured you should be able to access the data as:
$this->Model1->Model2->find('all', array('conditions' => array('Model1.id' => 'Model2.id', 'Model2.user_id' => $user_id, 'Model2.widgetID' => $wid_id)));
It will return an array with the data from both models. But until you can share what your models look like and a mapping of ids etc., this will be as good of an answer as you will get.
I have two models, called Book and Tag, which are in a HABTM relationship. I want a couple (book, tag) to be saved only once. In my models I have
var $hasAndBelongsToMany = array(
'Tag' => array(
'className' => 'Tag',
'joinTable' => 'books_tags',
'foreignKey' => 'book_id',
'associationForeignKey' => 'tag_id',
'unique' => true
)
);
and viceversa, but the Unique flag does not help me; I can still save two times the same couple.
How do I do this in CakePHP? Should I declare the couple (book, tag) unique in the database directly, or will this make CakePHP go nuts? Is there a Cakey way to handle this situation?
EDIT: I tried making the couple unique with the query (I'm using MySQL)
ALTER TABLE books_tags ADD UNIQUE (book_id,tag_id);
but this does not work well. When I save more than one tag at a time, everything goes well if all the couples are new. If at least one of the couples is repeated, CakePHP fails to do the whole operation, so it does not save ANY new couple (not even the good ones).
The way Cake usually saves HABTM data is by erasing all existing data for the model from the HABTM table and inserting all the relationships anew from the data array you feed into save(). If you're exclusively using this way to deal with HABTM relationship, you shouldn't have a problem.
Of course, that's not always the ideal way. To enforce validation on the HABTM table, you need to create the model for it and add some custom validation, like this:
class BooksTag extends AppModel {
var $validate = array(
'book_id' => array('rule' => 'uniqueCombi'),
'tag_id' => array('rule' => 'uniqueCombi')
);
function uniqueCombi() {
$combi = array(
"{$this->alias}.book_id" => $this->data[$this->alias]['book_id'],
"{$this->alias}.tag_id" => $this->data[$this->alias]['tag_id']
);
return $this->isUnique($combi, false);
}
}
Just make sure to save data using saveAll($data, array('validate' => 'first')) to trigger the validation.
You may have to specify the BooksTag model explicitly in the relationship:
var $hasAndBelongsToMany = array(
'Tag' => array(
...
'with' => 'BooksTag'
)
);
If on top of that you're also enforcing the uniqueness on the DB level, all the better.
In my opinion, any time you have data requirements, those requirements should be specified at the data(base) level. Enforce your rules at the lowest possible level. If the application can enforce those rules as well, and you choose to do so, then I'd say it can't hurt. I'd advise against using an application as the sole means of enforcing data constraints, though.
I would recommend that you put the constraint in the database schema. It would be easy if you could declare the combined columns as the primary key although I think this would cause cake some problems.
Depending on which RDBMS you use, you may be able to create a combined row index and declare a unique constraint on that.
You other option, for keeping this logic in Cake, would be to alter the beforeSave() function in the Book model, so that it first runs a find() on the passed data, and will only save if the find returns false (meaning there is no previous pair, meaning you've fulfilled your unique constraint).
One option that I don't promise will work, but looks like it might, would be the CakePHP Counter Cache behavior. Not sure it works by default with HABTM, but here is the link to the cake book (http://book.cakephp.org/view/816/counterCache-Cache-your-count) and the link to the addon that will make it work with habtm (http://bakery.cakephp.org/articles/view/counter-cache-behavior-for-habtm-relations)
Hackish way:
If you can have a third DB field (like the one above) do a varchar(32) that is UNIQUE that is an MD5 hash of the first two.
You'd have to modify all saves to be $md5_unique_field = md5( $field_one.$field_two );
CakePHP might allow for a custom validation or a custom model extension that could do this automatically.
I agree that this should be enforced at the DB level also though, and CakePHP should catch the errors.