OneToMany for the trivial case of Entity with Set/List [String]

26 views
Skip to first unread message

samzil...@gmail.com

unread,
Apr 22, 2013, 1:34:59 PM4/22/13
to mapp...@googlegroups.com
Hi again,
I just bumped into a pretty trivial scenario, which I have only found a somewhat complicated solution for.
Perhaps you know of a simple one? 

Scenario: 
case class User has a set of Emails (String).
Or to make it a little more complicated, a set of "Role" - a case class that is persisted to db by its name (kinda like a java enum persistence), but doesn't need a db table of its own because it's hard-coded.
The db holds a "users" table matching the UserEntity and a "users_roles" join table with user_id, role_name.

The somewhat complex solution:
The only solution I found so far is to create an Entity for the join table - UsersRolesEntity.
Then using this entity and a onetomany to construct the User case class with the correct simple Role objects. 
This solution is the same even for a simple Set[Emails] - to build the User case class one needs to extract the emails Set[String] from the given Set[UsersEmailsEntity].

Is there a simpler way to accomplish this?

Thanks,
Sam

samzil...@gmail.com

unread,
Apr 22, 2013, 1:37:58 PM4/22/13
to mapp...@googlegroups.com, samzil...@gmail.com
Hmm, sorry,
I somehow missed this page of the manual
Will try this out :)

samzil...@gmail.com

unread,
Apr 22, 2013, 2:45:28 PM4/22/13
to mapp...@googlegroups.com, samzil...@gmail.com
Eventually I did run into an annoying problem.
The given fk for the join table is ignored.
I created a table user_roles with user_id, role.
Created the entities, but am getting a sql error because mapper puts "where users_id = ?" instead of user_id.
It's either a bug or I'm missing something simple.


object UserEntity extends Entity[Long, SurrogateLongId, User]("users") {

  val RolesEntity = StringEntity.oneToMany("users_roles", "this fk is totaly ignored!", "role" )

  val id = key("id") autogenerated (_.id)
  val email = column("email") to (_.email)
  val password = column("password") option (_.password)
  val roles = onetomany(RolesEntity) tostring (user => user.roles.map(rol => rol.toString))

  def constructor(implicit m: ValuesMap) = {
    val rls: Traversable[Role] = m(roles).map(roleStr => Role.role(roleStr.value)) //role from string...
    new User(email = email, password = password, roles = rls.toSet)
      with Stored {
      override val id: Long = UserEntity.id
    }
  }
}

On Monday, April 22, 2013 7:34:59 PM UTC+2, samzil...@gmail.com wrote:

Konstantinos Kougios

unread,
Apr 22, 2013, 3:34:45 PM4/22/13
to mapp...@googlegroups.com
Hi Sam,

    You're right, this is a bug. The foreign column shouldn't be part of the StringEntity. The way the model of mapperdao works, you need to declare it on the main entity.

So it will look like:

  val RolesEntity = StringEntity.oneToMany("users_roles", "role" )

...

  val roles = onetomany(RolesEntity) foreignkey "my_fk" tostring (user => user.roles.map(rol => rol.toString))

