Deeper database relationships

[b]Hello,

I have a question regarding the MANY_MANY relationships inside Yii.

How do i get a result from $usergroups->i18n->groupname inside the following situation:[/b]

I have 4 database tables: (core_usergroups, core_usergroups_i18n, core_users, core_users_usergroups)

[sql]

// Table core_usergroups

CREATE TABLE IF NOT EXISTS core_usergroups (

id smallint(5) unsigned NOT NULL AUTO_INCREMENT,

module varchar(50) COLLATE utf8_unicode_ci NOT NULL,

name varchar(40) COLLATE utf8_unicode_ci NOT NULL,

PRIMARY KEY (id),

KEY name (name),

KEY module (module)

) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=5 ;

// Table core_usergroups_i18n

CREATE TABLE IF NOT EXISTS core_usergroups_i18n (

usergroup varchar(40) COLLATE utf8_unicode_ci NOT NULL,

language char(2) COLLATE utf8_unicode_ci NOT NULL,

groupname varchar(40) COLLATE utf8_unicode_ci NOT NULL,

description varchar(400) COLLATE utf8_unicode_ci NOT NULL,

KEY usergroup (usergroup),

KEY language (language)

) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

// Table core_users

CREATE TABLE IF NOT EXISTS core_users (

id int(11) unsigned NOT NULL AUTO_INCREMENT,

username varchar(15) COLLATE utf8_unicode_ci NOT NULL,

password varchar(40) COLLATE utf8_unicode_ci NOT NULL,

email varchar(200) COLLATE utf8_unicode_ci NOT NULL,

birthdate date DEFAULT NULL,

PRIMARY KEY (id),

KEY username (username),

KEY email (email)

) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=3 ;

// Table core_users_usergroups

CREATE TABLE IF NOT EXISTS core_users_usergroups (

user varchar(15) COLLATE utf8_unicode_ci NOT NULL,

usergroup varchar(40) COLLATE utf8_unicode_ci NOT NULL,

KEY user (user),

KEY usergroup (usergroup)

) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

[/sql]

[size=“1”]I don’t think i have to explain what the relationships are… but in short, the usergroups_i18n table has groupnames that can be displayed. So if the application is set to dutch, the language column will be nl and the groupname for group guest will be gast[/size]

Now the real question comes… I want to show the groupnames of each group that the user is member of, using the core_usergroups_i18n table on the users list page

As far as i know i have to do this inside the Users Model:




public function relations()

	{

		return array(

			'usergroups' => array(self::MANY_MANY,'Usergroups','core_users_usergroups(user,usergroup)',

				'with'=>'i18n'

			),

		);

	}



And this inside the Usergroups Model




public function relations()

	{

		return array(

			'i18n' => array(self::HAS_ONE, 'UsergroupsI18n', 'usergroup', 'condition'=>'language="en"'),

			'users' => array(self::MANY_MANY, 'Users', 'core_users_usergroups(user,username)'),

		);

	}



But this throws me a [color="#FF0000"]"Invalid argument supplied for foreach()"[/color] when i try to display the i18n groupname inside a view:




<?php foreach($users as $user): ?>

	Usergroups: 

	<?php foreach($user->usergroups as $key => $usergroup): ?>

	<?php echo CHtml::encode($usergroup->i18n->groupname); ?>

	<?php endforeach; ?>

<?php endforeach; ?>



Is this to hard?.. sorry for the bump but i can’t do anything for days now…

How do i get the groupname from the i18n table…

this should be the $users array




