Send email from MySQL trigger when a table updated - mysql

Consider a table as table2, i like to add a trigger on this table update as
select Num into num from table1 where ID=new.id;
BEGIN
DECLARE done int default false;
DECLARE cur1 CURSOR FOR select EmailId from users where Num=num;
DECLARE continue handler for not found set done = true;
OPEN cur1;
my_loop: loop
fetch cur1 into email_id;
if done then
leave my_loop;
end if;
//send mail to all email id.
end loop my_loop;
close cur1;
END;
Is there any simple method to write in the place commented? To send a email to all the email id retrieved from table users.
I am using MySQL in XAMPP(phpmyadmin).

I'm against that solution. Putting the load of sending e-mails in your Database Server could end very bad.
In my opinion, what you should do is create a table that queues the emails you would want to send and have a process that checks if there are emails to be sent and send them if positive.

As everyone said, making your database to deal with E-Mails is a bad idea. Making your database to write an external file is another bad idea as IO operations are time consuming compared to database operations.
There are several ways to deal with your issue, one that I like: Having a table to queue up your notifications/emails and a client program to dispatch the emails.
Before you continue, you should make sure you have an E-Mail account or service that you can use to send out emails. It could be SMTP or any other service.
If you are on .NET you can copy this, if not you get the idea how to rewrite in your platform
Create a table in your DB
CREATE TABLE `tbl_system_mail_pickup` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`mFrom` varchar(128) NOT NULL DEFAULT 'noreplyccms#YourDomain.co.uk',
`mTo` varchar(128) NOT NULL,
`mCC` varchar(128) DEFAULT NULL,
`mBCC` varchar(128) DEFAULT NULL,
`mSubject` varchar(254) DEFAULT NULL,
`mBody` longtext,
`added_by` varchar(36) DEFAULT NULL,
`updated_by` varchar(36) DEFAULT NULL,
`added_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`email_sent` bit(1) DEFAULT b'0',
`send_tried` int(11) DEFAULT '0',
`send_result` text,
PRIMARY KEY (`id`),
UNIQUE KEY `id_UNIQUE` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
this table will hold all your notifications and emails. Your Table trigger or events which is trying to send an email will add a record to above table. Your client will then pickup the emails and dispatch at an interval you specify.
Here is a sample console app on .NET just to give you an idea how the client might look like. You can modify it according to your needs. ( I use Entity-framework as its much easier and simpler)
Create new Console Project in Visual Studio.
In package manager console, install following packages (install-package EntityFramework, install-package MySQL.Data.Entity)
Add a reference to System.Configuration
Now open "App.Config" file and add your connection-strings to your MySQL database.
In the same App.Config file, add your SMTP email settings.
check your port and ssl option
<system.net>
<mailSettings>
<smtp from="Noreplyccms#youractualEmailAccount.co.uk">
<network host="smtp.office365.com(your host)" port="587" enableSsl="true" password="dingDong" userName="YourUsername#YourDomain" />
</smtp>
</mailSettings>
Create new Folder called Models in your project tree.
Add new item -> ADO.NET Entity Data Model -> EF Designer from database
Select your Connection-String -> tick the Save connection setting in App.Config - > Name your database.
Next - > Select only the above table from your database and complete the wizard.
Now you have a connection to your database and email configurations in place. Its time to read emails and dispatch them.
Here is a full program to understand the concept. You can modify this if you have win application, or web application an in cooperate this idea.
This console application will check every minutes for new email records and dispatches them. You can extend above table and this script to be able to add attachments.
using System;
using System.Linq;
using System.Timers;
using System.Data;
using System.Net.Mail;
using ccms_email_sender_console.Models;
namespace ccms_email_sender_console
{
class Program
{
static Timer aTimer = new Timer();
static void Main(string[] args)
{
aTimer.Elapsed += new ElapsedEventHandler(OnTimedEvent);// add event to timer
aTimer.Interval = 60000; // timer interval
aTimer.Enabled = true;
Console.WriteLine("Press \'q\' to exit");
while (Console.Read() != 'q');
}
private static void OnTimedEvent(object source, ElapsedEventArgs e)
{
var db_ccms_common = new DB_CCMS_COMMON(); // the database name you entered when creating ADO.NET Entity model
Console.WriteLine(DateTime.Now+"> Checking server for emails:");
aTimer.Stop(); /// stop the timer until we process current queue
var query = from T1 in db_ccms_common.tbl_system_mail_pickup where T1.email_sent == false select T1; // select everything that hasn't been sent or marked as sent.
try
{
foreach (var mRow in query)
{
MailMessage mail = new MailMessage();
mail.To.Add(mRow.mTo.ToString());
if (mRow.mCC != null && !mRow.mCC.ToString().Equals(""))
{
mail.CC.Add(mRow.mCC.ToString());
}
if (mRow.mBCC != null && !mRow.mBCC.ToString().Equals(""))
{
mail.CC.Add(mRow.mBCC.ToString());
}
mail.From = new MailAddress(mRow.mFrom.ToString());
mail.Subject = mRow.mSubject.ToString();
mail.Body = mRow.mBody.ToString();
mail.IsBodyHtml = true;
SmtpClient smtp = new SmtpClient();
smtp.Send(mail);
mRow.email_sent = true;
Console.WriteLine(DateTime.Now + "> email sent to: " + mRow.mTo.ToString());
}
db_ccms_common.SaveChanges(); // mark all sent emails as sent. or you can also delete the record.
aTimer.Start(); // start the timer for next batch
}
catch (Exception ex)
{
Console.WriteLine(DateTime.Now + "> Error:" + ex.Message.ToString());
// you can restart the timer here if you want
}
}
}
}
to Test: Run above code and insert a record into your tbl_system_mail_pickup table. In about 1 minute you will receive an email to the sender you've specified.
Hope this helps someone.

You asked, "is there a simple method to ... send a email" from a trigger?
The answer is no, not in standard MySQL.
This is usually done by creating some kind of pending_message table and writing a client program to poll that table regularly and send the messages.
Or, you could consider using a user-defined MySQL function. But most production apps don't much like building network functions like email sending into their databases, because performance can be unpredictable.

Related

Unable to execute commands against an offline database when I am trying to connect Oracle offline

I am using the below code:
private static String file="create-table.yml";
public static void main(String[] args) throws Exception {
Database database =createOfflineDatabase("offline:oracle");
Liquibase liquibase = new Liquibase(file, new ClassLoaderResourceAccessor(), database);
liquibase.update("test");
liquibase.dropAll();
}
private static Database createOfflineDatabase(String url) throws Exception {
DatabaseConnection databaseConnection = new OfflineConnection(url, new ClassLoaderResourceAccessor());
return DatabaseFactory.getInstance().openDatabase(url, null, null, null, null);
}
Getting this exception :
Exception in thread "main" liquibase.exception.MigrationFailedException: Migration failed for change set create-table.yml::create-table.yml::vishwakarma:
Reason: liquibase.exception.DatabaseException: Cannot execute commands against an offline database
at liquibase.changelog.ChangeSet.execute(ChangeSet.java:619)
at liquibase.changelog.visitor.UpdateVisitor.visit(UpdateVisitor.java:51)
at liquibase.changelog.ChangeLogIterator.run(ChangeLogIterator.java:79)
at liquibase.Liquibase.update(Liquibase.java:214)
at liquibase.Liquibase.update(Liquibase.java:192)
at liquibase.Liquibase.update(Liquibase.java:188)
at liquibase.Liquibase.update(Liquibase.java:181)
at com.test.liquibase.LiquibaseTest.main(LiquibaseTest.java:27)
Am I doing something wrong or missing something? Please help.
Thanks in Advance
The Liquibase class you are using and that is in most example code doesn't seem to have an "updateSQL". You can programmatically invoke the main method passing in the necessary --url parameters to get it to do the work. for example:
liquibase.integration.commandline.Main.main(new String[]{"--changeLogFile=src/test/resources/db.changelog.xml"
,"--outputFile=target/updateSql.txt"
,"--url=offline:unknown?outputLiquibaseSql=true"
, "updateSQL"});
Will generate:
-- *********************************************************************
-- Update Database Script
-- *********************************************************************
-- Change Log: src/test/resources/db.changelog.xml
-- Ran at: 12/04/20 11:51
-- Against: null#offline:unknown?outputLiquibaseSql=true
-- Liquibase version: 3.8.9
-- *********************************************************************
CREATE TABLE DATABASECHANGELOG (ID VARCHAR(255) NOT NULL, AUTHOR VARCHAR(255) NOT NULL, FILENAME VARCHAR(255) NOT NULL, DATEEXECUTED datetime NOT NULL, ORDEREXECUTED INT NOT NULL, EXECTYPE VARCHAR(10) NOT NULL, MD5SUM VARCHAR(35), DESCRIPTION VARCHAR(255), COMMENTS VARCHAR(255), TAG VARCHAR(255), LIQUIBASE VARCHAR(20), CONTEXTS VARCHAR(255), LABELS VARCHAR(255), DEPLOYMENT_ID VARCHAR(10));
-- Changeset src/test/resources/db.changelog.xml::createTable-example::liquibase-docs
CREATE TABLE public.person (address VARCHAR(255));
INSERT INTO DATABASECHANGELOG (ID, AUTHOR, FILENAME, DATEEXECUTED, ORDEREXECUTED, MD5SUM, DESCRIPTION, COMMENTS, EXECTYPE, CONTEXTS, LABELS, LIQUIBASE, DEPLOYMENT_ID) VALUES ('createTable-example', 'liquibase-docs', 'src/test/resources/db.changelog.xml', CURRENT_TIMESTAMP, 1, '8:49e8eb557129b33d282c4ad2fdc5d4d9', 'createTable tableName=person', '', 'EXECUTED', NULL, NULL, '3.8.9', '6688703163');
And also outputs a databasechangelog.csv that tracks what should be the state of the DATABASECHANGELOG for subsequent runs.
If you edit the code to load "db.changelog2.xml" with something new in it with a different ID and tell it to write out the sql to a new file that will only have the second change set in it. This is because the databasechangelog.csv will be used to know what was previously run. The CSV will then be updated for the next time:
"ID","AUTHOR","FILENAME","DATEEXECUTED","ORDEREXECUTED","EXECTYPE","MD5SUM","DESCRIPTION","COMMENTS","TAG","LIQUIBASE","CONTEXTS","LABELS","DEPLOYMENT_ID"
"createTable-example","liquibase-docs","src/test/resources/db.changelog.xml","2020-04-12T12:10:16.989","2","EXECUTED","8:49e8eb557129b33d282c4ad2fdc5d4d9","createTable tableName=person","","","3.8.9","()","","6689816939"
"createTable-example2","liquibase-docs","src/test/resources/db.changelog2.xml","2020-04-12T12:26:56.664","4","EXECUTED","8:3740614394b969eeb1edcc9fd0187bdb","createTable tableName=person2",,"","3.8.9","()","","6690816645"
Warning: It seems that if you call the main method directly it will internally call System.exit() so if you really wanted to run updateSQL inside a tool you will need to look at the code inside that main class to figure out how to do it without the JVM being terminated by System.exit().
Offline databases are something that Liquibase came up with to allow you to generate SQL for updating a database that the liquibase user is not able to update directly - they are not able to be updated. See this documentation for more details about offline databases.

EF6 MySql: Update-Database -Script generates SQL without semicolon

I am having the following uneasy situation when using EF6 with MySql: each time I create a new migrations I apply the changes to my development environment using
Update-Database
but when I want to generate the SQL script for my other environments (and to keep in source control) I use
Update-Database -Script
and the generated SQL is something like this:
CREATE TABLE `AddressType` (`Id` NVARCHAR(10) NOT NULL ,`Description` NVARCHAR(30) NOT NULL ,PRIMARY KEY ( `Id`) ) ENGINE=INNODB AUTO_INCREMENT=0
CREATE TABLE `Bank` (`Id` INT NOT NULL ,`CNPJ` BIGINT NOT NULL ,`Name` NVARCHAR(100) ,`WebSite` NVARCHAR(500) ,PRIMARY KEY ( `Id`) ) ENGINE=INNODB AUTO_INCREMENT=0
When I try running the generated script on SQLyog and run it I get an error that my script has invalid syntax. I believe this is because EF only added one semicolon at the end of it, as it runs when I add the semicolons manually. One problem I have when I add the semicolons is that if the script fails for some reason the database is in inconsistent state, meaning the migration system will fail onwards because tables/columns will already exist.
Are there any settings that automatically add the semicolons after every statement? Is there any way I can ask MySql to do all or nothing when running my scripts / migrations?
Thanks.
You can accomplish this by extending the MySqlMigrationSqlGenerator as follows:
/// <summary>
/// Custom MigrationSqlGenerator to add semi-colons to the end of
/// all migration statements.
/// </summary>
public class CustomMySqlMigrationSqlGenerator : MySqlMigrationSqlGenerator {
public override IEnumerable<MigrationStatement> Generate(IEnumerable<MigrationOperation> migrationOperations, string providerManifestToken) {
IEnumerable<MigrationStatement> statements = base.Generate(migrationOperations, providerManifestToken);
foreach (MigrationStatement statement in statements) {
if (!statement.Sql.EndsWith(";")) {
statement.Sql = statement.Sql.TrimEnd() + ";";
}
}
return statements;
}
}
And enable it in Configuration.cs:
public Configuration() {
AutomaticMigrationsEnabled = false;
SetSqlGenerator("MySql.Data.MySqlClient", new CustomMySqlMigrationSqlGenerator());
}

Enable hbm2ddl.keywords=auto-quote in Fluent NHibernate

I have made a tiny software tool that allows me to display or run SQL generated from NHibernate. I made this because hbm2ddl.auto is not recommended for production.
I have one problem: when I generate the SQL I always get the infamous Index column unquoted, because I need .AsList() mappings. This prevents me to run the SQL.
In theory, if I had an XML configuration of NHibernate I could use hbm2ddl.keywords tag, but unfortunately since my tool is designed as a DBA-supporting tool for multiple environments, I must use a programmatic approach.
My approach (redundant) is the following:
private static Configuration BuildNHConfig(string connectionString, DbType dbType, out Dialect requiredDialect)
{
IPersistenceConfigurer persistenceConfigurer;
switch (dbType)
{
case DbType.MySQL:
{
persistenceConfigurer =
MySQLConfiguration
.Standard
.Dialect<MySQL5Dialect>()
.Driver<MySqlDataDriver>()
.FormatSql()
.ShowSql()
.ConnectionString(connectionString);
requiredDialect = new MySQL5Dialect();
break;
}
case DbType.MsSqlAzure:
{
persistenceConfigurer = MsSqlConfiguration.MsSql2008
.Dialect<MsSqlAzure2008Dialect>()
.Driver<SqlClientDriver>()
.FormatSql()
.ShowSql()
.ConnectionString(connectionString);
requiredDialect = new MsSqlAzure2008Dialect();
break;
}
default:
{
throw new NotImplementedException();
}
}
FluentConfiguration fc = Fluently.Configure()
.Database(persistenceConfigurer)
.ExposeConfiguration(
cfg => cfg.SetProperty("hbm2ddl.keywords", "keywords")
.SetProperty("hbm2ddl.auto", "none"))
.Mappings(
m => m.FluentMappings.AddFromAssemblyOf<NHibernateFactory>());
Configuration ret = fc.BuildConfiguration();
SchemaMetadataUpdater.QuoteTableAndColumns(ret);
return ret;
}
...
public static void GenerateSql(MainWindowViewModel viewModel)
{
Dialect requiredDialect;
Configuration cfg = BuildNHConfig(viewModel.ConnectionString, viewModel.DbType.Value, out requiredDialect);
StringBuilder sqlBuilder = new StringBuilder();
foreach (string sqlLine in cfg.GenerateSchemaCreationScript(requiredDialect))
sqlBuilder.AppendLine(sqlLine);
viewModel.Sql = sqlBuilder.ToString();
}
Explanation: when I want to set the ViewModel's SQL to display on a TextBox (yea, this is WPF) I initialize the configuration programmatically with connection string given in ViewModel and choose the dialect/provider accordingly. When I Fluently Configure NHibernate I both set hbm2ddl.keywords (tried both auto-quote and keywords, this being the default) and, following this blog post, I also use the SchemaMetadataUpdater.
The result is that I'm always presented with SQL like
create table `OrderHistoryEvent` (Id BIGINT NOT NULL AUTO_INCREMENT, EventType VARCHAR(255) not null, EventTime DATETIME not null, EntityType VARCHAR(255), Comments VARCHAR(255), Order_id VARCHAR(255), Index INTEGER, primary key (Id))
where the guilty Index column is not quoted.
The question is: given a programmatic and fluent configuration of NHibernate, how do I tell NHibernate to quote any reserved word in the SQL exported by GenerateSchemaCreationScript?
I have found a workaround: when I generate the update script (the one that runs with hbm2ddl.auto=update) the script is correctly quoted.
The infamous Index column has been already discussed and from my findings it's hardcoded in FNH (ToManyBase.cs, method public T AsList()).
Since the update script is a perfectly working creational script on an empty database, changing the code to generate an update script on an empty DB should equal generating a creational script.
This happens only because I want to generate the script on my own. There is probably a bug in NHibernate that only activates when you call GenerateSchemaCreationScript and not when you let your SessionFactory build the DB for you

TIMESTAMP column not updating

I am using Struts2, Spring and Hibernate in my application and database is MySQL 5.5. I have this table in database:
create table if not exists campaigns(
id int(10) not null auto_increment,
campaignId int(25) not null unique,
createdBy int(25) not null REFERENCES users(userId),
campaignName varchar(255) not null,
subject varchar(500),
body varchar(50000),
modifiedOn TIMESTAMP,
triggeredOn date,
numberOfTargets int(10),
primary key (id, campaignId)
);
And I save and update the "Campaign" objects with the following methods (hibernate-mapping through hbm files) :
public boolean addCampaign(long createdBy, String campaignName) throws NoSuchAlgorithmException {
Campaign campaignObject = new Campaign();
SecureRandom generatedHash = SecureRandom.getInstance("SHA1PRNG");
campaignObject.setCampaignId(new Integer(generatedHash.nextInt()));
campaignObject.setCreatedBy(createdBy);
campaignObject.setCampaignName(campaignName);
getHibernateTemplate().save(campaignObject);
getSession().flush();
return true;
}
public Date updateCampaign(String campaignId, String subject, String body) throws NumberFormatException {
Campaign campaign = getCampaignByCampaignId(Long.parseLong(campaignId));
if(campaign != null) {
campaign.setSubject(subject);
campaign.setBody(body);
getHibernateTemplate().save(campaign);
getSession().flush();
return campaign.getModifiedOn();
}
return null;
}
The "modifiedOn" column updates when I run a update query on database. But hibernate is failing to update it. Thanks for your time.
First of all, save is for inserting a new entity. You should not use it when updating an attached entity. An attached entity's state is automatically written in the database (if changed) at flush time. You don't need to call anything to have the state updated.
And Hibernate won't magically re-read the row it has inserted/updated to get the generated timestamp. A specific #Generated annotation is needed to do that. But it will decrease the performance of the application.
I would use a pre-insert/pre-update hook to set the modifiedOn value programmatically in the entity, and avoid auto-modified timestamps in the database.
In addition to JB Nizet's response, if modifiedOn is being updated by a trigger, take a look at org.hibernate.Session#refresh().
In case the field is updated by a trigger, when hibernate saves the entity it has the old date, the trigger udpates the record at DB level (not the hibernate entity), and then when the Session closes, at commit time, Hibernate sees the entity as dirty because the modifiedOn field has a different value in DB. So, another update is launched and it is as if the trigger never updated the field. refresh() will update the entity's state with the one from the DB after the update, and the trigger execution, so they can be in synch at commit time.
public Date updateCampaign(String campaignId, String subject, String body)
throws NumberFormatException {
Campaign campaign = getCampaignByCampaignId(Long.parseLong(campaignId));
if(campaign != null) {
campaign.setSubject(subject);
campaign.setBody(body);
getHibernateTemplate().update(campaign);
getSession().flush();
getSession.refresh(campaign);
return campaign.getModifiedOn();
}
return null;
}

Linq to SQL concurrency problem

Hallo,
I have web service that has multiple methods that can be called. Each time one of these methods is called I am logging the call to a statistics database so we know how many times each method is called each month and the average process time.
Each time I log statistic data I first check the database to see if that method for the current month already exists, if not the row is created and added. If it already exists I update the needed columns to the database.
My problem is that sometimes when I update a row I get the "Row not found or changed" exception and yes I know it is because the row has been modified since I read it.
To solve this I have tried using the following without success:
Use using around my datacontext.
Use using around a TransactionScope.
Use a mutex, this doesn’t work because the web service is (not sure I am calling it the right think) replicated out on different PC for performance but still using the same database.
Resolve concurrency conflict in the exception, this doesn’t work because I need to get the new database value and add a value to it.
Below I have added the code used to log the statistics data. Any help would be appreciated very much.
public class StatisticsGateway : IStatisticsGateway
{
#region member variables
private StatisticsDataContext db;
#endregion
#region Singleton
[ThreadStatic]
private static IStatisticsGateway instance;
[ThreadStatic]
private static DateTime lastEntryTime = DateTime.MinValue;
public static IStatisticsGateway Instance
{
get
{
if (!lastEntryTime.Equals(OperationState.EntryTime) || instance == null)
{
instance = new StatisticsGateway();
lastEntryTime = OperationState.EntryTime;
}
return instance;
}
}
#endregion
#region constructor / initialize
private StatisticsGateway()
{
var configurationAppSettings = new System.Configuration.AppSettingsReader();
var connectionString = ((string)(configurationAppSettings.GetValue("sqlConnection1.ConnectionString", typeof(string))));
db = new StatisticsDataContext(connectionString);
}
#endregion
#region IStatisticsGateway members
public void AddStatisticRecord(StatisticRecord record)
{
using (db)
{
var existing = db.Statistics.SingleOrDefault(p => p.MethodName == record.MethodName &&
p.CountryID == record.CountryID &&
p.TokenType == record.TokenType &&
p.Year == record.Year &&
p.Month == record.Month);
if (existing == null)
{
//Add new row
this.AddNewRecord(record);
return;
}
//Update
existing.Count += record.Count;
existing.TotalTimeValue += record.TotalTimeValue;
db.SubmitChanges();
}
}
I would suggest letting SQL Server deal with the concurrency.
Here's how:
Create a stored procedure that accepts your log values (method name, month/date, and execution statistics) as arguments.
In the stored procedure, before anything else, get an application lock as described here, and here. Now you can be sure only one instance of the stored procedure will be running at once. (Disclaimer! I have not tried sp_getapplock myself. Just saying. But it seems fairly straightforward, given all the examples out there on the interwebs.)
Next, in the stored procedure, query the log table for a current-month's entry for the method to determine whether to insert or update, and then do the insert or update.
As you may know, in VS you can drag stored procedures from the Server Explorer into the DBML designer for easy access with LINQ to SQL.
If you're trying to avoid stored procedures then this solution obviously won't be for you, but it's how I'd solve it easily and quickly. Hope it helps!
If you don't want to use the stored procedure approach, a crude way of dealing with it would simply be retrying on that specific exception. E.g:
int maxRetryCount = 5;
for (int i = 0; i < maxRetryCount; i++)
{
try
{
QueryAndUpdateDB();
break;
}
catch(RowUpdateException ex)
{
if (i == maxRetryCount) throw;
}
}
I have not used the sp_getapplock, instead I have used HOLDLOCK and ROWLOCK as seen below:
CREATE PROCEDURE [dbo].[UpdateStatistics]
#MethodName as varchar(50) = null,
#CountryID as varchar(2) = null,
#TokenType as varchar(5) = null,
#Year as int,
#Month as int,
#Count bigint,
#TotalTimeValue bigint
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRAN
UPDATE dbo.[Statistics]
WITH (HOLDLOCK, ROWLOCK)
SET Count = Count + #Count
WHERE MethodName=#MethodName and CountryID=#CountryID and TokenType=#TokenType and Year=#Year and Month=#Month
IF ##ROWCOUNT=0
INSERT INTO dbo.[Statistics] (MethodName, CountryID, TokenType, TotalTimeValue, Year, Month, Count) values (#MethodName, #CountryID, #TokenType, #TotalTimeValue, #Year, #Month, #Count)
COMMIT TRAN
END
GO
I have tested it by calling my web service methods by multiple threads simultaneous and each call is logged without any problems.