(btw, isn't that meant to be like user=>user.roles.mkString(",") or so?)

I am deploying a snapshot of rc21 as we speak. But also you can use the foreignkey in rc20 and leave your RolesEntity as is.

Cheers
--
You received this message because you are subscribed to the Google Groups "mapperdao" group.
To unsubscribe from this group and stop receiving emails from it, send an email to mapperdao+...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.
 
 

samzil...@gmail.com

unread,
Apr 23, 2013, 10:14:44 AM4/23/13
to mapp...@googlegroups.com, samzil...@gmail.com
I knew I must be missing something trivial, foreign key declaration on the column itself...
Thanks!

Your comment with mkString on this made me realize something more.

user=>user.roles.mkString(",")
would mean that I'm persisting the whole set "inline" or with just using one row:
user_id, role -> 10, "Admin, Normal, BlaBla"

What I actually tried to achieve is persisting the Set into several rows:
10,"Admin"
10,"Normal"
...

I manually put a couple of rows like this into the db, and so far loading the User with his roles was a success.
I haven't tried persisting a new set or adding to this one, but I suspect I might run into trouble.

Is there some special or correct way to handle this with mapperdao?

Konstantinos Kougios

unread,
Apr 23, 2013, 10:18:25 AM4/23/13
to mapp...@googlegroups.com
ah , yes, you need user=>user.roles  . I thought you tried to store it all in 1 row.

samzil...@gmail.com

unread,
Apr 24, 2013, 12:37:05 PM4/24/13
to mapp...@googlegroups.com, samzil...@gmail.com
follow up question:

After setting up this oneToMany relation I also wanted to make it lazy, but was unable.
here is the setup followed up by a little more info:

sealed abstract class Role(dbName: String)
object Role {
  case object Admin extends Role("Admin")
  case object Premium extends Role("Premium")
  case object Unknown extends Role("Unknown")
  def roles: Seq[Role] = List(Admin, Premium, ContentUploader, Unknown)
  def fromString(s: String) = roles find { _.toString == s } getOrElse(Unknown)
}

case class User(id:Long, bla bla, roles:Set[Role]=Set.empty[Role])

object UserEntity extends Entity[Long, SurrogateLongId, User]("users") {

  val RolesEntity = StringEntity.oneToMany("users_roles","unused fk", "role" )

columns...
...
val roles = onetomany(RolesEntity) foreignkey("user_id") getter("roles") tostring (user => user.roles.map(rol => rol.toString))

  def constructor(implicit m: ValuesMap) = {
    val rls: Traversable[Role] = m(roles).map(roleStr => Role.role(roleStr.value))
    new User(email = email, password = password, facebook_id = facebookId, created = created, roles = rls.toSet)
    
Obviously this can never truly be lazy because I'm explicitly calling the roles relation and building it before constructing the User obj.
I tried to achieve this by creating a custom type Role and got stuck because of the Set[Role].
oneToMany requires an entity...
So then I tried creating my own RoleEntity - in the same way Double/String/Float/etc Entity is made.
But that didn't turn out well either. no matter what I tried I always ended up with this compilation error:
[error]  found   : Set[models.Role]
[error]  required: Traversable[persistence.mapperdao.RoleValue]
[error]   val roles = onetomany(RoleEntity.oneToMany("users_roles","role")) foreignkey("user_id") getter("roles") to (_.roles <-----)

I noticed you showed a way to achieve lazyness by having a function in the domain class instead of a simple val, but I didn't like it for the exact same reasons you wrote down after the example.

Any idea how I can achieve this?

Konstantinos Kougios

unread,
Apr 24, 2013, 3:29:49 PM4/24/13
to mapp...@googlegroups.com
Hi,


On 24/04/13 17:37, samzil...@gmail.com wrote:
follow up question:

After setting up this oneToMany relation I also wanted to make it lazy, but was unable.
here is the setup followed up by a little more info:

sealed abstract class Role(dbName: String)
object Role {
  case object Admin extends Role("Admin")
  case object Premium extends Role("Premium")
  case object Unknown extends Role("Unknown")
  def roles: Seq[Role] = List(Admin, Premium, ContentUploader, Unknown)
  def fromString(s: String) = roles find { _.toString == s } getOrElse(Unknown)
}

case class User(id:Long, bla bla, roles:Set[Role]=Set.empty[Role])

object UserEntity extends Entity[Long, SurrogateLongId, User]("users") {

  val RolesEntity = StringEntity.oneToMany("users_roles","unused fk", "role" )

columns...
...
val roles = onetomany(RolesEntity) foreignkey("user_id") getter("roles") tostring (user => user.roles.map(rol => rol.toString))

  def constructor(implicit m: ValuesMap) = {
    val rls: Traversable[Role] = m(roles).map(roleStr => Role.role(roleStr.value))
    new User(email = email, password = password, facebook_id = facebookId, created = created, roles = rls.toSet)
    
Obviously this can never truly be lazy because I'm explicitly calling the roles relation and building it before constructing the User obj.
Correct, it will load it when you do m(roles).map...


I tried to achieve this by creating a custom type Role and got stuck because of the Set[Role].
oneToMany requires an entity...
So then I tried creating my own RoleEntity - in the same way Double/String/Float/etc Entity is made.
Correct but don't use StringEntity as a prototype. MapperDao is aware of those specific entities and treats them differently.

Just declare an entity as usual, something like:

object RoleEntity extends Entity[Int,SurrogateIntId,Role]
{
    val id=key("id") autogenerated to(_.id)
    val value=column("value") to (_.value)
    def constructor(implicit m:ValuesMap)=new Role(value) with Stored {
        val id:Int=RoleEntity.id
    }
}


Then you won't have to m(roles).map()

If you got many of those then a trait can help:

trait MySimpleEntity[T] extends Entity[Int,SurrogateIntId,T]
{
    val id=key("id") autogenerated to(_.id)
    val value=column("value") to (_.value)
}

Let me know if it doesn't work for you.

Cheers


But that didn't turn out well either. no matter what I tried I always ended up with this compilation error:
[error]  found   : Set[models.Role]
[error]  required: Traversable[persistence.mapperdao.RoleValue]
[error]   val roles = onetomany(RoleEntity.oneToMany("users_roles","role")) foreignkey("user_id") getter("roles") to (_.roles <-----)

I noticed you showed a way to achieve lazyness by having a function in the domain class instead of a simple val, but I didn't like it for the exact same reasons you wrote down after the example.

Any idea how I can achieve this?

samzil...@gmail.com

unread,
Apr 25, 2013, 10:03:35 AM4/25/13
to mapp...@googlegroups.com, samzil...@gmail.com
I tried this but it doesn't help in this problem.
Role is not an entity with a table that has an id and is saved in the db.
it's more like an Enum, only it's name is saved.

The problem is similiar to 
https://code.google.com/p/mapperdao/wiki/Enumerations
But trying to persist a Set[Gender] and not just one Gender.
It makes more sense when speaking on something else than Gender :-)

Your enum in the example does not have it's own table in the db, it functions only to encapsulate some string info ("F"/"M") and giving it context of a Female/Male.

On Monday, April 22, 2013 7:34:59 PM UTC+2, samzil...@gmail.com wrote:

Konstantinos Kougios

unread,
Apr 25, 2013, 5:03:43 PM4/25/13
to mapp...@googlegroups.com
On 25/04/13 15:03, samzil...@gmail.com wrote:
I tried this but it doesn't help in this problem.
Role is not an entity with a table that has an id and is saved in the db.
it's more like an Enum, only it's name is saved.

So ok the simple solution is not to lazy load it. If you eager load it then your code works.

Lazy loading makes things hard. Lazy loading is achieved using proxies (like i.e. hibernate), so what mapperdao does is:

1. Calls your constructor with all lazy loaded values set to empty, i.e. Nil, Set() etc

2. Creates a proxy of the class with underlying value what your constructor returned except from the lazy loaded values. Getters/Setters are redirected to mapperdao code

3. If the getter is called, then mapper dao does the query and loads the related Set[String].

So if you plan to use StringEntity, you need a Set[String] (even if it is private) in your User class. I.e the following , though not so clean, would work:

class User(id:Long, private val rolesSet:Set[String]) {
    lazy val roles=rolesSet.map(roleStr => Role.role(roleStr.value))
}

The above can be lazy loaded (because mapperdao can proxy rolesSet) but is ugly.

There is no workaround with a clean domain model that I can think of. An alternative solution is to not use your enum-like Roles object and let the "enum" live in the database and load it as you would do with any other entity.



The problem is similiar to 
https://code.google.com/p/mapperdao/wiki/Enumerations
But trying to persist a Set[Gender] and not just one Gender.
It makes more sense when speaking on something else than Gender :-)

Your enum in the example does not have it's own table in the db, it functions only to encapsulate some string info ("F"/"M") and giving it context of a Female/Male.

On Monday, April 22, 2013 7:34:59 PM UTC+2, samzil...@gmail.com wrote:
Hi again,
I just bumped into a pretty trivial scenario, which I have only found a somewhat complicated solution for.
Perhaps you know of a simple one? 

Scenario: 
case class User has a set of Emails (String).
Or to make it a little more complicated, a set of "Role" - a case class that is persisted to db by its name (kinda like a java enum persistence), but doesn't need a db table of its own because it's hard-coded.
The db holds a "users" table matching the UserEntity and a "users_roles" join table with user_id, role_name.

The somewhat complex solution:
The only solution I found so far is to create an Entity for the join table - UsersRolesEntity.
Then using this entity and a onetomany to construct the User case class with the correct simple Role objects. 
This solution is the same even for a simple Set[Emails] - to build the User case class one needs to extract the emails Set[String] from the given Set[UsersEmailsEntity].

Is there a simpler way to accomplish this?

Thanks,
Sam
--

samzil...@gmail.com

unread,
Apr 26, 2013, 7:45:03 AM4/26/13
to mapp...@googlegroups.com, samzil...@gmail.com
Thanks for the detailed answer.
I decided to use the KISS solution and just used Set[String] for roles.
Then wherever I need to get the Role object an implicit conversion from String->Role does the trick.

I have a suggestion though, that could solve this, but not sure if it's doable:
From your enum example:
 val gender = column("gender") to (customer => Gender.toString(customer.gender))
...

def constructor(implicit m) = {
        val g
= Gender.fromString(m(gender))

if the Gender.fromString(m(gender)) could be passed to the column definition :

val gender = column("gender") ...blabla... convert(implicit m=> v => Gender.fromString(m(gender)))

So as far as mapper cares this is a String (or Set[String]) but when constructing the Model it would run this conversion function (just as it uses the customer => func).
Maybe this would work with lazyness too as this func can be run later, after lazy loading from the db.

Not sure how possible this is. but if it is it would certainly enrich the library.
I dug a little into the source code but there's a lot happening there and it is still beyond me.

On Monday, April 22, 2013 7:34:59 PM UTC+2, samzil...@gmail.com wrote:

Konstantinos Kougios

unread,
Apr 26, 2013, 4:47:04 PM4/26/13
to mapp...@googlegroups.com
Hi,

    Actually I was thinking of a convert function myself. It might be feasible. 1 problem I can think of is that the way you want to use it, the column type is String but the Scala type is Role and that might be tricky.
--
Reply all
Reply to author
Forward
0 new messages