$users = array(

	0 => array(

		'id' => 1,

		'username' => 'nique',

		'email' => 'niquesemail@domain.com',

		'usergroups' => array(

				0 => array(

					'name' => 'administrators' // this is indexed

					'i18n' => array(

						'usergroup' => 'administrators', // Refers to the indexed name)

						'language' => 'en',

						'groupname' => 'Administrator',

						'description' => 'This is the administrators i18n description'

					)

				)

			)			

		)

	),

	1 => array(

		'id' => 2,

		'username' => 'dirk',

		'email' => 'dirksemail@domain.com',

		'usergroups' => array(

				0 => array(

					'name' => 'members' // this is indexed

					'i18n' => array(

						'usergroup' => 'members', // Refers to the indexed name)

						'language' => 'en',

						'groupname' => 'Member',

						'description' => 'This is the members i18n description'

					)

				)

			)			

		)

	)

);



and so on…




<?php foreach($users as $rowKey => $user): ?>

	Username: <?php echo $user->username; ?>			// Outputs Nique or Dirk

	Email: <?php echo $user->email; ?>				// Outputs niquesemail@domain.com or dirksemail@domain.com

	Usergroups:							// Outputs the usergroups table (with the i18n table included for language support) 

	<?php foreach($user->usergroups as $key => $usergroup): ?>

	<?php echo CHtml::encode($usergroup->i18n->groupname); ?>	// The i18n Groupname

	<?php endforeach; ?>

<?php endforeach; ?>



