I tried using MongoDB 2.0.6 to replace MySQL 5.5.25 for a test Grails 2.1 App and am encountering some strange problems.
Issues when using MongoDB but not MySQL:
When using Scaffolding, I cannot get the fields to order by using static constraints
When I specify inList as a constraint, I get a drop-down when using a MySQL backend, but a field when using a MongoDB backend.
No * (asterisk) on fields where blank=false constraint specified.
Domain Class:
package study
class Student {
String login
String firstName
String lastName
String gender
Boolean active
Date dateCreated
Date lastUpdated
static constraints = {
login()
firstName(blank: false)
lastName(blank: false)
gender(inList: ['M', 'F'])
active()
}
}
Controller
package study
class StudentController {
def scaffold = true
}
DataSource.groovy (MySQL stuff commented out):
grails {
mongo {
host = "dev-linux"
port = 27017
username = "study"
password= "********"
databaseName = "study"
}
}
//dataSource {
// pooled = true
// driverClassName = "com.mysql.jdbc.Driver"
// dialect = "org.hibernate.dialect.MySQL5InnoDBDialect"
// username = "study"
// password = "********"
// dbCreate = "create-drop" // one of 'create', 'create-drop','update'
// url = "jdbc:mysql://dev-linux:3306/study"
//
//}
//hibernate {
// cache.use_second_level_cache = true
// cache.use_query_cache = true
// cache.provider_class = "net.sf.ehcache.hibernate.EhCacheProvider"
//}
BuildConfig.groovy (plugins section shown was all I changed to put MongoDB in place of MySQL, the remainder of this file is the default created by Grails)
plugins {
// build ":hibernate:$grailsVersion"
// compile ":mysql-connectorj:5.1.12"
compile ":mongodb:1.0.0.GA"
build ":tomcat:$grailsVersion"
}
The only changes I made to put in MongoDB and take out MySQL is the changes to the DataSource.groovy and BuildConfig.groovy shown above.
Is there any configuration item that I am missing?
I did see someone mention on this Nabble forum post that the field ordering may be an issue with MongoDB.
However, this post did not have any details.
Also, I did not understand why or how the back end Database engine could impact how the view is rendered when using scaffolding. Specifically, the ordering on a page and drop-down vs textfield.
I would have thought that would come from the Domain Class's field types and constraints.
Has anyone come across this odd behavior when using Grails+Scaffolding with MongoDB before? Does anyone know of a fix or have any insight?
Thank you very much in advance, I appreciate it.
Scaffolding with MongoDB works, the problem is if you just install mongodb plugin, grails will see ambiguous domain mappings and errors like these pop up. You need to either:
Remove hibernate plugin like this:
grails uninstall-plugin hibernate
Also remove these lines from BuildConfig.groovy:
runtime ":database-migration:1.1"
runtime ":hibernate:$grailsVersion"
Explicitly tell a given domain is persisted by Mongo by adding this line to it:
static mapWith="mongo"
Related
I am currently using Grails 2.5.4, with the MongoDB plugin (org.grails.plugins:mongodb:6.0.0.RC2) and whenever I try to update a List of any domain class, it doesn't work.
When this code executes, the remove function succeeds and so does the save method. But the record on the database keeps the same.
def updateMessage(){
String id = '-1001066675850-7184293742'
Message message = Message.findById(id)
def entity = message.entities.get(0)
message.removeFromEntities(entity)
message.save(validate: true, failOnError: true, flush: true)
render message.entities
}
Message domain class is mapped like this:
class Message {
User fromUser
String text
Chat chat
Date sentDate
List<MessageEntity> entities
String sticker
Point location
String id
boolean pinned = false
static belongsTo = [entities: MessageEntity]
static constraints = {
chat nullable: false
fromUser nullable: false
sentDate nullable: false
}
static hasMany = [entities: MessageEntity]
static mapping = {
id generator: 'assigned'
location geoIndex: '2dsphere'
}
}
I've tried creating a new list from a scratch, adding it to the Message and it still won't update.
Is there anything i'm missing on the documentation?
Apparently updating lists was broken on either GORM for MongoDB 6.0.0.RC2 or for some particular settings on my (And a friend's) environment.
Solved it by downgrading to version 3.0.1.
I am trying to connect my Scala application to a Postgres cluster consisting of one master node and 3 slaves/read replicas. My application.conf looks like this today:
slick {
dbs {
default {
driver = "com.company.division.db.ExtendedPgDriver$"
db {
driver = "org.postgresql.Driver"
url = "jdbc:postgresql://"${?DB_ADDR}":"${?DB_PORT}"/"${?DB_NAME}
user = ${?DB_USERNAME}
password = ${?DB_PASSWORD}
}
}
}
}
Based on Postgres' documentation, I can define the master and slaves all in one JDBC URL, which will give me some failover capabilities, like this:
jdbc:postgresql://host1:port1,host2:port2/database
However, if I want to separate my connections by read and write capabilities, I have to define two JDBC URls, like this:
jdbc:postgresql://node1,node2,node3/database?targetServerType=master
jdbc:postgresql://node1,node2,node3/database?targetServerType=preferSlave&loadBalanceHosts=true
How can I define two JDBC URLs within Slick? Should I define two separate entities under slick.dbs, or can my slick.dbs.default.db entity have multiple multiple URLs defined?
Found an answer from Daniel Westheide's blog post. To summarize, it can be done with a DB wrapper class and custom Effect types that provides specific rules to control where read-only queries are directed vs. write queries are directed.
Then your slick file would look like this:
slick {
dbs {
default {
driver = "com.yourdomain.db.ExtendedPgDriver$"
db {
driver = "org.postgresql.Driver"
url = "jdbc:postgresql://"${?DB_PORT_5432_TCP_ADDR}":"${?DB_PORT_5432_TCP_PORT}"/"${?DB_NAME}
user = ${?DB_USERNAME}
password = ${?DB_PASSWORD}
}
}
readonly {
driver = "com.yourdomain.db.ExtendedPgDriver$"
db {
driver = "org.postgresql.Driver"
url = ${DB_READ_REPLICA_URL}
user = ${?DB_USERNAME}
password = ${?DB_PASSWORD}
}
}
}
}
And it's up to your DB wrapper class to route queries to either 'default' or 'readonly'
I'm trying to put some of my domain classes into the MongoDB using the mongoDB grails plugin. Some of the classes stays in MySQL. Everything works fine even the saving of domain class instances into the MongoDB (for example in service on controller code). However, If I try to save the instance from the afterUpdate() of certain not-mongoDB class it doesn't work. It doesn't throw any exception or whatever...
My not-mongoDB domain class:
class CarState extends AbstractCarState {
...
def afterUpdate() {
def logItemInstance = new CarStateLogItem(this.properties)
logItemInstance.save(failOnError: true)
}
}
MongoDB domain class:
class CarStateLogItem extends AbstractCarState {
ObjectId id
static mapWith = "mongo"
...
}
The weird thing is that if I run the afterUpdate() code from controller it saves into the MongoDB. Am I something missing? Or why I cannot save the instance?
Thanks for any advice,
Mateo
I think you need to initiate a new transaction in order to save in mongodb. If you notice, the transaction for CarState will be of MySQL. In order to transact with mongodb from the afterUpdate event there has to be a new mongodb transaction. Try this.
def afterUpdate() {
CarStateLogItem.withTransaction{status ->
def logItemInstance = new CarStateLogItem(this.properties)
logItemInstance.save(failOnError: true)
}
}
I am attempting to use Entity Framework code based migrations with my web site. I currently have a solution with multiple projects in it. There is a Web API project which I want to initialize the database and another project called the DataLayer project. I have enabled migrations in the DataLayer project and created an initial migration that I am hoping will be used to create the database if it does not exist.
Here is the configuration I got when I enabled migrations
public sealed class Configuration : DbMigrationsConfiguration<Harris.ResidentPortal.DataLayer.ResidentPortalContext>
{
public Configuration()
{
AutomaticMigrationsEnabled = false;
}
protected override void Seed(Harris.ResidentPortal.DataLayer.ResidentPortalContext context)
{
// This method will be called after migrating to the latest version.
// You can use the DbSet<T>.AddOrUpdate() helper extension method
// to avoid creating duplicate seed data. E.g.
//
// context.People.AddOrUpdate(
// p => p.FullName,
// new Person { FullName = "Andrew Peters" },
// new Person { FullName = "Brice Lambson" },
// new Person { FullName = "Rowan Miller" }
// );
//
}
}
The only change I made to this after it was created was to change it from internal to public so the WebAPI could see it and use it in it's databaseinitializer. Below is the code in the code in the Application_Start that I am using to try to initialize the database
Database.SetInitializer(new MigrateDatabaseToLatestVersion<ResidentPortalContext, Configuration>());
new ResidentPortalUnitOfWork().Context.Users.ToList();
If I run this whether or not a database exists I get the following error
Directory lookup for the file "C:\Users\Dave\Documents\Visual Studio 2012\Projects\ResidentPortal\Harris.ResidentPortal.WebApi\App_Data\Harris.ResidentPortal.DataLayer.ResidentPortalContext.mdf" failed with the operating system error 2(The system cannot find the file specified.).
CREATE DATABASE failed. Some file names listed could not be created. Check related errors.
It seems like it is looking in the totally wrong place for the database. It seems to have something to do with this particular way I am initializing the database because if I change the code to the following.
Database.SetInitializer(new DropCreateDatabaseAlways<ResidentPortalContext>());
new ResidentPortalUnitOfWork().Context.Users.ToList();
The database will get correctly created where it needs to go.
I am at a loss for what is causing it. Could it be that I need to add something else to the configuration class or does it have to do with the fact that all my migration information is in the DataLayer project but I am calling this from the WebAPI project?
I have figured out how to create a dynamic connection string for this process. You need to first add this line into your EntityFramework entry on Web or App.Config instead of the line that gets put there by default.
<defaultConnectionFactory type="<Namespace>.<ConnectionStringFacotry>, <Assembly>"/>
This tells the program you have your own factory that will return a DbConnection. Below is the code I used to make my own factory. Part of this is a hack to get by the fact that a bunch of programmers work on the same set of code but some of us use SQL Express while others use full blown SQL Server. But this will give you an example to go by for what you need.
public sealed class ResidentPortalConnectionStringFactory: IDbConnectionFactory
{
public DbConnection CreateConnection(string nameOrConnectionString)
{
SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(ConfigurationManager.ConnectionStrings["PortalDatabase"].ConnectionString);
//save off the original catalog
string originalCatalog = builder.InitialCatalog;
//we're going to connect to the master db in case the database doesn't exist yet
builder.InitialCatalog = "master";
string masterConnectionString = builder.ToString();
//attempt to connect to the master db on the source specified in the config file
using (SqlConnection conn = new SqlConnection(masterConnectionString))
{
try
{
conn.Open();
}
catch
{
//if we can't connect, then append on \SQLEXPRESS to the data source
builder.DataSource = builder.DataSource + "\\SQLEXPRESS";
}
finally
{
conn.Close();
}
}
//set the connection string back to the original database instead of the master db
builder.InitialCatalog = originalCatalog;
DbConnection temp = SqlClientFactory.Instance.CreateConnection();
temp.ConnectionString = builder.ToString();
return temp;
}
}
Once I did that I coudl run this code in my Global.asax with no issues
Database.SetInitializer(new MigrateDatabaseToLatestVersion<ResidentPortalContext, Configuration>());
using (ResidentPortalUnitOfWork temp = new ResidentPortalUnitOfWork())
{
temp.Context.Database.Initialize(true);
}
I have data writing to a mongoDB database with issues using integration tests and the Grails scaffolding. When trying to select a domain instance from the 'list' type page, I get the error "[domain name] not found with id null".
I am sure it is because of the Grails url [controller]/[action]/[id]. This id is a string and needs to be converted to an ObjectId for Grails queries.
Is there a way to do this so that it affects a specified domain or even better yet, all of the domains at once?
I guess as I'm writing my app, I can convert it to an ObjectId from within the action method, but I'd like to have the scaffolding work or provide a global solution.
I believe this is happening because the show() method (that the Grails scaffolding functionality generates as an action) accepts an id parameter of type Long ie.
def show(Long id) {
def suiteInstance = Suite.get(id)
if (!suiteInstance) {
flash.message = message(code: 'default.not.found.message', args: [message(code: 'suite.label', default: 'MyDomainClass'), id])
redirect(action: "list")
return
}
[suiteInstance: suiteInstance]
}
which binds the id parameter to the argument. Because the ObjectId can't be converted to a Long, it ends up being null, hence the call to MyDomainClass.get(id) fails with the error message.
You can get around this by overriding the show() action in your scaffolded controller so that it expects an ObjectId or String, but I would say the proper fix for this is to update the Grails scaffolding plugin so it is a little more liberal in the types of IDs it accepts.
I had this problem as well. You can keep the domain object id as an ObjectId and update the controller as follows:
domain Object:
import org.bson.types.ObjectId;
class DomainObject {
ObjectId id
// Add other member variables...
}
Controller:
def show(String id) {
def domainObjectInstance = domainObject.get(new ObjectId(id))
if (!domainObjectInstance) {
flash.message = message(code: 'default.not.found.message', args: [message(code: 'domainObject.label', default: 'DomainObject'), id])
redirect(action: "list")
return
}
[domainObjectInstance: domainObjectInstance]
}
You would also need to update your other controller methods that use id as well such as edit, update etc.
Additionally, if you want the grails default controller generation to work like this for all your domain objects you can update the template as coderLMN suggests.
The get(params.id) call in show() method will NOT convert params.id String to an ObjectId object, so the domain instance will be null, then the following code takes you to list action with an error message:
if (!exampleInstance) {
flash.message = message(code: 'default.not.found.message', args: [message(code: 'example.label', default: 'Example'), params.id])
redirect(action: "list")
return
}
Possible solutions:
you can run "grails install-template" command, so that the scaffolding templates in src/templates/scaffolding/ directory can be modified. Then you have new scaffold ready to generate customized controllers, views, tests for all your Domain classes.
A simpler solution is to define the id property as String instead of ObjectId. A String id will be equal to objectId.toString(), in this case your scaffold will work.
In domain classes keep you id type as ObjectId and keep scaffold = true for all respective controllers.
In Domain class :
ObjectId id
In respective controller :
static scaffold = true
Clear all existing collections from Mongo
I guess that's sufficient to have Grails-Mongo app up & running, considering you have correctly configured mongo-plugin