I hope this makes a lot clear. The array gets its information from a total of 4 tables:

  • users (#1),

  • usergroups (#2) via (the referer table) users_usergroups (#3)

  • usergroups_i18n (#4) via the usergroups table for language support.

I want this all with ONE query…

The error is hard to pinpoint with the information you’re giving.

Is that the exact line where the error occurs, or is it somewhere in the CActiveRecord class? You can enable debugging to see the context and stacktrace of the error. Enable DB profiling and check out the queries that are executed. Try:


CVarDumper::dump($user->usergroups);

to see what’s in that attribute. If the error does indeed happen in that foreach line in your view code, then odds are $user->usergroups is not an array. Maybe that’ll get you started :)

Hi Sander… thanks,

It happens in the CActiveRecord (CActiveFinder) class.

(i’ll dump the error here)

I can’t dump any output, because immediately after i add ‘with’=>‘i18n’ in the relations method (in the usergroups model) i get the above error.

OK, looking at the context it seems like there’s a problem with your primary keys. If I look at the tables in your first post, I notice that in core_users_usergroups, the usergroup is referenced by name (varchar 40), but in the core_usergroups table, the primary key is id (an integer). Same goes for user, it has an integer ID buut you reference it by its name in the core_users_usergroups table. I’m pretty sure you need to reference the actual primary keys of the user and the usergroup in the connector-table of a MANY_MANY relationship.

Does that help?

EDIT – So basically make your core_users_usergroups table look something like this:

[sql]

CREATE TABLE IF NOT EXISTS core_users_usergroups (

user int(11) unsigned NOT NULL,

usergroup smallint(5) unsigned NOT NULL,

KEY user (user),

KEY usergroup (usergroup)

) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

[/sql]

And reference them by id instead of by name.

Oh, well i did this relational settings with innoDB for the first time inside my database. This should work right?, it works with the users_usergroups table refering to users (core_users.username is indexed as unique, every user has its primary key as an integer one).

I’ll try your approach now:

I changed the tables: (when it goes red, it goes wrong…)

[color="#9ACD32"]core_users.id = primary

core_users_usergroups.user = related to core_users.id (user = many_many)

core_users_usergroups.usergroup = related to core_usergroups.id (usergroup = has_many)

core_usergroups.id = primary[/color]

[color="#8B0000"]core_usergroups_i18n.id = related to core_usergroups.id[/color]

Reaction:

Everything goes fine till the usergroups model tries to get the i18n table to join.

The users model is including the usergroups table, and the usergroups table is including the usergroups_i18n table. It seems that i cant include a 3 level deep table…

[size="3"][edit 1][/size]

I created a crud on the core_usergroups model

when i tried to include the core_usergroups_i18n table, i got the same error. This is going somewhere…

[size="3"][edit 2][/size]

When i set column ‘usergroup’ as PRIMARY KEY… everything works ok… but well… what if i just want more usergroups having different languages.

PK | LANG

0 | nl | Nederlandse omschrijving

0 | en | English description

0 | de | Deutsche bezeichnung

wel never ever work … PK has to be unique… great

[size="3"][edit 3][/size]

Ok, this is soooo weird…

Why do i have to declare a primary key… !? i dont want a freakin primary key. So, then yii tells me to load it using the many_many relationship but i dont want that either… i dont want to use another table just to get the right language out of the reference table.

usergroups

usergroups_i18n

how do i get usergroups from the database with usergroups_i18n joined … without using a completely idiot many_many relationship table? I allready know what i want, i just want ONE row from the usergroups_i18n table with the information i got from the usergroups table and the language that is declared in the application (‘en’).

How do i let it join with the usergroups table in one query? Attention: the usergroups_i18n HAS more rows with the same reference ID

1 "en" "English language for usergroup 1"

1 "nl" "Nederlandse taal voor gebruikersgroep 1"

2 "en" english name for usergroup 2"

2 "nl" "Nederlandse taal voor gebruikersgroep 2"

i can NEVER use a primary key here… Sorry i’m freaking out here

You’re losing me here. Maybe you should try reading up on the use of primary/foreign keys first, if you don’t really get the concept. In short, the table which actually defines the model (in your case, core_users and core_usergroups should define primary keys (usually these columns are called id, or user_id or something similar). Other tables that reference these models should do that by referencing the primary key value. Not the name, even if it is unique. Not every table needs to have a primary key, but if it doesn’t you can only reference other tables with a BELONGS_TO relation:




public function relations() {

 return array(

  'relation_name' => array(self::BELONGS_TO, 'Other_Table', 'foreign_key_in_THIS_table');

 );

}



Yii will match the foreign key you specified to the primary key of the specfied table. It will automatically find out which column in that table is the primary key.

To do it the other way around, a HAS_MANY relation should look like this:




public function relations() {

 return array(

  'relation_name' => array(self::HAS_MANY, 'Other_Table', 'foreign_key_in_OTHER_table');

 );

}



Yii will match the primary key of the current table to the foreign key specified in the other table.

Hope this helps…

I have been monitoring this thread over the past few days just to see how this is handled with ActiveRecord. I think usergroups in the core_usergroups_118 table needs to be replaced with a foreign key to the id of core_usergoups in order to ensure that referential integrity works correctly when records are deleted from core_usergoups (i.e., associated records in the i18 table are deleted automatically). Then the foreign key and language columns in core_usergroups_i18 can be treated as a two-column primary key or a unique key to give a unique identity to each language instance.

For an example of how this is handled with MYISAM, take a look at the products and products_description tables in osCommerce which handles this very well. Note that the osCommerce schema converts to INNODB easily by adding foreign key constraints. osCommerce may be a spagetti code mess, but the database schema is not that bad.

The next thing works, but i cant use the command line tool anymore to make crud from the model.

core_usergroups is unchanged

core_usergroups_i18n has a 2 column primary key:

  • id

  • language

This works when i set the relation to a has_many relationship using the id to reference to the core_usergroups table.

I found something else also. The reason i make a i18n table is because i want the system to be multilingual. There is a locale/language class available inside yii. Can i use this to support new languages from the database?

Of course for usergroups, the results from the database should be cached after they were changed. This is because i’ll lose db performance if i have to pull the usergroup name each single time from the database while it probably stays the same for ages. (i still want it to be changeable by the end-user, that’s why i use the db)

Thanks for the replies above.

Ok, but now, when i have the information pulled out of the database i cant use it. When i do:




foreach ($usergroups as $key => $usergroup):


$usergroup->i18n->name // NAME/I18N IS NO OBJECT

$usergroup['i18n']['name'] // THIS PRINTS JUST NOTHING


endforeach;



How do i access the i18n object ?